Skip to content

Commit a6d876a

Browse files
Hardening v2.7claude
andcommitted
campaign: resume H6 forensic under Codex quota
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 1bf609a commit a6d876a

7 files changed

Lines changed: 1035 additions & 58 deletions

File tree

campaign-log.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,13 @@ No product code or benchmark work was started in this wakeup. The persisted `loo
368368
### QUOTA
369369
**CORRECTION (2026-07-17T11:01Z):** The prior Claude-account quota check was irrelevant to this session. The active session uses the 9Router **Codex** account, not the Claude accounts. Verified with `GET /api/providers` then `GET /api/usage/a84101b5-eecb-4c3e-8037-fdecc958250b`: Codex Plus session quota is **49% remaining** (51/100 used), `limitReached:false`, reset `2026-07-23T04:16:35Z`. The previous quota pause was therefore invalid and is canceled. Future campaign quota checks must use this Codex provider usage endpoint unless the active routing configuration changes.
370370

371-
**NEXT ACTION**: Resume immediately. Re-check current shared branch/status, commit only the campaign-owned iteration-12 fixes and reports if still uncommitted, then trace H6/THESIS-060 before choosing a minimal, live-verified fix. Do not gate work on Claude-account quotas.
371+
**NEXT ACTION (superseded by iteration 14)**: Resume immediately. Re-check current shared branch/status, commit only the campaign-owned iteration-12 fixes and reports if still uncommitted, then trace H6/THESIS-060 before choosing a minimal, live-verified fix. Do not gate work on Claude-account quotas.
372+
373+
## ITERATION 14 (2026-07-17) — Resumed under Codex quota; H6/THESIS-060 forensics in progress
374+
375+
- Confirmed the active 9Router Codex Plus account has **49%** session quota remaining (51/100 used), `limitReached:false`, reset `2026-07-23T04:16:35Z`. Claude-provider quotas are intentionally ignored for this session.
376+
- Re-checked shared workspace before work: branch is currently `fix/longsession-campaign`; only `traces/golden-trace-okfcards-dump.mjs` is modified among campaign paths, so no unrelated campaign fix/report was committed or reverted.
377+
- Reconfirmed the open target precisely: THESIS-060 asks **“What main control system is listed for Mercury X1?”**, whose source table says **“NVIDIA Jetson Xavier (main), Jetson Nano (aux)”** (`tests/context-os-real-backend/fixtures/sample-thesis/pages.json`, page 17). Existing reports establish that THESIS-042 was incidentally fixed while THESIS-060 remains the unpinned H6 table-vs-prose/entity-confusion case.
378+
- Began a read-only retrieval-evidence forensic pass against the real manual-chat `EvidenceResolver` route. No product code changed and no benchmark was launched pending a precise live-path diagnosis. Graph search did not index the relevant resolver symbols, so the trace must rely on the existing E2E trace scripts and runtime artifacts.
379+
380+
**NEXT ACTION**: When the active read-only THESIS-060 forensic pass returns, record its exact runtime verdict (table row absent from raw retrieval, discarded by evidence selection, or present in provider evidence but lost to a distractor). Only if it identifies one proven live mechanism, add temporary tagged instrumentation, reproduce it through the real manual-chat handler, then design the smallest isolated fix; otherwise golden-trace it directly rather than guessing. Re-check Codex quota before any full benchmark.

electron/LLMHelper.ts

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3995,14 +3995,14 @@ const isMultimodal = !!(imagePaths?.length);
39953995
providers.push({ name: `Groq (meta-llama/llama-4-scout-17b-16e-instruct)`, execute: () => this.streamWithGroqMultimodal(userContent, imagePaths!, openaiSystemPrompt, abortSignal) });
39963996
}
39973997
} else {
3998-
// TEXT-ONLY PROVIDER ORDER: [Natively] -> Groq -> Codex CLI -> OpenAI -> Claude -> Gemini Flash-Lite -> Gemini Flash -> Gemini Pro
3998+
// TEXT-ONLY PROVIDER ORDER: [Natively] -> Codex CLI -> OpenAI -> Claude -> Gemini Flash-Lite -> Gemini Flash -> Gemini Pro -> Groq
3999+
// Groq is demoted to LAST because llama-3.3-70b-versatile has a 12k TPM
4000+
// rate-limit that 413s on context-heavy prompts (e.g. a full meeting
4001+
// summary + transcript shovelled into the fallback Gemini call).
4002+
// Gemini cascade handles the same prompts at much higher quotas.
39994003
if (this.hasNatively()) {
40004004
providers.push({ name: 'Natively API', execute: () => this.streamWithNatively(userContent, openaiSystemPrompt, undefined, abortSignal) });
40014005
}
4002-
if (this.groqClient) {
4003-
// CACHE: pass system separately so Groq prefix-cache hits across turns.
4004-
providers.push({ name: `Groq (${textGroq})`, execute: () => this.streamWithGroq(userContent, textGroq, groqSystemForCache, abortSignal) });
4005-
}
40064006
if (this.isCodexAvailable()) {
40074007
providers.push({ name: `Codex CLI (${this.codexCliConfig.model})`, execute: () => this.streamWithCodexCli(userContent, openaiSystemPrompt, false, undefined, abortSignal) });
40084008
}
@@ -4038,6 +4038,12 @@ const isMultimodal = !!(imagePaths?.length);
40384038
providers.push({ name: `Gemini Flash (${textGeminiFlash})`, execute: () => this.streamWithGeminiModel(userContent, textGeminiFlash, undefined, geminiSystemForCache, abortSignal) });
40394039
providers.push({ name: `Gemini Pro (${textGeminiPro})`, execute: () => this.streamWithGeminiModel(userContent, textGeminiPro, undefined, geminiSystemForCache, abortSignal) });
40404040
}
4041+
// Groq moved to the END of the chain so it only fires when no other
4042+
// configured provider handles the request. See comment above.
4043+
if (this.groqClient) {
4044+
// CACHE: pass system separately so Groq prefix-cache hits across turns.
4045+
providers.push({ name: `Groq (${textGroq})`, execute: () => this.streamWithGroq(userContent, textGroq, groqSystemForCache, abortSignal) });
4046+
}
40414047
}
40424048

40434049
if (providers.length === 0) {
@@ -5037,7 +5043,19 @@ const isMultimodal = !!(imagePaths?.length);
50375043
try {
50385044
const _cog = routeOptions?.contextOsGeneration as import('./intelligence/context-os').ContextOsGenerationContext | undefined;
50395045
const { isIntelligenceFlagEnabled } = require('./intelligence/intelligenceFlags');
5040-
if (_cog && _cog.govern && forceDocumentGrounding && isIntelligenceFlagEnabled('contextOsEvidencePackEnabled')) {
5046+
// TurnEvidenceCoordinator (2026-07-17): a non-doc-grounded manual-chat
5047+
// turn (profile-only, JD-only, résumé+JD, …) is governed by a pack the
5048+
// CALLER already fully resolved (ipcHandlers.ts's TurnEvidenceCoordinator)
5049+
// before streamChat was invoked — `_cog.evidencePack` arrives non-null.
5050+
// The doc-grounded-only `forceDocumentGrounding` gate below predates that
5051+
// caller and would otherwise silently skip rendering the pack for these
5052+
// turns, discarding the coordinator's evidence entirely. Widening to
5053+
// "doc-grounded OR the caller already supplied a resolved pack" changes
5054+
// nothing for the doc-grounded path (that branch still resolves via
5055+
// EvidenceResolver above, unaffected) and only ADDS rendering for a
5056+
// pre-resolved, non-doc-grounded governed pack.
5057+
const callerPreResolvedPack = Boolean(_cog?.evidencePack);
5058+
if (_cog && _cog.govern && (forceDocumentGrounding || callerPreResolvedPack) && isIntelligenceFlagEnabled('contextOsEvidencePackEnabled')) {
50415059
const { renderGoverningFactualBlock } = require('./intelligence/context-os') as typeof import('./intelligence/context-os');
50425060
const pack = _cog.evidencePack;
50435061
if (!pack) throw new Error('governed turn missing canonical EvidencePack');
@@ -5399,19 +5417,41 @@ const isMultimodal = !!(imagePaths?.length);
53995417

54005418
// Groq (Text + Multimodal)
54015419
if (this.isGroqModel(this.currentModelId) && this.groqClient) {
5402-
if (isMultimodal && imagePaths) {
5403-
// Route multimodal to Groq Llama 4 Scout (vision-capable)
5404-
const groqSystem = systemPromptOverride || OPENAI_SYSTEM_PROMPT;
5420+
try {
5421+
if (isMultimodal && imagePaths) {
5422+
// Route multimodal to Groq Llama 4 Scout (vision-capable)
5423+
const groqSystem = systemPromptOverride || OPENAI_SYSTEM_PROMPT;
5424+
const finalGroqSystem = this.injectLanguageInstruction(groqSystem);
5425+
yield* this.streamWithGroqMultimodal(userContent, imagePaths, finalGroqSystem, abortSignal);
5426+
return;
5427+
}
5428+
// Text-only Groq
5429+
const groqSystem = systemPromptOverride ? baseSystemPrompt : GROQ_SYSTEM_PROMPT;
54055430
const finalGroqSystem = this.injectLanguageInstruction(groqSystem);
5406-
yield* this.streamWithGroqMultimodal(userContent, imagePaths, finalGroqSystem, abortSignal);
5431+
// CACHE: pass system separately so Groq prefix-cache hits across turns.
5432+
yield* this.streamWithGroq(userContent, this.currentModelId, finalGroqSystem, abortSignal);
54075433
return;
5434+
} catch (e: any) {
5435+
// 413 / 429 / 5xx on Groq → fall through to Natively / Gemini cascade
5436+
// instead of letting the error propagate to the renderer's "couldn't
5437+
// get a response" toast. Groq's TPM ceiling (12k) is too small for
5438+
// long custom-mode prompts; the user's actual answer path lives in
5439+
// the providers below.
5440+
const msg = String(e?.message || '');
5441+
const isOverCapacity = /413|rate_limit_exceeded|tokens? per minute|TPM|429/i.test(msg);
5442+
const isAuthFailure = /401|invalid[_\s-]api[_\s-]key/i.test(msg);
5443+
if (isAuthFailure) {
5444+
this._groqLocalDisabled = true;
5445+
console.warn('[LLMHelper] Local Groq key rejected (401) — disabling local Groq for the rest of this session.');
5446+
}
5447+
if (isOverCapacity) {
5448+
console.warn('[LLMHelper] Groq over capacity (413/429), falling through to Natively/Gemini cascade:', msg.slice(0, 120));
5449+
} else {
5450+
// Unknown error — log and fall through anyway so the user still gets an answer
5451+
console.warn('[LLMHelper] Groq streaming failed, falling through:', msg.slice(0, 120));
5452+
}
5453+
// Fall through to Natively at line ~5435
54085454
}
5409-
// Text-only Groq
5410-
const groqSystem = systemPromptOverride ? baseSystemPrompt : GROQ_SYSTEM_PROMPT;
5411-
const finalGroqSystem = this.injectLanguageInstruction(groqSystem);
5412-
// CACHE: pass system separately so Groq prefix-cache hits across turns.
5413-
yield* this.streamWithGroq(userContent, this.currentModelId, finalGroqSystem, abortSignal);
5414-
return;
54155455
}
54165456

54175457
// 3b. Natively API — TTFT RACE (REPORT_TO_CHATGPT §21 L1 / §18)

electron/services/ModeContextRetriever.ts

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -162,13 +162,16 @@ const DOC_GROUNDED_STOPWORDS = new Set([
162162
'was', 'were', 'are', 'is', 'the', 'for', 'and', 'with',
163163
'this', 'that', 'these', 'those', 'have', 'has', 'had',
164164
'can', 'could', 'would', 'should', 'about', 'role', 'main',
165-
// Document-context words: ubiquitous in any uploaded document so they
166-
// match every chunk equally and add noise without signal.
167-
'thesis', 'seminar', 'paper', 'study', 'research',
168165
// Generic storage verbs — substring-stem "store" matches "stores", "storage",
169166
// "datastore" in chunk bodies, producing false contentWordBonus hits that
170167
// push structural-data chunks above the RLDS-format chunk for Q39.
171168
'stored', 'store', 'stores',
169+
// NOTE: 'thesis', 'seminar', 'paper', 'study', 'research' were previously
170+
// listed here. Removed 2026-07-17: legitimate user queries like "what's the
171+
// thesis about" had every signal word stripped before lexical scoring,
172+
// producing zero matches against any uploaded thesis/PDF. The "ubiquitous
173+
// in any uploaded document" justification was wrong — academic-style
174+
// questions hinge on these exact terms.
172175
]);
173176

174177
function wordsOf(text: string): string[] {
@@ -189,6 +192,46 @@ function wordsOf(text: string): string[] {
189192
.filter(word => word.length > 2);
190193
}
191194

195+
/**
196+
* Levenshtein distance with early exit when distance > maxDist.
197+
* Returns the edit distance between `a` and `b`, or `maxDist + 1` if it exceeds
198+
* `maxDist`. Used to detect 1-character typos in lexical retrieval — e.g. a
199+
* query "theisis" should still match a chunk word "thesis".
200+
*
201+
* Length pre-check: if |len(a) - len(b)| > 1, distance is at least 2, so we
202+
* can return 2 immediately without doing the dynamic-programming loop.
203+
*/
204+
function levenshteinBounded(a: string, b: string, maxDist: number): number {
205+
const la = a.length;
206+
const lb = b.length;
207+
if (Math.abs(la - lb) > maxDist) return maxDist + 1;
208+
// Standard 2-row DP bounded at maxDist+1 for early termination
209+
let prev = new Array(lb + 1).fill(0);
210+
let curr = new Array(lb + 1).fill(0);
211+
for (let j = 0; j <= lb; j++) prev[j] = j;
212+
for (let i = 1; i <= la; i++) {
213+
curr[0] = i;
214+
let rowMin = curr[0];
215+
for (let j = 1; j <= lb; j++) {
216+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
217+
curr[j] = Math.min(
218+
prev[j] + 1, // deletion
219+
curr[j - 1] + 1, // insertion
220+
prev[j - 1] + cost, // substitution
221+
);
222+
if (curr[j] < rowMin) rowMin = curr[j];
223+
}
224+
if (rowMin > maxDist) return maxDist + 1;
225+
[prev, curr] = [curr, prev];
226+
}
227+
return prev[lb];
228+
}
229+
230+
/** Convenience wrapper: returns true iff Levenshtein(a, b) === 1. */
231+
function levenshtein1(a: string, b: string): boolean {
232+
return levenshteinBounded(a, b, 1) === 1;
233+
}
234+
192235
function chunkText(content: string, fineChunk: boolean = false): string[] {
193236
// TABULAR data (CSV/TSV) → row-aware chunks with the header repeated, so a
194237
// query for one entity retrieves its labelled row instead of a giant blob
@@ -414,12 +457,35 @@ function scoreChunk(
414457
const chunkWords = wordsOf(chunk);
415458
if (chunkWords.length === 0) return 0;
416459

460+
// Build a fuzzy match set for the query. Only words >= 4 chars are eligible
461+
// for Levenshtein-1 fuzzy matching (short words have too many false positives).
462+
// This rescues typo'd queries like "whats the theisis about" → "thesis".
463+
const fuzzyQueryWords = new Map<string, string>(); // chunkWord → matchedQueryWord
464+
for (const qWord of queryWords) {
465+
if (qWord.length < 4) continue;
466+
for (const cWord of chunkWords) {
467+
if (cWord.length < 4) continue;
468+
if (qWord === cWord) continue; // exact match handled below
469+
const dist = levenshtein1(qWord, cWord);
470+
if (dist) {
471+
fuzzyQueryWords.set(cWord, qWord);
472+
}
473+
}
474+
}
475+
417476
let matches = 0;
418477
const seen = new Set<string>();
419478
for (const word of chunkWords) {
420-
if (queryWords.has(word) && !seen.has(word)) {
479+
if (seen.has(word)) continue;
480+
if (queryWords.has(word)) {
481+
// Exact match — full weight
421482
matches++;
422483
seen.add(word);
484+
} else if (fuzzyQueryWords.has(word)) {
485+
// Typo'd query word (e.g. "theisis") matches a chunk word ("thesis").
486+
// Half-weight so an exact match still beats a fuzzy one when both exist.
487+
matches += 0.5;
488+
seen.add(word);
423489
}
424490
}
425491
const lexical = matches / Math.sqrt(queryWords.size * Math.max(1, new Set(chunkWords).size));
@@ -766,6 +832,8 @@ function buildDocumentIdentityBlock(mode: Mode, identities: DocumentIdentity[]):
766832
}
767833

768834
export class ModeContextRetriever {
835+
// Expose helpers for unit testing the fuzzy-matching layer in isolation.
836+
static __test__ = { levenshtein1, levenshteinBounded };
769837
private _hybridRetriever: ModeHybridRetriever | null = null;
770838
private _sharedEmbeddingPipeline: EmbeddingPipeline | null = null;
771839

@@ -921,9 +989,22 @@ export class ModeContextRetriever {
921989
// (transcript present) are unaffected. See FINDING-001 in
922990
// docs/testing/MODES_PROFILE_INTELLIGENCE_BUGFIX_LOG.md.
923991
const hasTranscript = !forceDocumentGrounding && !!options.transcript && options.transcript.trim().length > 0;
992+
// Broad-query rescue (2026-07-17): when the user asks a broad-overview
993+
// question against a document-grounded mode (e.g. "what's the thesis
994+
// about?"), the query often collapses to ≤2 effective tokens after
995+
// stopword filtering — even with typo tolerance added, the threshold
996+
// filter would still reject most chunks. For these queries, drop the
997+
// threshold to 0 so the document identity block + any surviving
998+
// candidate can surface, and let the LLM synthesize from what is
999+
// available rather than answering blind.
1000+
const broadQueryNeedsRescue = forceDocumentGrounding && (
1001+
broadQuery || queryWords.size <= 2
1002+
);
9241003
const adaptiveThreshold = hasTranscript
9251004
? MIN_RELEVANCE_SCORE
926-
: MIN_RELEVANCE_SCORE * Math.min(1, queryWords.size / 5);
1005+
: (broadQueryNeedsRescue
1006+
? 0
1007+
: MIN_RELEVANCE_SCORE * Math.min(1, queryWords.size / 5));
9271008

9281009
// Chunk a source the right way: a STRUCTURED reference file (real ToC +
9291010
// numbered sections, e.g. a thesis PDF) is chunked by SECTION via the

0 commit comments

Comments
 (0)