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.
Files changed (200) hide show
  1. package/.agents/skills/cyberstrike-compare-not-clone/SKILL.md +36 -0
  2. package/.agents/skills/gitlab-sandbox-allowlist-not-trust/SKILL.md +77 -0
  3. package/.agents/skills/jit-harness-compare-not-clone/SKILL.md +34 -0
  4. package/.agents/skills/openui-catalog-compose-honesty/SKILL.md +64 -0
  5. package/.agents/skills/zvec-grep-compare-not-clone/SKILL.md +34 -0
  6. package/.claude-plugin/plugin.json +1 -1
  7. package/.well-known/llms.txt +1 -0
  8. package/.well-known/mcp/server-card.json +1 -1
  9. package/CONTRIBUTING.md +95 -0
  10. package/README.md +195 -632
  11. package/THIRD_PARTY_NOTICES.md +89 -0
  12. package/adapters/claude/.mcp.json +2 -2
  13. package/adapters/forge/forge.yaml +3 -3
  14. package/adapters/future-agi/.mcp.json +8 -0
  15. package/adapters/future-agi/FUTURE_AGI.md +23 -0
  16. package/adapters/future-agi/config.toml +3 -0
  17. package/adapters/future-agi/future-agi-bridge.js +9 -0
  18. package/adapters/future-agi/opencode.json +8 -0
  19. package/adapters/herdr/herdr-plugin.toml +18 -0
  20. package/adapters/mcp/server-stdio.js +238 -25
  21. package/adapters/opencode/opencode.json +1 -1
  22. package/adapters/workos/WORKOS.md +52 -0
  23. package/bin/cli.js +373 -5
  24. package/bin/futureagi-bridge +9 -0
  25. package/config/gate-templates.json +653 -4
  26. package/config/gates/actor-critic-audit.json +34 -0
  27. package/config/gates/default.json +21 -2
  28. package/config/gates/five-walls-governance.json +34 -0
  29. package/config/gates/future-agi-guardrails.json +34 -0
  30. package/config/gates/radware-threat-defense-2026.json +61 -0
  31. package/config/gates/simatree-data-governance.json +33 -0
  32. package/config/mcp-allowlists.json +4 -0
  33. package/config/merge-quality-checks.json +10 -1
  34. package/config/model-candidates.json +382 -24
  35. package/config/model-tiers.json +18 -0
  36. package/config/post-deploy-marketing-pages.json +10 -0
  37. package/config/progressive/01-wire-only.json +11 -0
  38. package/config/progressive/02-dashboard-empty-ok.json +10 -0
  39. package/config/progressive/03-one-lesson.json +10 -0
  40. package/config/progressive/04-warn-fires.json +11 -0
  41. package/config/progressive/05-strict-optional.json +11 -0
  42. package/config/progressive/README.md +15 -0
  43. package/config/schemas/broker-execution-receipt.schema.json +139 -0
  44. package/config/schemas/provider-execution-attestation-v1.schema.json +58 -0
  45. package/conformance/provider-attestation/vectors.json +320 -0
  46. package/docs/specs/provider-execution-attestation-v1.md +69 -0
  47. package/openapi/openapi.yaml +15 -0
  48. package/package.json +401 -147
  49. package/public/about.html +2 -2
  50. package/public/ai-malpractice-prevention.html +7 -7
  51. package/public/blog/a-10-dollar-vps-is-not-a-computer.html +143 -0
  52. package/public/blog/a-receipt-is-not-world-state.html +388 -0
  53. package/public/blog/git-at-agent-scale.html +374 -0
  54. package/public/blog/no-llm-in-the-gate.html +133 -0
  55. package/public/blog.html +80 -0
  56. package/public/case-studies.html +16 -1
  57. package/public/compare.html +28 -0
  58. package/public/diagnostic.html +216 -7
  59. package/public/docs/connectors.html +39 -0
  60. package/public/federal.html +2 -2
  61. package/public/founders.html +639 -0
  62. package/public/index.html +87 -9
  63. package/public/install.html +8 -8
  64. package/public/learn.html +39 -0
  65. package/public/numbers.html +2 -2
  66. package/public/peter.html +310 -0
  67. package/public/platform-partners.html +119 -0
  68. package/public/pricing.html +24 -3
  69. package/public/privacy.html +117 -0
  70. package/public/pro.html +17 -0
  71. package/public/support.html +62 -0
  72. package/public/terms.html +130 -0
  73. package/public/third-party-notices.html +95 -0
  74. package/public/yt.html +351 -0
  75. package/scripts/action-receipts.js +133 -3
  76. package/scripts/adaptive-governance-arena.js +349 -0
  77. package/scripts/admin-override.js +205 -0
  78. package/scripts/agent-action-inventory.js +869 -0
  79. package/scripts/agent-audit-trace.js +42 -2
  80. package/scripts/agent-egress-policy.js +1117 -0
  81. package/scripts/agent-memory-lifecycle.js +141 -2
  82. package/scripts/agent-operations-planner.js +441 -1
  83. package/scripts/agent-readiness.js +68 -0
  84. package/scripts/agent-security-central.js +647 -0
  85. package/scripts/allowlist-bridge-honesty.js +417 -0
  86. package/scripts/async-job-runner.js +102 -11
  87. package/scripts/audit-trail.js +212 -0
  88. package/scripts/auto-promote-gates.js +178 -27
  89. package/scripts/billing.js +1 -1
  90. package/scripts/broker-execution-receipts.js +719 -0
  91. package/scripts/budget-aware-gates-proof.js +423 -0
  92. package/scripts/claude-feedback-sync.js +29 -3
  93. package/scripts/claw-harness-production.js +237 -0
  94. package/scripts/cli-progress.js +111 -0
  95. package/scripts/cli-schema.js +163 -1
  96. package/scripts/codex-runbook-flywheel.js +318 -0
  97. package/scripts/context-footprint.js +186 -0
  98. package/scripts/contextfs.js +143 -61
  99. package/scripts/dashboard-limits.js +27 -0
  100. package/scripts/dashboard.js +279 -9
  101. package/scripts/deepseek-v4-runtime-guardrails.js +72 -6
  102. package/scripts/docker-sandbox-planner.js +18 -0
  103. package/scripts/double-blind-eval-protocol.js +252 -0
  104. package/scripts/edotenv-rl-gateway.js +259 -0
  105. package/scripts/ensure-production-search-corpus.js +162 -0
  106. package/scripts/eval-holdout.js +311 -0
  107. package/scripts/feedback-aggregate.js +21 -2
  108. package/scripts/feedback-loop.js +87 -5
  109. package/scripts/feedback-quality.js +9 -0
  110. package/scripts/file-ledger-lock.js +4 -1
  111. package/scripts/financial-control-plane.js +41 -1
  112. package/scripts/find-dormant-requires.js +118 -0
  113. package/scripts/fs-utils.js +84 -8
  114. package/scripts/gate-stats.js +2 -2
  115. package/scripts/gates-engine.js +859 -58
  116. package/scripts/generate-case-study-outreach.js +24 -15
  117. package/scripts/git-at-scale.js +628 -0
  118. package/scripts/governance-conflict-audit.js +1650 -0
  119. package/scripts/governance-difficulty-curriculum.js +328 -0
  120. package/scripts/graphrag-retrieval.js +275 -0
  121. package/scripts/gurobi-optimizer.js +324 -0
  122. package/scripts/gurobi_optimizer.py +485 -0
  123. package/scripts/harness-selector.js +82 -1
  124. package/scripts/hidden-entry-points.js +284 -0
  125. package/scripts/human-escalation.js +199 -1
  126. package/scripts/hybrid-feedback-context.js +152 -19
  127. package/scripts/intent-governed-execution.js +602 -0
  128. package/scripts/intervention-policy.js +123 -20
  129. package/scripts/jit-harness-compose.js +628 -0
  130. package/scripts/jsonl-watcher.js +10 -0
  131. package/scripts/lesson-embedding-index.js +95 -12
  132. package/scripts/lesson-retrieval.js +105 -19
  133. package/scripts/local-model-profile.js +19 -2
  134. package/scripts/mailer/resend-mailer.js +1 -1
  135. package/scripts/matryoshka-embedding.js +235 -0
  136. package/scripts/mcp-oauth.js +42 -4
  137. package/scripts/mcp-session-handles.js +1016 -0
  138. package/scripts/mcp-wiring-doctor.js +314 -0
  139. package/scripts/memory-firewall.js +115 -2
  140. package/scripts/memory-scope-readiness.js +299 -0
  141. package/scripts/memory-vs-rag-route.js +161 -0
  142. package/scripts/model-tier-router.js +148 -21
  143. package/scripts/nvidia-specdecode-al-doctor.js +536 -0
  144. package/scripts/openui-catalog-compose-honesty.js +593 -0
  145. package/scripts/operational-integrity.js +19 -1
  146. package/scripts/override-audit.js +213 -0
  147. package/scripts/package-manager-honesty-doctor.js +458 -0
  148. package/scripts/pr-manager.js +63 -1
  149. package/scripts/prove-herdr-adapter.js +52 -0
  150. package/scripts/prove-memory-pyramid-and-symbolic-canvas.js +95 -0
  151. package/scripts/prove-workos.js +73 -0
  152. package/scripts/provider-attestation-conformance.js +192 -0
  153. package/scripts/provider-receipt-contract.js +136 -0
  154. package/scripts/qwen38-max-cost-optimizer.js +401 -0
  155. package/scripts/radware-threat-defense.js +280 -0
  156. package/scripts/rag-embedding-identity.js +221 -0
  157. package/scripts/rag-precision-guardrails.js +112 -2
  158. package/scripts/remote-feedback-capture.js +159 -0
  159. package/scripts/research-agent-harness.js +256 -0
  160. package/scripts/rsi-safety-hillclimb.js +200 -0
  161. package/scripts/rule-sprawl.js +188 -0
  162. package/scripts/schedule-manager.js +147 -0
  163. package/scripts/self-heal.js +8 -0
  164. package/scripts/session-lease.js +415 -0
  165. package/scripts/simatree-data-governance.js +347 -0
  166. package/scripts/slo-alert-engine.js +172 -7
  167. package/scripts/solver-parity.js +539 -0
  168. package/scripts/stealth-memory-injection-gate.js +333 -0
  169. package/scripts/switchyard-router.js +366 -0
  170. package/scripts/telemetry-analytics.js +84 -27
  171. package/scripts/temporal-decay-weighting.js +138 -0
  172. package/scripts/test-all.js +165 -0
  173. package/scripts/token-savings.js +42 -0
  174. package/scripts/tool-kpi-tracker.js +108 -5
  175. package/scripts/tool-registry.js +193 -5
  176. package/scripts/universal-claim-evaluator.js +14 -2
  177. package/scripts/vector-store.js +279 -9
  178. package/scripts/workflow-notebook.js +391 -0
  179. package/scripts/workflow-sentinel.js +111 -12
  180. package/scripts/workos-production-guard.js +260 -0
  181. package/scripts/workspace-search-route.js +515 -0
  182. package/server.json +2 -2
  183. package/src/agent-identity-boundary.js +76 -0
  184. package/src/agent-retrieval-cache.js +155 -0
  185. package/src/alert-noise-ledger.js +502 -0
  186. package/src/api/server.js +802 -185
  187. package/src/git-fast-cache.js +220 -0
  188. package/src/git-wal-sync.js +156 -0
  189. package/src/hash-anchored-edit.js +82 -0
  190. package/src/hermes-platform-protocol.js +475 -0
  191. package/src/hermes-sync-plane.js +241 -0
  192. package/src/index.js +30 -1
  193. package/src/iso42001-compliance-guard.js +97 -0
  194. package/src/latency-budget.js +244 -0
  195. package/src/mcp-writeguard.js +316 -0
  196. package/src/miminions-adapter.js +106 -0
  197. package/src/pipeline-compass.js +104 -0
  198. package/src/ppl-alert-pipeline.js +284 -0
  199. package/src/rendezvous-router.js +90 -0
  200. 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
- let queryVector = await embedder(queryText, { kind: 'query', task: 'code retrieval' });
174
- if (!Array.isArray(queryVector) || queryVector.length === 0) return [];
175
-
176
- if (truncateDimension) {
177
- queryVector = truncateVector(queryVector, truncateDimension);
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 scored = [];
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
- const docVector = truncateDimension ? truncateVector(entry.vector, truncateDimension) : entry.vector;
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
- return scored.sort((a, b) => b.score - a.score);
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, take top-50 candidates
232
- const candidates = memories
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
- .slice(0, RERANK_CANDIDATE_POOL);
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.slice(0, RERANK_CANDIDATE_POOL), { topK: maxResults, toolName });
438
- return filterTopP(dedupeSupersededLessons(reranked), resolveTopP(options), { minKeep: options.minKeep }).map(shapeLesson);
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 }).map(shapeLesson);
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
- const ageMs = Date.now() - new Date(memory.timestamp).getTime();
650
- const ageDays = ageMs / (1000 * 60 * 60 * 24);
651
- const decay = Math.max(0, 1 - ageDays / RECENCY_DECAY_DAYS);
652
- score *= 0.5 + 0.5 * decay;
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 = isLocalGlm ? (GLM_MODEL_ROLES[normalized] || MODEL_ROLES[normalized]) : MODEL_ROLES[normalized];
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 = 'igor.ganapolsky@gmail.com';
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
+ };