thumbgate 1.34.3 → 1.37.1
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/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
- package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
- package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
- package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
- package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/llms.txt +1 -0
- package/.well-known/mcp/server-card.json +1 -1
- package/CONTRIBUTING.md +95 -0
- package/README.md +195 -632
- package/THIRD_PARTY_NOTICES.md +89 -0
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/future-agi/.mcp.json +8 -0
- package/adapters/future-agi/FUTURE_AGI.md +23 -0
- package/adapters/future-agi/config.toml +3 -0
- package/adapters/future-agi/future-agi-bridge.js +9 -0
- package/adapters/future-agi/opencode.json +8 -0
- package/adapters/herdr/herdr-plugin.toml +18 -0
- package/adapters/mcp/server-stdio.js +238 -25
- package/adapters/opencode/opencode.json +1 -1
- package/adapters/workos/WORKOS.md +52 -0
- package/bin/cli.js +373 -5
- package/bin/futureagi-bridge +9 -0
- package/config/gate-templates.json +653 -4
- package/config/gates/actor-critic-audit.json +34 -0
- package/config/gates/default.json +21 -2
- package/config/gates/five-walls-governance.json +34 -0
- package/config/gates/future-agi-guardrails.json +34 -0
- package/config/gates/radware-threat-defense-2026.json +61 -0
- package/config/gates/simatree-data-governance.json +33 -0
- package/config/mcp-allowlists.json +4 -0
- package/config/merge-quality-checks.json +10 -1
- package/config/model-candidates.json +382 -24
- package/config/model-tiers.json +18 -0
- package/config/post-deploy-marketing-pages.json +10 -0
- package/config/progressive/01-wire-only.json +11 -0
- package/config/progressive/02-dashboard-empty-ok.json +10 -0
- package/config/progressive/03-one-lesson.json +10 -0
- package/config/progressive/04-warn-fires.json +11 -0
- package/config/progressive/05-strict-optional.json +11 -0
- package/config/progressive/README.md +15 -0
- package/config/schemas/broker-execution-receipt.schema.json +139 -0
- package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
- package/conformance/provider-attestation/vectors.json +320 -0
- package/docs/specs/provider-execution-attestation-v1.md +69 -0
- package/openapi/openapi.yaml +15 -0
- package/package.json +401 -147
- package/public/about.html +2 -2
- package/public/ai-malpractice-prevention.html +7 -7
- package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
- package/public/blog/a-receipt-is-not-world-state.html +388 -0
- package/public/blog/git-at-agent-scale.html +374 -0
- package/public/blog/no-llm-in-the-gate.html +133 -0
- package/public/blog.html +80 -0
- package/public/case-studies.html +16 -1
- package/public/compare.html +28 -0
- package/public/diagnostic.html +216 -7
- package/public/docs/connectors.html +39 -0
- package/public/federal.html +2 -2
- package/public/founders.html +639 -0
- package/public/index.html +87 -9
- package/public/install.html +8 -8
- package/public/learn.html +39 -0
- package/public/numbers.html +2 -2
- package/public/peter.html +310 -0
- package/public/platform-partners.html +119 -0
- package/public/pricing.html +24 -3
- package/public/privacy.html +117 -0
- package/public/pro.html +17 -0
- package/public/support.html +62 -0
- package/public/terms.html +130 -0
- package/public/third-party-notices.html +95 -0
- package/public/yt.html +351 -0
- package/scripts/action-receipts.js +133 -3
- package/scripts/adaptive-governance-arena.js +349 -0
- package/scripts/admin-override.js +205 -0
- package/scripts/agent-action-inventory.js +869 -0
- package/scripts/agent-audit-trace.js +42 -2
- package/scripts/agent-egress-policy.js +1117 -0
- package/scripts/agent-memory-lifecycle.js +141 -2
- package/scripts/agent-operations-planner.js +441 -1
- package/scripts/agent-readiness.js +68 -0
- package/scripts/agent-security-central.js +647 -0
- package/scripts/allowlist-bridge-honesty.js +417 -0
- package/scripts/async-job-runner.js +102 -11
- package/scripts/audit-trail.js +212 -0
- package/scripts/auto-promote-gates.js +178 -27
- package/scripts/billing.js +1 -1
- package/scripts/broker-execution-receipts.js +719 -0
- package/scripts/budget-aware-gates-proof.js +423 -0
- package/scripts/claude-feedback-sync.js +29 -3
- package/scripts/claw-harness-production.js +237 -0
- package/scripts/cli-progress.js +111 -0
- package/scripts/cli-schema.js +163 -1
- package/scripts/codex-runbook-flywheel.js +318 -0
- package/scripts/context-footprint.js +186 -0
- package/scripts/contextfs.js +143 -61
- package/scripts/dashboard-limits.js +27 -0
- package/scripts/dashboard.js +279 -9
- package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
- package/scripts/docker-sandbox-planner.js +18 -0
- package/scripts/double-blind-eval-protocol.js +252 -0
- package/scripts/edotenv-rl-gateway.js +259 -0
- package/scripts/ensure-production-search-corpus.js +162 -0
- package/scripts/eval-holdout.js +311 -0
- package/scripts/feedback-aggregate.js +21 -2
- package/scripts/feedback-loop.js +87 -5
- package/scripts/feedback-quality.js +9 -0
- package/scripts/file-ledger-lock.js +4 -1
- package/scripts/financial-control-plane.js +41 -1
- package/scripts/find-dormant-requires.js +118 -0
- package/scripts/fs-utils.js +84 -8
- package/scripts/gate-stats.js +2 -2
- package/scripts/gates-engine.js +859 -58
- package/scripts/generate-case-study-outreach.js +24 -15
- package/scripts/git-at-scale.js +628 -0
- package/scripts/governance-conflict-audit.js +1650 -0
- package/scripts/governance-difficulty-curriculum.js +328 -0
- package/scripts/graphrag-retrieval.js +275 -0
- package/scripts/gurobi-optimizer.js +324 -0
- package/scripts/gurobi_optimizer.py +485 -0
- package/scripts/harness-selector.js +82 -1
- package/scripts/hidden-entry-points.js +284 -0
- package/scripts/human-escalation.js +199 -1
- package/scripts/hybrid-feedback-context.js +152 -19
- package/scripts/intent-governed-execution.js +602 -0
- package/scripts/intervention-policy.js +123 -20
- package/scripts/jit-harness-compose.js +628 -0
- package/scripts/jsonl-watcher.js +10 -0
- package/scripts/lesson-embedding-index.js +95 -12
- package/scripts/lesson-retrieval.js +105 -19
- package/scripts/local-model-profile.js +19 -2
- package/scripts/mailer/resend-mailer.js +1 -1
- package/scripts/matryoshka-embedding.js +235 -0
- package/scripts/mcp-oauth.js +42 -4
- package/scripts/mcp-session-handles.js +1016 -0
- package/scripts/mcp-wiring-doctor.js +314 -0
- package/scripts/memory-firewall.js +115 -2
- package/scripts/memory-scope-readiness.js +299 -0
- package/scripts/memory-vs-rag-route.js +161 -0
- package/scripts/model-tier-router.js +148 -21
- package/scripts/nvidia-specdecode-al-doctor.js +536 -0
- package/scripts/openui-catalog-compose-honesty.js +593 -0
- package/scripts/operational-integrity.js +19 -1
- package/scripts/override-audit.js +213 -0
- package/scripts/package-manager-honesty-doctor.js +458 -0
- package/scripts/pr-manager.js +63 -1
- package/scripts/prove-herdr-adapter.js +52 -0
- package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
- package/scripts/prove-workos.js +73 -0
- package/scripts/provider-attestation-conformance.js +192 -0
- package/scripts/provider-receipt-contract.js +136 -0
- package/scripts/qwen38-max-cost-optimizer.js +401 -0
- package/scripts/radware-threat-defense.js +280 -0
- package/scripts/rag-embedding-identity.js +221 -0
- package/scripts/rag-precision-guardrails.js +112 -2
- package/scripts/remote-feedback-capture.js +159 -0
- package/scripts/research-agent-harness.js +256 -0
- package/scripts/rsi-safety-hillclimb.js +200 -0
- package/scripts/rule-sprawl.js +188 -0
- package/scripts/schedule-manager.js +147 -0
- package/scripts/self-heal.js +8 -0
- package/scripts/session-lease.js +415 -0
- package/scripts/simatree-data-governance.js +347 -0
- package/scripts/slo-alert-engine.js +172 -7
- package/scripts/solver-parity.js +539 -0
- package/scripts/stealth-memory-injection-gate.js +333 -0
- package/scripts/switchyard-router.js +366 -0
- package/scripts/telemetry-analytics.js +84 -27
- package/scripts/temporal-decay-weighting.js +138 -0
- package/scripts/test-all.js +165 -0
- package/scripts/token-savings.js +42 -0
- package/scripts/tool-kpi-tracker.js +108 -5
- package/scripts/tool-registry.js +193 -5
- package/scripts/universal-claim-evaluator.js +14 -2
- package/scripts/vector-store.js +279 -9
- package/scripts/workflow-notebook.js +391 -0
- package/scripts/workflow-sentinel.js +111 -12
- package/scripts/workos-production-guard.js +260 -0
- package/scripts/workspace-search-route.js +515 -0
- package/server.json +2 -2
- package/src/agent-identity-boundary.js +76 -0
- package/src/agent-retrieval-cache.js +155 -0
- package/src/alert-noise-ledger.js +502 -0
- package/src/api/server.js +802 -185
- package/src/git-fast-cache.js +220 -0
- package/src/git-wal-sync.js +156 -0
- package/src/hash-anchored-edit.js +82 -0
- package/src/hermes-platform-protocol.js +475 -0
- package/src/hermes-sync-plane.js +241 -0
- package/src/index.js +30 -1
- package/src/iso42001-compliance-guard.js +97 -0
- package/src/latency-budget.js +244 -0
- package/src/mcp-writeguard.js +316 -0
- package/src/miminions-adapter.js +106 -0
- package/src/pipeline-compass.js +104 -0
- package/src/ppl-alert-pipeline.js +284 -0
- package/src/rendezvous-router.js +90 -0
- package/src/security-questionnaire.js +195 -0
|
@@ -153,6 +153,10 @@ function resolveProviderFingerprint(options, vector) {
|
|
|
153
153
|
* Returns [{ id, score }] sorted descending. Embeds the query once; reuses cached
|
|
154
154
|
* document vectors and only embeds new/changed lessons.
|
|
155
155
|
*
|
|
156
|
+
* Fail-closed on embedding identity (Pete Johnson / SDS #1017): never cosine
|
|
157
|
+
* vectors from different providers or base dimensions. Optional progressive
|
|
158
|
+
* Matryoshka: coarse truncate filter, then fine re-rank survivors.
|
|
159
|
+
*
|
|
156
160
|
* @returns {Promise<Array<{id:string, score:number}>>}
|
|
157
161
|
*/
|
|
158
162
|
async function semanticRank(queryText, lessons = [], options = {}) {
|
|
@@ -162,6 +166,8 @@ async function semanticRank(queryText, lessons = [], options = {}) {
|
|
|
162
166
|
persist = true,
|
|
163
167
|
pruneCache = true,
|
|
164
168
|
truncateDimension = null,
|
|
169
|
+
progressiveMatryoshka = false,
|
|
170
|
+
progressiveTopK = null,
|
|
165
171
|
cacheFile = CACHE_FILE,
|
|
166
172
|
} = options;
|
|
167
173
|
if (!queryText || !Array.isArray(lessons) || lessons.length === 0) return [];
|
|
@@ -170,19 +176,36 @@ async function semanticRank(queryText, lessons = [], options = {}) {
|
|
|
170
176
|
const cache = readCache(cachePath);
|
|
171
177
|
let cacheDirty = false;
|
|
172
178
|
|
|
173
|
-
|
|
174
|
-
if (!Array.isArray(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
const provider = resolveProviderFingerprint(options, queryVector);
|
|
180
|
-
const dimension = queryVector.length;
|
|
179
|
+
const fullQueryVector = await embedder(queryText, { kind: 'query', task: 'code retrieval' });
|
|
180
|
+
if (!Array.isArray(fullQueryVector) || fullQueryVector.length === 0) return [];
|
|
181
|
+
|
|
182
|
+
const provider = resolveProviderFingerprint(options, fullQueryVector);
|
|
183
|
+
const dimension = fullQueryVector.length;
|
|
181
184
|
if (!options.embedder && /(?:built-in|feature-hash)/i.test(provider)) {
|
|
182
185
|
throw new Error('Semantic embedding provider degraded to feature hashing');
|
|
183
186
|
}
|
|
184
187
|
|
|
185
|
-
const
|
|
188
|
+
const { assertCompatibleEmbeddings, buildProgressiveRetrievalPlan } = require('./rag-embedding-identity');
|
|
189
|
+
|
|
190
|
+
// Progressive Matryoshka: coarse funnel first, full cosine only for survivors.
|
|
191
|
+
// Never score full-dim then coarse-filter — that is strictly more expensive.
|
|
192
|
+
let progressivePlan = null;
|
|
193
|
+
let coarseDim = null;
|
|
194
|
+
if (progressiveMatryoshka && !truncateDimension) {
|
|
195
|
+
progressivePlan = buildProgressiveRetrievalPlan({ embeddingDim: dimension });
|
|
196
|
+
const plannedCoarse = progressivePlan.stages[0].dimension;
|
|
197
|
+
if (plannedCoarse < dimension) coarseDim = plannedCoarse;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const scoreDim = truncateDimension || null;
|
|
201
|
+
const compareQueryVector = scoreDim
|
|
202
|
+
? truncateVector(fullQueryVector, scoreDim)
|
|
203
|
+
: fullQueryVector;
|
|
204
|
+
const coarseQueryVector = coarseDim
|
|
205
|
+
? truncateVector(fullQueryVector, coarseDim)
|
|
206
|
+
: null;
|
|
207
|
+
|
|
208
|
+
const candidates = [];
|
|
186
209
|
for (const lesson of lessons) {
|
|
187
210
|
if (!lesson || !lesson.id) continue;
|
|
188
211
|
const text = lessonText(lesson);
|
|
@@ -198,6 +221,7 @@ async function semanticRank(queryText, lessons = [], options = {}) {
|
|
|
198
221
|
|| !Array.isArray(entry.vector)
|
|
199
222
|
|| entry.vector.length !== dimension
|
|
200
223
|
) {
|
|
224
|
+
// Provider/dim drift → re-embed. Never cosine across mismatched identities.
|
|
201
225
|
const vector = await embedder(text, {
|
|
202
226
|
kind: 'document',
|
|
203
227
|
task: 'code retrieval',
|
|
@@ -205,13 +229,32 @@ async function semanticRank(queryText, lessons = [], options = {}) {
|
|
|
205
229
|
});
|
|
206
230
|
if (!Array.isArray(vector) || vector.length === 0) continue;
|
|
207
231
|
if (vector.length !== dimension) continue;
|
|
232
|
+
// Re-resolve AFTER each document embed so a same-dim fallback provider
|
|
233
|
+
// cannot be cached under the query provider fingerprint (Codex P1).
|
|
234
|
+
const documentProvider = resolveProviderFingerprint(options, vector);
|
|
235
|
+
const identity = assertCompatibleEmbeddings({
|
|
236
|
+
queryProvider: provider,
|
|
237
|
+
queryDimension: dimension,
|
|
238
|
+
documentProvider,
|
|
239
|
+
documentDimension: vector.length,
|
|
240
|
+
});
|
|
241
|
+
if (!identity.ok) continue;
|
|
242
|
+
// Cache under the query-session provider only when identities match.
|
|
208
243
|
entry = { hash, provider, dimension, vector };
|
|
209
244
|
cache[lesson.id] = entry;
|
|
210
245
|
cacheDirty = true;
|
|
246
|
+
} else {
|
|
247
|
+
// Cached entry still must match the active query identity.
|
|
248
|
+
const identity = assertCompatibleEmbeddings({
|
|
249
|
+
queryProvider: provider,
|
|
250
|
+
queryDimension: dimension,
|
|
251
|
+
documentProvider: entry.provider,
|
|
252
|
+
documentDimension: entry.dimension,
|
|
253
|
+
});
|
|
254
|
+
if (!identity.ok) continue;
|
|
211
255
|
}
|
|
212
256
|
|
|
213
|
-
|
|
214
|
-
scored.push({ id: lesson.id, score: cosineSimilarity(queryVector, docVector) });
|
|
257
|
+
candidates.push({ id: lesson.id, vector: entry.vector });
|
|
215
258
|
}
|
|
216
259
|
|
|
217
260
|
// Prune only when the caller supplied the complete corpus. Metadata-filtered
|
|
@@ -229,7 +272,47 @@ async function semanticRank(queryText, lessons = [], options = {}) {
|
|
|
229
272
|
|
|
230
273
|
if (persist && cacheDirty) writeCache(cachePath, cache);
|
|
231
274
|
|
|
232
|
-
|
|
275
|
+
if (candidates.length === 0) return [];
|
|
276
|
+
|
|
277
|
+
// Coarse-first path: score at coarse dim, then full-dim only survivors.
|
|
278
|
+
if (coarseDim && coarseQueryVector && candidates.length > 1) {
|
|
279
|
+
const coarseScored = candidates.map((row) => ({
|
|
280
|
+
id: row.id,
|
|
281
|
+
score: cosineSimilarity(coarseQueryVector, truncateVector(row.vector, coarseDim)),
|
|
282
|
+
vector: row.vector,
|
|
283
|
+
})).sort((a, b) => b.score - a.score);
|
|
284
|
+
|
|
285
|
+
const keepBase = Math.max(
|
|
286
|
+
1,
|
|
287
|
+
Number(progressiveTopK) || Math.min(candidates.length, Math.ceil(candidates.length / 2) || 8)
|
|
288
|
+
);
|
|
289
|
+
const keep = Math.min(
|
|
290
|
+
coarseScored.length,
|
|
291
|
+
keepBase * (progressivePlan.stages[0].topKMultiplier || 4)
|
|
292
|
+
);
|
|
293
|
+
const survivors = coarseScored.slice(0, keep);
|
|
294
|
+
return survivors
|
|
295
|
+
.map((row) => ({
|
|
296
|
+
id: row.id,
|
|
297
|
+
score: cosineSimilarity(fullQueryVector, row.vector),
|
|
298
|
+
}))
|
|
299
|
+
.sort((a, b) => b.score - a.score);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Standard path: single-pass cosine at requested (or full) dimension.
|
|
303
|
+
return candidates
|
|
304
|
+
.map((row) => {
|
|
305
|
+
const docVector = scoreDim ? truncateVector(row.vector, scoreDim) : row.vector;
|
|
306
|
+
if (docVector.length !== compareQueryVector.length) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return {
|
|
310
|
+
id: row.id,
|
|
311
|
+
score: cosineSimilarity(compareQueryVector, docVector),
|
|
312
|
+
};
|
|
313
|
+
})
|
|
314
|
+
.filter(Boolean)
|
|
315
|
+
.sort((a, b) => b.score - a.score);
|
|
233
316
|
}
|
|
234
317
|
|
|
235
318
|
module.exports = {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const RECENCY_DECAY_DAYS = 30;
|
|
17
|
+
const { DEFAULT_HALF_LIFE_MS, applyTemporalDecay } = require('./temporal-decay-weighting');
|
|
17
18
|
const RERANK_CANDIDATE_POOL = 50; // bi-encoder retrieves this many; reranker picks topK
|
|
18
19
|
const MAX_RETRIEVAL_MEMORY_CHARS = 20000;
|
|
19
20
|
|
|
@@ -173,6 +174,48 @@ async function buildQueryPlan(query, options = {}) {
|
|
|
173
174
|
}
|
|
174
175
|
}
|
|
175
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Measure retrieveRelevantLessons against a wall-clock budget.
|
|
179
|
+
* Concurrent agent load (New Stack retrieval-failure process, not a product):
|
|
180
|
+
* latency stacking is a budget, not "return everything". Over-budget is
|
|
181
|
+
* reported; the caller decides. Does not raise maxResults.
|
|
182
|
+
*/
|
|
183
|
+
function retrieveWithLatencyBudget(toolName, actionContext, options = {}) {
|
|
184
|
+
const started = Date.now();
|
|
185
|
+
const lessons = retrieveRelevantLessons(toolName, actionContext, options);
|
|
186
|
+
const latencyMs = Date.now() - started;
|
|
187
|
+
const budget = Number(options.latencyBudgetMs);
|
|
188
|
+
const hasBudget = Number.isFinite(budget) && budget > 0;
|
|
189
|
+
let oversizedRejected = false;
|
|
190
|
+
try {
|
|
191
|
+
const { getFeedbackPaths, readJSONL } = require('./feedback-loop');
|
|
192
|
+
const pathMod = require('path');
|
|
193
|
+
const paths = options.feedbackDir
|
|
194
|
+
? { MEMORY_LOG_PATH: pathMod.join(options.feedbackDir, 'memory-log.jsonl') }
|
|
195
|
+
: getFeedbackPaths();
|
|
196
|
+
const raw = readJSONL(paths.MEMORY_LOG_PATH, { maxLines: MAX_RETRIEVAL_MEMORY_LINES });
|
|
197
|
+
const maxChars = Number.isFinite(options.maxMemoryChars)
|
|
198
|
+
? Math.max(1, options.maxMemoryChars)
|
|
199
|
+
: MAX_RETRIEVAL_MEMORY_CHARS;
|
|
200
|
+
oversizedRejected = raw.some((m) => {
|
|
201
|
+
if (!m || typeof m !== 'object') return false;
|
|
202
|
+
const combined = `${String(m.title || '')}\n${String(m.content || '')}`.trim();
|
|
203
|
+
return combined.length > maxChars;
|
|
204
|
+
});
|
|
205
|
+
} catch {
|
|
206
|
+
oversizedRejected = false;
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
lessons,
|
|
210
|
+
count: Array.isArray(lessons) ? lessons.length : 0,
|
|
211
|
+
maxResults: options.maxResults == null ? 5 : options.maxResults,
|
|
212
|
+
latencyMs,
|
|
213
|
+
latencyBudgetMs: hasBudget ? budget : null,
|
|
214
|
+
overBudget: hasBudget ? latencyMs > budget : false,
|
|
215
|
+
oversizedRejected,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
176
219
|
function retrieveRelevantLessons(toolName, actionContext, options = {}) {
|
|
177
220
|
const { maxResults = 5, feedbackDir } = options;
|
|
178
221
|
|
|
@@ -228,21 +271,25 @@ function retrieveRelevantLessons(toolName, actionContext, options = {}) {
|
|
|
228
271
|
|
|
229
272
|
const actionSig = buildActionSignature(toolName, actionContext);
|
|
230
273
|
|
|
231
|
-
// Stage 1 — local first-stage score,
|
|
232
|
-
|
|
274
|
+
// Stage 1 — local first-stage score, then a dedupe-aware pool cut. Collapsing
|
|
275
|
+
// near-duplicate clusters BEFORE the top-50 cut keeps a dense cluster from
|
|
276
|
+
// crowding genuinely distinct lessons out of the reranker's candidate pool.
|
|
277
|
+
const scored = memories
|
|
233
278
|
.map((mem) => ({
|
|
234
279
|
...mem,
|
|
235
280
|
relevanceScore: scoreRelevance(mem, toolName, actionContext, actionSig),
|
|
236
281
|
}))
|
|
237
282
|
.filter((m) => m.relevanceScore > 0.1)
|
|
238
|
-
.sort((a, b) => b.relevanceScore - a.relevanceScore)
|
|
239
|
-
|
|
283
|
+
.sort((a, b) => b.relevanceScore - a.relevanceScore);
|
|
284
|
+
const candidates = dedupeCandidatePool(scored);
|
|
240
285
|
|
|
241
286
|
if (candidates.length === 0) return [];
|
|
242
287
|
|
|
243
|
-
// Stage 2 — field-aware BM25F reranker (not a neural cross-encoder)
|
|
288
|
+
// Stage 2 — field-aware BM25F reranker (not a neural cross-encoder). Over-fetch
|
|
289
|
+
// 2× so a post-rerank dedupe collapse backfills from ranked survivors instead of
|
|
290
|
+
// under-filling the caller's slot budget (mirrors the pragmatic path's topK).
|
|
244
291
|
const reranked = rerankLessons(actionContext, candidates, {
|
|
245
|
-
topK: maxResults,
|
|
292
|
+
topK: Math.max(maxResults * 2, maxResults),
|
|
246
293
|
toolName,
|
|
247
294
|
});
|
|
248
295
|
|
|
@@ -251,7 +298,7 @@ function retrieveRelevantLessons(toolName, actionContext, options = {}) {
|
|
|
251
298
|
const deduped = dedupeSupersededLessons(reranked);
|
|
252
299
|
const selected = filterTopP(deduped, resolveTopP(options), { minKeep: options.minKeep });
|
|
253
300
|
|
|
254
|
-
const shaped = selected.map((m) => ({
|
|
301
|
+
const shaped = selected.slice(0, maxResults).map((m) => ({
|
|
255
302
|
id: m.id,
|
|
256
303
|
title: m.title,
|
|
257
304
|
content: m.content,
|
|
@@ -434,8 +481,10 @@ async function retrieveRelevantLessonsAsync(toolName, actionContext, options = {
|
|
|
434
481
|
if (conclusive) {
|
|
435
482
|
// Short-circuit: skip embedding/dense search completely
|
|
436
483
|
const { rerankLessons } = require('./lesson-reranker');
|
|
437
|
-
const reranked = rerankLessons(actionContext, lexicalScored
|
|
438
|
-
return filterTopP(dedupeSupersededLessons(reranked), resolveTopP(options), { minKeep: options.minKeep })
|
|
484
|
+
const reranked = rerankLessons(actionContext, dedupeCandidatePool(lexicalScored), { topK: Math.max(maxResults * 2, maxResults), toolName });
|
|
485
|
+
return filterTopP(dedupeSupersededLessons(reranked), resolveTopP(options), { minKeep: options.minKeep })
|
|
486
|
+
.slice(0, maxResults)
|
|
487
|
+
.map(shapeLesson);
|
|
439
488
|
}
|
|
440
489
|
|
|
441
490
|
const queryPlan = lexicalScored[0]?.relevanceScore >= (options.rewriteBelowScore ?? 0.6)
|
|
@@ -558,8 +607,8 @@ async function retrieveRelevantLessonsAsync(toolName, actionContext, options = {
|
|
|
558
607
|
const lexById = new Map(lexicalScored.map((m) => [m.id, m.relevanceScore]));
|
|
559
608
|
const topFusedScore = fused[0].score || 1;
|
|
560
609
|
|
|
561
|
-
const candidates = fused
|
|
562
|
-
.slice(0, RERANK_CANDIDATE_POOL)
|
|
610
|
+
const candidates = dedupeCandidatePool(fused
|
|
611
|
+
.slice(0, RERANK_CANDIDATE_POOL * 2)
|
|
563
612
|
.map((entry) => {
|
|
564
613
|
const mem = byId.get(entry.id);
|
|
565
614
|
if (!mem) return null;
|
|
@@ -571,13 +620,15 @@ async function retrieveRelevantLessonsAsync(toolName, actionContext, options = {
|
|
|
571
620
|
: entry.score / topFusedScore;
|
|
572
621
|
return { ...mem, relevanceScore };
|
|
573
622
|
})
|
|
574
|
-
.filter(Boolean);
|
|
623
|
+
.filter(Boolean));
|
|
575
624
|
|
|
576
625
|
if (candidates.length === 0) return [];
|
|
577
626
|
|
|
578
627
|
const { rerankLessons } = require('./lesson-reranker');
|
|
579
|
-
const reranked = rerankLessons(actionContext, candidates, { topK: maxResults, toolName });
|
|
580
|
-
const rows = filterTopP(dedupeSupersededLessons(reranked), resolveTopP(options), { minKeep: options.minKeep })
|
|
628
|
+
const reranked = rerankLessons(actionContext, candidates, { topK: Math.max(maxResults * 2, maxResults), toolName });
|
|
629
|
+
const rows = filterTopP(dedupeSupersededLessons(reranked), resolveTopP(options), { minKeep: options.minKeep })
|
|
630
|
+
.slice(0, maxResults)
|
|
631
|
+
.map(shapeLesson);
|
|
581
632
|
return attachArrayRetrievalMeta(rows, {
|
|
582
633
|
strategy: 'hybrid-rrf+bm25',
|
|
583
634
|
indexUpdatedAtMs: options.indexUpdatedAtMs ?? null,
|
|
@@ -645,11 +696,26 @@ function scoreRelevance(memory, toolName, actionContext, actionSig) {
|
|
|
645
696
|
|
|
646
697
|
if (memory.tags?.includes('negative')) score += 0.1;
|
|
647
698
|
|
|
648
|
-
if (memory.timestamp) {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
699
|
+
if (memory.timestamp || memory.receivedAt || memory.created_at) {
|
|
700
|
+
// Half-life temporal decay (scripts/temporal-decay-weighting.js), applied as
|
|
701
|
+
// a recency *factor* with the legacy 0.5 floor so old-but-still-valid
|
|
702
|
+
// lessons remain retrievable (pure exp decay zeroed fixtures in CI).
|
|
703
|
+
const stamp = memory.timestamp || memory.receivedAt || memory.created_at;
|
|
704
|
+
const halfLifeMs =
|
|
705
|
+
Number.isFinite(Number(process.env.THUMBGATE_RETRIEVAL_HALF_LIFE_MS)) &&
|
|
706
|
+
Number(process.env.THUMBGATE_RETRIEVAL_HALF_LIFE_MS) > 0
|
|
707
|
+
? Number(process.env.THUMBGATE_RETRIEVAL_HALF_LIFE_MS)
|
|
708
|
+
: DEFAULT_HALF_LIFE_MS;
|
|
709
|
+
const activeMode = process.env.THUMBGATE_RETRIEVAL_ACTIVE_MODE === '1';
|
|
710
|
+
try {
|
|
711
|
+
const factor = applyTemporalDecay(1, stamp, halfLifeMs, activeMode);
|
|
712
|
+
score *= 0.5 + 0.5 * Math.max(0, Math.min(1, factor));
|
|
713
|
+
} catch {
|
|
714
|
+
const ageMs = Date.now() - new Date(stamp).getTime();
|
|
715
|
+
const ageDays = ageMs / (1000 * 60 * 60 * 24);
|
|
716
|
+
const decay = Math.max(0, 1 - ageDays / RECENCY_DECAY_DAYS);
|
|
717
|
+
score *= 0.5 + 0.5 * decay;
|
|
718
|
+
}
|
|
653
719
|
}
|
|
654
720
|
|
|
655
721
|
if (memory.structuredRule) score += 0.15;
|
|
@@ -791,6 +857,24 @@ function dedupeSupersededLessons(lessons, options = {}) {
|
|
|
791
857
|
return kept.map((idx) => lessons[idx]);
|
|
792
858
|
}
|
|
793
859
|
|
|
860
|
+
/**
|
|
861
|
+
* Dedupe-aware candidate-pool cut. dedupeSupersededLessons() historically ran only
|
|
862
|
+
* AFTER the RERANK_CANDIDATE_POOL cut, so a dense cluster of near-duplicate lessons
|
|
863
|
+
* could fill the pool and crowd genuinely distinct lessons out before dedupe ever
|
|
864
|
+
* saw them. Collapse duplicates over a bounded window (pool × 2, keeping the O(n²)
|
|
865
|
+
* bigram pass cheap in the hook's hot path) BEFORE cutting to pool size. Clusters
|
|
866
|
+
* larger than the window can still crowd; store compaction is the durable fix.
|
|
867
|
+
*
|
|
868
|
+
* @param {Array<object>} scored - candidates sorted best-first
|
|
869
|
+
* @param {number} [pool=RERANK_CANDIDATE_POOL] - final pool size
|
|
870
|
+
* @returns {Array<object>} deduped best-first list, at most `pool` long
|
|
871
|
+
*/
|
|
872
|
+
function dedupeCandidatePool(scored, pool = RERANK_CANDIDATE_POOL) {
|
|
873
|
+
if (!Array.isArray(scored) || scored.length === 0) return [];
|
|
874
|
+
const bound = Math.max(1, Math.floor(pool));
|
|
875
|
+
return dedupeSupersededLessons(scored.slice(0, bound * 2)).slice(0, bound);
|
|
876
|
+
}
|
|
877
|
+
|
|
794
878
|
function calculateRetrievalEntropy(lessons) {
|
|
795
879
|
if (!Array.isArray(lessons) || lessons.length === 0) return 0;
|
|
796
880
|
let pW = 0, nW = 0, tW = 0;
|
|
@@ -807,6 +891,7 @@ function calculateRetrievalEntropy(lessons) {
|
|
|
807
891
|
|
|
808
892
|
module.exports = {
|
|
809
893
|
retrieveRelevantLessons,
|
|
894
|
+
retrieveWithLatencyBudget,
|
|
810
895
|
retrieveRelevantLessonsAsync,
|
|
811
896
|
reciprocalRankFusion,
|
|
812
897
|
scoreRelevance,
|
|
@@ -817,6 +902,7 @@ module.exports = {
|
|
|
817
902
|
filterTopP,
|
|
818
903
|
resolveTopP,
|
|
819
904
|
dedupeSupersededLessons,
|
|
905
|
+
dedupeCandidatePool,
|
|
820
906
|
isRetrievableMemory,
|
|
821
907
|
selectRetrievalMemories,
|
|
822
908
|
matchesMetadataFilters,
|
|
@@ -33,6 +33,16 @@ const GLM_MODEL_ROLES = {
|
|
|
33
33
|
vlm: 'glm-4v-9b',
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
// vLLM / vllm.ai high-throughput model IDs for self-hosted local inference serving.
|
|
37
|
+
// Activate by setting THUMBGATE_LOCAL_MODEL_FAMILY=vllm or THUMBGATE_LOCAL_LLM_ENDPOINT.
|
|
38
|
+
const VLLM_MODEL_ROLES = {
|
|
39
|
+
normal: 'Qwen/Qwen2.5-Coder-32B-Instruct',
|
|
40
|
+
thinking: 'deepseek-ai/DeepSeek-R1-Distill-Qwen-32B',
|
|
41
|
+
critique: 'Qwen/Qwen2.5-Coder-32B-Instruct',
|
|
42
|
+
compaction: 'Qwen/Qwen2.5-Coder-7B-Instruct',
|
|
43
|
+
vlm: 'Qwen/Qwen2-VL-7B-Instruct',
|
|
44
|
+
};
|
|
45
|
+
|
|
36
46
|
const VALID_MODEL_ROLES = Object.keys(MODEL_ROLES);
|
|
37
47
|
|
|
38
48
|
const EMBEDDING_PROFILES = {
|
|
@@ -342,9 +352,15 @@ function resolveModelRole(role, env) {
|
|
|
342
352
|
const envKey = `THUMBGATE_MODEL_ROLE_${normalized.toUpperCase()}`;
|
|
343
353
|
const modelFamily = resolveModelFamily(e);
|
|
344
354
|
const isLocalGlm = modelFamily.startsWith('glm');
|
|
355
|
+
// Only explicit vLLM family/server selects VLLM_MODEL_ROLES. A generic
|
|
356
|
+
// THUMBGATE_LOCAL_LLM_ENDPOINT (Ollama, LM Studio, etc.) must keep its own model id.
|
|
357
|
+
const serverEngine = normalizeSlug(e.THUMBGATE_LOCAL_MODEL_SERVER || e.THUMBGATE_MODEL_SERVER || '');
|
|
358
|
+
const isLocalVllm = modelFamily.startsWith('vllm') || serverEngine === 'vllm';
|
|
345
359
|
const providerMode = resolveProviderMode(e);
|
|
346
|
-
const provider = isLocalGlm ? 'local' : (providerMode === 'vertex' ? 'vertex' : 'gemini');
|
|
347
|
-
const defaultModel =
|
|
360
|
+
const provider = (isLocalGlm || isLocalVllm) ? 'local' : (providerMode === 'vertex' ? 'vertex' : 'gemini');
|
|
361
|
+
const defaultModel = isLocalVllm
|
|
362
|
+
? (VLLM_MODEL_ROLES[normalized] || MODEL_ROLES[normalized])
|
|
363
|
+
: (isLocalGlm ? (GLM_MODEL_ROLES[normalized] || MODEL_ROLES[normalized]) : MODEL_ROLES[normalized]);
|
|
348
364
|
const model = (e[envKey] && String(e[envKey]).trim()) || defaultModel;
|
|
349
365
|
return { role: normalized, model, provider, envKey };
|
|
350
366
|
}
|
|
@@ -384,6 +400,7 @@ module.exports = {
|
|
|
384
400
|
DEFAULT_FEEDBACK_DIR,
|
|
385
401
|
EMBEDDING_PROFILES,
|
|
386
402
|
GLM_MODEL_ROLES,
|
|
403
|
+
VLLM_MODEL_ROLES,
|
|
387
404
|
INDEXCACHE_SERVER_ENGINES,
|
|
388
405
|
LONG_CONTEXT_TAGS,
|
|
389
406
|
LONG_CONTEXT_TASK_TYPES,
|
|
@@ -19,7 +19,7 @@ const dns = require('node:dns').promises;
|
|
|
19
19
|
|
|
20
20
|
const PRODUCT_NAME = 'ThumbGate Pro';
|
|
21
21
|
const DASHBOARD_URL = 'https://thumbgate-production.up.railway.app/dashboard';
|
|
22
|
-
const DEFAULT_CONTACT_EMAIL = '
|
|
22
|
+
const DEFAULT_CONTACT_EMAIL = 'support@thumbgate.ai';
|
|
23
23
|
const DEFAULT_FROM = 'onboarding@resend.dev';
|
|
24
24
|
const DEFAULT_REPLY_TO = DEFAULT_CONTACT_EMAIL;
|
|
25
25
|
const DEFAULT_UNSUBSCRIBE_EMAIL = DEFAULT_CONTACT_EMAIL;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Matryoshka Embedding Implementation for ThumbGate
|
|
6
|
+
*
|
|
7
|
+
* Based on podcast insights from "The RAG Mistake Almost Every Team Is Making"
|
|
8
|
+
* Episode #1017, featuring Pete Johnson (Field CTO, AI at MongoDB)
|
|
9
|
+
*
|
|
10
|
+
* Matryoshka embeddings are nested representations where:
|
|
11
|
+
* - Lower dimensions contain meaningful semantic information at multiple granularities
|
|
12
|
+
* - Enables efficient retrieval without full-dimensional computation
|
|
13
|
+
* - Supports progressive disclosure: query at 256 dims, refine at 1024, full at 3072
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const PYRAMID_LAYERS = {
|
|
17
|
+
L0_CONVERSATION: 'L0_CONVERSATION',
|
|
18
|
+
L1_ATOM: 'L1_ATOM',
|
|
19
|
+
L2_SCENARIO: 'L2_SCENARIO',
|
|
20
|
+
L3_PERSONA_SOP: 'L3_PERSONA_SOP',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Matryoshka dimension tiers - each tier contains the semantic information of lower tiers
|
|
24
|
+
const MATRYOSHKA_DIMENSIONS = [256, 512, 768, 1024, 1536, 2048, 3072, 4096, 7680];
|
|
25
|
+
|
|
26
|
+
// Embedding model quality thresholds for RAG pipelines
|
|
27
|
+
const EMBEDDING_QUALITY_THRESHOLDS = {
|
|
28
|
+
recall: 0.95, // Minimum recall@k for baseline
|
|
29
|
+
precision: 0.15, // Minimum precision@k (covers 15% correctly)
|
|
30
|
+
cross_encoder_reranker: true, // Use cross-encoder for refinement
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function normalizeToMatryoshkaDimension(requestedDim) {
|
|
34
|
+
const dim = Number(requestedDim) || 768;
|
|
35
|
+
if (dim <= 0 || !Number.isFinite(dim)) return 768;
|
|
36
|
+
let closest = MATRYOSHKA_DIMENSIONS[0];
|
|
37
|
+
let minDiff = Math.abs(dim - closest);
|
|
38
|
+
for (const tier of MATRYOSHKA_DIMENSIONS) {
|
|
39
|
+
const diff = Math.abs(tier - dim);
|
|
40
|
+
if (diff < minDiff) { closest = tier; minDiff = diff; }
|
|
41
|
+
}
|
|
42
|
+
return closest;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getSemanticScore(dim) {
|
|
46
|
+
const tierIndex = MATRYOSHKA_DIMENSIONS.indexOf(dim);
|
|
47
|
+
if (tierIndex === -1) return 0;
|
|
48
|
+
return (tierIndex + 1) / MATRYOSHKA_DIMENSIONS.length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function classifyMemoryLayer(memory = {}) {
|
|
52
|
+
const type = String(memory.type || 'episodic').toLowerCase();
|
|
53
|
+
const content = String(memory.content || '').toLowerCase();
|
|
54
|
+
const tags = Array.isArray(memory.tags)
|
|
55
|
+
? new Set(memory.tags.map(t => String(t).toLowerCase()))
|
|
56
|
+
: new Set();
|
|
57
|
+
|
|
58
|
+
const transportLike = type === 'transcript'
|
|
59
|
+
|| tags.has('transport')
|
|
60
|
+
|| tags.has('transcript')
|
|
61
|
+
|| /^(user|assistant|system|tool)\s*:/m.test(content);
|
|
62
|
+
if (transportLike) return PYRAMID_LAYERS.L0_CONVERSATION;
|
|
63
|
+
|
|
64
|
+
const explicitPolicy = /^(never|always|must|do not|require)\b/i.test(content.trim());
|
|
65
|
+
if (type === 'preference' || tags.has('sop') || tags.has('rule') || tags.has('policy')
|
|
66
|
+
|| tags.has('guardrail') || explicitPolicy) {
|
|
67
|
+
return PYRAMID_LAYERS.L3_PERSONA_SOP;
|
|
68
|
+
}
|
|
69
|
+
if (type === 'procedural' || tags.has('workflow') || tags.has('scenario')
|
|
70
|
+
|| tags.has('pipeline') || tags.has('playbook')) {
|
|
71
|
+
return PYRAMID_LAYERS.L2_SCENARIO;
|
|
72
|
+
}
|
|
73
|
+
if (type === 'semantic' || tags.has('fact') || tags.has('lesson') || tags.has('atom')
|
|
74
|
+
|| memory.whatWentWrong || memory.whatWorked) {
|
|
75
|
+
return PYRAMID_LAYERS.L1_ATOM;
|
|
76
|
+
}
|
|
77
|
+
return PYRAMID_LAYERS.L0_CONVERSATION;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildEmbeddingTaskPrefix(layer, task = 'code retrieval') {
|
|
81
|
+
const layerWeights = {
|
|
82
|
+
[PYRAMID_LAYERS.L3_PERSONA_SOP]: 4,
|
|
83
|
+
[PYRAMID_LAYERS.L2_SCENARIO]: 3,
|
|
84
|
+
[PYRAMID_LAYERS.L1_ATOM]: 2,
|
|
85
|
+
[PYRAMID_LAYERS.L0_CONVERSATION]: 1,
|
|
86
|
+
};
|
|
87
|
+
const weight = layerWeights[layer] || 1;
|
|
88
|
+
const normalizedTask = String(task).trim().toLowerCase().replace(/[_-]+/g, ' ');
|
|
89
|
+
return `layer:${layer} weight:${weight} task:${normalizedTask} `;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function validateEmbeddingQuality({
|
|
93
|
+
recall,
|
|
94
|
+
precision,
|
|
95
|
+
recallBaseline,
|
|
96
|
+
precisionBaseline,
|
|
97
|
+
embeddingDim,
|
|
98
|
+
goldenCases,
|
|
99
|
+
perCaseRecall,
|
|
100
|
+
} = {}) {
|
|
101
|
+
const issues = [];
|
|
102
|
+
if (!Number.isFinite(recall)) {
|
|
103
|
+
issues.push({ severity: 'high', issue: 'recall_missing_or_non_finite', actual: recall, recommended: 'Run the deterministic golden retrieval suite.' });
|
|
104
|
+
} else if (recall < EMBEDDING_QUALITY_THRESHOLDS.recall) {
|
|
105
|
+
issues.push({ severity: 'high', issue: 'recall_below_threshold', actual: recall, expected: EMBEDDING_QUALITY_THRESHOLDS.recall, recommended: 'Lower embedding dimension or use hybrid retrieval' });
|
|
106
|
+
}
|
|
107
|
+
if (!Number.isFinite(precision)) {
|
|
108
|
+
issues.push({ severity: 'high', issue: 'precision_missing_or_non_finite', actual: precision, recommended: 'Measure deterministic precision before promotion.' });
|
|
109
|
+
} else if (precision < EMBEDDING_QUALITY_THRESHOLDS.precision) {
|
|
110
|
+
issues.push({ severity: 'medium', issue: 'precision_below_threshold', actual: precision, expected: EMBEDDING_QUALITY_THRESHOLDS.precision, recommended: 'Enable cross-encoder reranker or increase dimension tier' });
|
|
111
|
+
}
|
|
112
|
+
if (!Number.isInteger(goldenCases) || goldenCases < 6) {
|
|
113
|
+
issues.push({ severity: 'high', issue: 'insufficient_golden_cases', actual: goldenCases, expected: 6, recommended: 'Provide at least six deterministic golden cases.' });
|
|
114
|
+
}
|
|
115
|
+
if (!Array.isArray(perCaseRecall) || perCaseRecall.length < 6
|
|
116
|
+
|| perCaseRecall.some((value) => !Number.isFinite(value) || value < 1)) {
|
|
117
|
+
issues.push({ severity: 'high', issue: 'per_case_recall_incomplete', actual: perCaseRecall, expected: 'at least 6 cases at recall 1.0', recommended: 'Require 100% recall for every golden case.' });
|
|
118
|
+
}
|
|
119
|
+
if (Number.isFinite(recallBaseline) && Number.isFinite(precisionBaseline)
|
|
120
|
+
&& Number.isFinite(recall) && Number.isFinite(precision)) {
|
|
121
|
+
if (recall < recallBaseline * 0.95) {
|
|
122
|
+
issues.push({ severity: 'high', issue: 'recall_regression_from_baseline', actual: recall, baseline: recallBaseline, recommended: 'Retrain embedding model with baseline preserved' });
|
|
123
|
+
}
|
|
124
|
+
if (precision < precisionBaseline * 0.95) {
|
|
125
|
+
issues.push({ severity: 'medium', issue: 'precision_regression_from_baseline', actual: precision, baseline: precisionBaseline, recommended: 'Verify embedding changes maintained retrieval quality' });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
valid: issues.length === 0,
|
|
130
|
+
issues,
|
|
131
|
+
dimensionTier: normalizeToMatryoshkaDimension(embeddingDim || 768),
|
|
132
|
+
semanticScore: getSemanticScore(normalizeToMatryoshkaDimension(embeddingDim || 768)),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildMatryoshkaConfig(options = {}) {
|
|
137
|
+
const baseDim = normalizeToMatryoshkaDimension(options.embeddingDim || 768);
|
|
138
|
+
const tiers = [];
|
|
139
|
+
let currentDim = 256;
|
|
140
|
+
while (currentDim <= baseDim) {
|
|
141
|
+
if (MATRYOSHKA_DIMENSIONS.includes(currentDim)) {
|
|
142
|
+
tiers.push({
|
|
143
|
+
dimension: currentDim,
|
|
144
|
+
semanticScore: getSemanticScore(currentDim),
|
|
145
|
+
useCase: currentDim < 512 ? 'fast_query' : currentDim < 1536 ? 'balanced' : 'full_precision',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
currentDim += 256;
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
baseDimension: baseDim,
|
|
152
|
+
tiers,
|
|
153
|
+
retrievalStrategy: options.retrievalStrategy || 'progressive_disclosure',
|
|
154
|
+
hybridRouting: options.hybridRouting !== undefined ? options.hybridRouting : true,
|
|
155
|
+
embeddingModel: options.embeddingModel || 'text-embedding-3-large',
|
|
156
|
+
provider: options.provider || 'openai',
|
|
157
|
+
qualityThresholds: EMBEDDING_QUALITY_THRESHOLDS,
|
|
158
|
+
rolloutPlan: [
|
|
159
|
+
'Use 256-dim embeddings for initial query filtering (fast, cheap)',
|
|
160
|
+
'Re-rank top candidates at 768-dim for balanced quality',
|
|
161
|
+
'Use full 1536+ dim embeddings for final precision or when cross-encoder confirms relevance',
|
|
162
|
+
'Log dimension-tier performance metrics for continuous optimization',
|
|
163
|
+
],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function distillMemoryForEmbedding(memories = [], options = {}) {
|
|
168
|
+
const safeMemories = Array.isArray(memories) ? memories : [];
|
|
169
|
+
const layerGroups = {
|
|
170
|
+
[PYRAMID_LAYERS.L3_PERSONA_SOP]: [],
|
|
171
|
+
[PYRAMID_LAYERS.L2_SCENARIO]: [],
|
|
172
|
+
[PYRAMID_LAYERS.L1_ATOM]: [],
|
|
173
|
+
[PYRAMID_LAYERS.L0_CONVERSATION]: [],
|
|
174
|
+
};
|
|
175
|
+
const maxItemsPerLayer = options.maxItemsPerLayer || 100;
|
|
176
|
+
for (const memory of safeMemories) {
|
|
177
|
+
const layer = classifyMemoryLayer(memory);
|
|
178
|
+
if (layerGroups[layer].length < maxItemsPerLayer) {
|
|
179
|
+
layerGroups[layer].push({
|
|
180
|
+
...memory,
|
|
181
|
+
pyramidLayer: layer,
|
|
182
|
+
semanticWeight: getSemanticScore(normalizeToMatryoshkaDimension(options.embeddingDim || 768)),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
kind: 'matryoshka-memory-distillation',
|
|
188
|
+
totalMemories: safeMemories.length,
|
|
189
|
+
layerDistribution: Object.entries(layerGroups).map(([layer, items]) => ({
|
|
190
|
+
layer,
|
|
191
|
+
count: items.length,
|
|
192
|
+
contentPreview: items.slice(0, 3).map(i => String(i.content || i.title || '').slice(0, 50)),
|
|
193
|
+
})),
|
|
194
|
+
layers: layerGroups,
|
|
195
|
+
embeddingStrategy: buildMatryoshkaConfig(options),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function generateEmbeddingPreventionRules(qualityReport) {
|
|
200
|
+
const rules = [];
|
|
201
|
+
if (!qualityReport.valid) {
|
|
202
|
+
for (const issue of qualityReport.issues) {
|
|
203
|
+
if (issue.issue === 'recall_below_threshold') {
|
|
204
|
+
const actualRecall = String(issue.actual).replace('.', '\\.');
|
|
205
|
+
rules.push({
|
|
206
|
+
id: 'block-low-embedding-recall',
|
|
207
|
+
name: 'Block embedding changes with low recall',
|
|
208
|
+
category: 'Embedding Quality',
|
|
209
|
+
signal: '👎',
|
|
210
|
+
defaultAction: 'block',
|
|
211
|
+
severity: 'high',
|
|
212
|
+
pattern: `(embedding|vector|retrieval).*(recall\\s*[:=]\\s*${actualRecall})`,
|
|
213
|
+
problem: 'Prevents deployment of embedding configurations that fail minimum recall thresholds.',
|
|
214
|
+
roi: 'Protects RAG pipeline effectiveness by ensuring embedding quality gates are maintained.',
|
|
215
|
+
rollout: 'Enable for all RAG workflows to prevent quality regressions.',
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return rules;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
PYRAMID_LAYERS,
|
|
225
|
+
MATRYOSHKA_DIMENSIONS,
|
|
226
|
+
EMBEDDING_QUALITY_THRESHOLDS,
|
|
227
|
+
buildMatryoshkaConfig,
|
|
228
|
+
classifyMemoryLayer,
|
|
229
|
+
distillMemoryForEmbedding,
|
|
230
|
+
generateEmbeddingPreventionRules,
|
|
231
|
+
getSemanticScore,
|
|
232
|
+
normalizeToMatryoshkaDimension,
|
|
233
|
+
validateEmbeddingQuality,
|
|
234
|
+
buildEmbeddingTaskPrefix,
|
|
235
|
+
};
|