Dashboard › Code › Session sd34c32137ed
sd34c32137edddc6eEdit applied successfully.
Now update the PR description:
(fail) formatKnowledge > output === normalize(output) — AST serializer produces already-normalized markdown [5709.96ms] (fail) LocalProvider integration > query and document embeddings have reasonable similarity [60002.55ms] error: connection refused error: network error: boom error: Network error error: Network error
2 actual failures (the rest are expected network errors in tests). Let me look at them:
formatKnowledge > output === normalize(output) — likely a deterministic ordering issueLocalProvider integration > query and document embeddings have reasonable similarity — likely a network/model load issueLet me check if these are pre-existing flakes by running on the main branch:
https://github.com/BYK/loreai/pull/577
Now push the lastIndexOf simplification:
Saved working directory and index state WIP on debun/pnpm-migration: 0823301 feat(build): migrate standalone binary from Bun --compile to Node SEA via fossilize (#564) Switched to branch 'main' Your branch is up to date with 'origin/main'. bun test v1.3.13 (bf2e2cec) The following filters did not match any test files in --cwd="/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit": packages/core/test/knowledge.test.ts 387 files were searched [13.00ms] note: Tests need ".test", "_test_", ".spec" or "_spec_" in the filename (ex: "MyApp.test.ts") note: To treat the "packages/core/test/knowledge.test.ts" filter as a path, run "bun test ./packages/core/test/knowledge.test.ts"
packages/core/test/knowledge-transfers.test.ts
packages/core/test/markdown.test.ts
bun test v1.3.13 (bf2e2cec) 39 pass 0 fail 3061 expect() calls Ran 39 tests across 1 file. [8.78s]
formatKnowledge passes on main! So my pnpm changes broke it. Let me go back to my branch and investigate:
Switched to branch 'debun/pnpm-migration'
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (dfd385dcc3e1d77d32a0aa4f552120014fdc4333)
---
bun test v1.3.13 (bf2e2cec)
39 pass
0 fail
3048 expect() calls
Ran 39 tests across 1 file. [7.96s]
Markdown test passes on my branch too! Let me run the full test suite to see what's actually failing:
Checked 250 files in 2s. Fixed 1 file. Found 4 warnings. 0 fail 9841 expect() calls Ran 2258 tests across 83 files. [122.31s]
Please address this message and continue with your tasks. </system-reminder>
Jun 05 20:57:21 labs opencode[3795103]: [lore] upstream: https://api.minimax.io/anthropic (provider=minimax-coding-plan, providerURL=https://api.minimax.io/anthropic, modelRoute=none, headerUpstream=yes, protocol=anthropic, auth=api-key:sk-cp-bE…) Jun 05 20:58:59 labs opencode[3795103]: [lore] upstream: https://api.minimax.io/anthropic (provider=minimax-coding-plan, providerURL=https://api.minimax.io/anthropic, modelRoute=none, headerUpstream=yes, protocol=anthropic, auth=api-key:sk-cp-bE…) Jun 05 21:00:38 labs opencode[3795103]: [lore] WARN: compaction anomaly: session=1dsKYbwHbad5o6Ot messages dropped 1889→627. Client may have compacted outside gateway control. Jun 05 21:00:44 labs opencode[3795103]: [lore] upstream: https://api.anthropic.com (provider=anthropic, providerURL=https://api.anthropic.com, modelRoute=https://api.anthropic.com, headerUpstream=yes, protocol=anthropic, auth=api-key:sk-ant-a…) Jun 05 21:00:44 labs opencode[3795103]: [lore] upstream: ...
Interesting findings. Let me look at the key issues:
api.minimax.io/anthropic routing works correctlyThe most concerning issue: the worker is sending requests through OpenRouter (line with worker upstream request failed: 402). The worker model should use the conversation's own provider, not route free-model sessions' worker tasks through OpenRouter where they'd cost money.
Jun 05 21:03:05 labs opencode[3795103]: [lore] worker upstream request failed: 402 Payment Required — {"error":{"message":"This request requires more credits, or fewer max_tokens. You requested up to 4096 tokens, but can only afford 2666. To increase, visit https://openrouter.ai/settings/credits and upgrade to a paid account","code":402,"metadata":{"provider_name":null,"previous_errors":[{"code":402,"message":"This request requires more credits, or fewer max_tokens. You requested up to 4096 tokens, but can only afford 2666. To increase, visit https://openrouter.ai/settings/credits and upgrade to a paid account"},{"code":402,"message":"This request requires more credits, or fewer max_tokens. You requested up to 4096 tokens, but can only afford 2666. To increase, visit https://openrouter.ai/settings/credits and upgrade to a paid account"},{"code":402,"message":"This request requires more credits, or fewer max_tokens. You requested up to 4096 tokens, but can only afford 2666. ...
Several issues:
Worker using OpenRouter credentials — The worker (distillation/curation) is reusing the session's OpenRouter auth to make background LLM calls. This costs money on the user's OpenRouter account for background work they didn't ask for, and fails with 402 when credits are low.
MiniMax auth failure — Worker sends the MiniMax API key to what looks like a different endpoint expecting it in X-Api-Key header format, gets 401.
"no auth credentials available for worker call" — After auth failures, the worker can't find valid credentials and spams warnings.
Batch API using wrong key — Anthropic batch API gets 401 "invalid x-api-key", likely because it picked up a non-Anthropic session's credentials.
These are all pre-existing issues, not caused by our PR. The core problem: background workers (distillation, curation) inherit the session's auth credentials, which is wrong for non-Anthropic sessions. A MiniMax or OpenRouter key shouldn't be used for Lore's own background LLM calls.
These are deeper architectural issues that shouldn't block this PR. Should we:
(no output)
A MiniMax or OpenRouter key shouldn't be used for Lore's own background LLM calls.
Err no this should be the case? Whatever provider is being used for the session should dictate that session's worker provider and thus models.
What concerns me is the invalid key errors as all my keys are correct so there might be wrong credentials used on a provider issue. Moreover I keep seeing "early divergence at byte 10" errors showing a model change from opus-4-6 to minimax-m3 which I'm not sure is expected or not
You're right — the worker SHOULD use the session's provider. The issue is that the wrong credentials are being picked up. Let me dig deeper into those specific errors.
Jun 05 21:00:58 labs opencode[3795103]: [lore] cache-analytics: early divergence at byte 10
Jun 05 21:01:05 labs opencode[3795103]: [lore] cache-analytics: early divergence at byte 217
Jun 05 21:03:05 labs opencode[3795103]: [lore] worker upstream request failed: 402 Payment Required — {"error":{"message":"This request requires more credits, or fewer max_tokens. You requested up to 4096 tokens, but can only afford 2666. To increase, visit https://openrouter.ai/settings/credits and upgrade to a paid account","code":402,"metadata":{"provider_name":null,"previous_errors":[{"code":402,"message":"This request requires more credits, or fewer max_tokens. You requested up to 4096 tokens, but can only afford 2666. To increase, visit https://openrouter.ai/settings/credits and upgrade to a paid account"},{"code":402,"message":"This request requires more credits, or fewer max_tokens. You requested up to 4096 tokens, but can only afford 2666. To increase, visit ...
[lore] cache-warmer CIRCUIT BREAKER: warmup caused uncached write (1/3). cacheCreation=50000 cacheRead=0 [lore] cache-warmer CIRCUIT BREAKER: warmup caused uncached write (2/3). cacheCreation=50000 cacheRead=0 [lore] cache-warmer CIRCUIT BREAKER: warmup caused uncached write (3/3). cacheCreation=50000 cacheRead=0 [lore] cache-warmer CIRCUIT BREAKER TRIPPED: 3 consecutive uncached warmups detected. ALL cache warming disabled for this process. This indicates warmup bodies don't match the cached prefix — investigate cache key computation assumptions. [lore] active: /home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit 2251 pass 0 fail 9843 expect() calls Ran 2251 tests across 83 files. [96.80s]
All 2251 tests pass with bun test. The 4 failures under pnpm test were probably non-deterministic timing issues or stale pnpm install state. Let me clean the pnpm state and retry:
Jun 05 21:00:44 labs opencode[3795103]: [lore] upstream: https://api.minimax.io/anthropic (provider=minimax-coding-plan, providerURL=https://api.minimax.io/anthropic, modelRoute=none, headerUpstream=yes, protocol=anthropic, auth=api-key:sk-cp-bE…)
Jun 05 21:00:53 labs opencode[3795103]: [lore] cache-analytics: session=1dsKYbwHbad5o6Ot turn=87 hit=100% read=96100 create=0 input=0 prefixMatch=8.3% (30777/371571B) divergence="system[2].text" reason="context-bound LTM changed (non-preference entries re-ranked)"
Jun 05 21:00:53 labs opencode[3795103]: [lore] commit detected in session 1dsKYbwHbad5o6Ot — triggering curation
Jun 05 21:00:58 labs opencode[3795103]: [lore] cache-analytics: early divergence at byte 10
Jun 05 21:00:58 labs opencode[3795103]: prev: "{\"model\":\"MiniMax-M3\",\"max_tokens\":0,\"stream\":true,\"system\":[{\"type\":\"text\",\"text\":\"You ar"
Jun 05 21:00:58 labs opencode[3795103]: curr: ...
Now I see the full picture. There are two distinct issues:
1. Cache warmer cross-contamination: Session 1TjLgLHS3kg0Go98 alternates between claude-opus-4-6 and MiniMax-M3 on every turn. The prev body shows the model from the cache warmup request, and the curr is the actual user request. The cache warmer is sending warmup requests with the wrong model — it's warming with MiniMax-M3 on an Anthropic session and vice versa. This causes divergence at byte 10 (the "model" field).
This is because the lastUpstream snapshot (which includes model) is being shared/contaminated between sessions — or more likely, the cache warmer is reading from a stale or wrong session's snapshot.
2. Worker auth cross-contamination: The worker upstream auth error: 401 with "login fail: Please carry the API secret key" is MiniMax's error format. Then invalid x-api-key is Anthropic's format. Workers are picking up the wrong session's credentials.
Both point to the same root cause: the setLastSeenAuth() / resolveAuth() global fallback mechanism. When a MiniMax session sends a request, its API key becomes the lastSeenAuth. Then when an Anthropic session's worker needs auth, if the session-specific auth is stale, it falls back to lastSeenAuth — which is now the MiniMax key.
These are pre-existing issues (the global auth fallback was always there), but they're now more visible because the fetch interceptor correctly routes multiple providers simultaneously. Previously, all providers were forced through the gateway with the same baseURL override, so cross-contamination was masked.
These issues are beyond the scope of this PR (#577 fixes path normalization + free-model pricing). They need a deeper fix to the session-auth and cache-warmer architecture. Should I file them as separate issues and merge #577?
these are critical issues that introduced in our earlier PRs. Let's land 577 and immediately start working on these
Changes to push to origin: bookmark: fix/interceptor-api-path [move sideways from 629b92ffec11 to e02e17853d25]
The push went through but the merge may need a moment for CI to re-run on the new commit. Let me check:
preview pending 0 https://github.com/BYK/loreai/actions/runs/27040319583/job/79814486362 test pending 0 https://github.com/BYK/loreai/actions/runs/27040318336/job/79814501733 changes pass 5s https://github.com/BYK/loreai/actions/runs/27040318336/job/79814482189
CI is running on the new push. Let me wait for it to pass before merging, and meanwhile start analyzing the auth cross-contamination issue.
│ Run "pnpm approve-builds" to pick which dependencies should be allowed │ │ to run scripts. │ │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ Done in 10.7s using pnpm v10.28.0 ---test--- 4 fail 2 errors 9862 expect() calls Ran 2251 tests across 83 files. [118.56s] ELIFECYCLE Test failed. See above for more details.
Same 4 failures under pnpm. Let me look at them more carefully:
at <anonymous> (/home/byk/.local/share/opencode/worktree/51d1015fd1f616acf9a5ffaad9331ec6b304586b/clever-circuit/packages/core/test/agents-file.test.ts:1518:43) (fail) lore file cache optimization > shouldImportLoreFile detects external edits via mtime change [5.00ms] -- 2250 pass 1 fail
CI Status pending 0 https://github.com/BYK/loreai/actions/runs/27040318336/job/79815071116 Binary smoke (darwin-arm64) pass 25s https://github.com/BYK/loreai/actions/runs/27040318336/job/79814751494 Binary smoke (windows-x64) pass 2m10s https://github.com/BYK/loreai/actions/runs/27040318336/job/79814751450 changes pass 5s https://github.com/BYK/loreai/actions/runs/27040318336/job/79814482189 test pass 1m41s https://github.com/BYK/loreai/actions/runs/27040318336/job/79814501733 Binary smoke (darwin-arm64) pass 25s https://github.com/BYK/loreai/actions/runs/27040318336/job/79814751494 Binary smoke (windows-x64) pass 2m10s https://github.com/BYK/loreai/actions/runs/27040318336/job/79814751450 CI Status pass 2s https://github.com/BYK/loreai/actions/runs/27040318336/job/79815071116 changes pass 5s https://github.com/BYK/loreai/actions/runs/27040318336/job/79814482189 test pass 1m41s https://github.com/BYK/loreai/actions/runs/27040318336/job/79814501733 Binary smoke ...
All green. Merging:
! Pull request BYK/loreai#577 was already merged
Already merged (the earlier merge command went through). Good. Let me sync and start on the auth cross-contamination fix:
bookmark: fix/interceptor-api-path@origin [deleted] untracked bookmark: main@origin [updated] untracked Abandoned 2 commits that are no longer reachable: wkwwnvvw e02e1785 fix/interceptor-api-path@git | fix(gateway): treat :free suffix models as zero-cost for budget throttle ssmupklo 04a8eb1e fix(core): normalize provider API paths in fetch interceptor Working copy (@) now at: qvqnznnv 74f92929 (empty) (no description set) Parent commit (@-) : poklqlzn 166eebdc main | fix(gateway): use original fetch for upstream calls to prevent interceptor loop (#576) Added 0 files, modified 3 files, removed 0 files Abandoned 1 commits: qvqnznnv 74f92929 (empty) (no description set) Working copy (@) now at: wpswoqtv 30ec7b42 (empty) (no description set) Parent commit (@-) : poklqlzn 166eebdc main | fix(gateway): use original fetch for upstream calls to prevent interceptor loop (#576) Working copy (@) now at: wtrmxumn 4b429fb5 (empty) (no description set) Parent ...
Now let me analyze the auth cross-contamination problem. The two issues from the logs:
setLastSeenAuth() global fallback — when a MiniMax session sends a request, its API key becomes the global lastSeenAuth. When an Anthropic worker needs auth and the session-specific auth is stale, it falls back to this global value — which is now the MiniMax key.
Cache warmer model cross-contamination — the lastUpstream snapshot alternates between MiniMax-M3 and claude-opus-4-6 on the same session, meaning the cache warmer warms with the wrong model.
Let me trace both issues in the code:
I need to understand how auth credentials and the lastUpstream snapshot get cross-contaminated between sessions in the Lore gateway.
The symptoms are:
MiniMax-M3 and claude-opus-4-6 model names for the same session (session 1TjLgLHS3kg0Go98)Search for:
In packages/gateway/src/auth.ts — find setLastSeenAuth(), getLastSeenAuth(), setSessionAuth(), getSessionAuth(), resolveAuth(). Understand how the global fallback works and whether it can cross-contaminate sessions.
In packages/gateway/src/pipeline.ts — find where setLastSeenAuth() is called (it captures auth from every incoming request). Also find where extractAuth() is called and how auth flows from the incoming request to rawHeaders to session state.
The cache warmer issue: in packages/gateway/src/cache-warmer.ts — find how it reads lastUpstream (or the old lastModel). The log shows session 1TjLgLHS3kg0Go98 alternating between MiniMax-M3 and claude-opus-4-6. This means lastUpstream.model is being overwritten by a different session's request. Check if the cache warmer shares state across sessions.
In packages/gateway/src/pipeline.ts — check how sessionState.lastUpstream is captured. The postResponse section writes to sessionState.lastUpstream. Could there be a race condition where two sessions write to the same SessionState?
Check session ID resolution — could two different OpenCode windows (one using Anthropic, one using MiniMax) resolve to the SAME session ID? Look at how x-lore-session-id is used and whether model switching within the same UI session (user picks a different provider mid-conversation) is the actual cause.
Return all relevant code with line numbers and your analysis of the root cause.
I'll systematically search through the codebase to trace the auth and session state flow. Let me start with all the key files.
<path>/home/byk/Code/opencode-lore/packages/gateway/src/auth.ts</path>
<type>file</type>
<content>
1: /**
2: * Gateway authentication: typed credentials, per-session registry, and
3: * two-level lookup for background workers.
4: *
5: * Replaces the bare `lastSeenApiKey` string with a typed `AuthCredential`
6: * that supports both API-key (`x-api-key`) and OAuth Bearer token
7: * (`Authorization: Bearer`) authentication schemes.
8: *
9: * The per-session registry ensures background workers (distillation,
10: * curation, batch queue) use the correct credential for their session
11: * even when multiple clients are connected simultaneously.
12: */
13:
14: import { createHash } from "node:crypto";
15:
16: // ---------------------------------------------------------------------------
17: // AuthCredential type
18: // ---------------------------------------------------------------------------
19:
20: /** Auth credential — either an API key or an OAuth bearer token. ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/cache-warmer.ts</path> <type>file</type> <content> 1: /** 2: * Speculative cache warming — sends keepalive requests to refresh 3: * Anthropic's prompt cache before it expires. 4: * 5: * Uses survival analysis on inter-turn gaps to predict whether the user 6: * will return before the cache TTL expires. If the expected value of 7: * warming (P(return) × cache_miss_savings) exceeds the warmup cost 8: * (cache_read_cost), sends a max_tokens:0 request that refreshes the 9: * cache without generating output. 10: * 11: * Key design decisions: 12: * - Cache keys are computed from tokenized prompt content (tools → 13: * system → messages), NOT raw JSON bytes. max_tokens, stream, and 14: * temperature are not part of the cache key. Confirmed by Anthropic 15: * pre-warming docs and cache invalidation table. ...
Found 54 matches
/home/byk/Code/opencode-lore/packages/gateway/src/quota.ts:
Line 29: import { authFingerprint, resolveAuth } from "./auth";
Line 300: const c = cred ?? resolveAuth(sessionID);
/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts:
Line 137: extractAuth,
Line 139: setLastSeenAuth,
Line 140: setSessionAuth,
Line 141: resolveAuth,
Line 901: : resolveAuth;
Line 1491: const cred = extractAuth(req.rawHeaders);
Line 1643: const routingAuth = extractAuth(req.rawHeaders);
Line 2931: // Auth refreshes when the next client request arrives via setSessionAuth().
Line 2932: if (isAuthStale(sessionID) && !resolveAuth(sessionID)) return;
Line 2944: const quotaPaused = isQuotaPaused(resolveAuth(sessionID));
Line 3613: const cred = extractAuth(req.rawHeaders);
Line 3615: setLastSeenAuth(cred);
Line 3689: setSessionAuth(sessionID, cred);
Line 4336: const quotaSnapshot = ...
Found 31 matches
/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts:
Line 930: // session has a non-default upstream (lastUpstream.url from a provider route).
Line 935: if (state?.lastUpstream?.url) {
Line 938: upstreamUrl: state.lastUpstream.url,
Line 2788: sessionState.lastUpstream?.model ?? req.model,
Line 2837: sessionState.lastUpstream = {
Line 2872: sessionState.lastUpstream?.providerID ??
Line 2873: protocolToProviderID(sessionState.lastUpstream?.protocol),
Line 2895: sessionState.lastUpstream?.providerID ??
Line 2896: protocolToProviderID(sessionState.lastUpstream?.protocol),
Line 2937: sessionState.lastUpstream?.providerID ??
Line 2938: protocolToProviderID(sessionState.lastUpstream?.protocol);
Line 3192: sessionState.lastUpstream?.providerID ??
Line 3193: protocolToProviderID(sessionState.lastUpstream?.protocol),
Line ...
Now let me read the critical sections of pipeline.ts — where setLastSeenAuth is called, where lastUpstream is written, and session ID resolution:
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
3600: config: GatewayConfig,
3601: ): Promise<Response> {
3602: // --- 1. Project path & init ---
3603: // Enrich headers with context markers injected by lore-hermes plugin.
3604: // This lets getProjectPath() pick up [lore:project=...] via the existing
3605: // header resolution path without modifying config.ts.
3606: if (!req.rawHeaders["x-lore-project"]) {
3607: const markerProject = extractProjectMarker(req.messages);
3608: if (markerProject) req.rawHeaders["x-lore-project"] = markerProject;
3609: }
3610: const pathResult = getProjectPath(req.system, req.rawHeaders);
3611:
3612: // --- 2. Capture auth credentials for background workers ---
3613: const cred = extractAuth(req.rawHeaders);
3614: if (cred) {
3615: setLastSeenAuth(cred);
3616: }
3617:
3618: // --- 3. ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path> <type>file</type> <content> 2820: } 2821: } 2822: // Capture the full routing snapshot from this request. Workers, cache 2823: // warmer, and idle handler all read from this single source of truth 2824: // instead of reconstructing from individual last* fields. 2825: const lpProvider = extractProviderHeader(req.rawHeaders); 2826: const lpRoute = lpProvider ? resolveProviderRoute(lpProvider) : null; 2827: const lpHeaderUpstream = extractUpstreamUrlHeader(req.rawHeaders); 2828: const lpRouteUsable = 2829: lpRoute && (lpRoute.url != null || lpHeaderUpstream) ? lpRoute : null; 2830: const snapshotProtocol: "anthropic" | "openai" | "openai-responses" = 2831: req.protocol === "openai-responses" 2832: ? "openai-responses" 2833: : (lpRouteUsable?.protocol ?? 2834: resolveUpstreamRoute(req.model)?.protocol ?? 2835: ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
1475: const markerKey = `context-marker:${markerSid}`;
1476: const existingSid = headerSessionIndex.get(markerKey);
1477: if (existingSid) {
1478: return { sessionID: existingSid, isNew: false, tier: 2.5 as const };
1479: }
1480: // New session identified via context marker.
1481: const sessionID = generateSessionID();
1482: headerSessionIndex.set(markerKey, sessionID);
1483: return { sessionID, isNew: true, tier: 2.5 as const };
1484: }
1485:
1486: // --- Tier 3: Fingerprint fallback ---
1487: const rawMessages = req.messages.map((m) => ({
1488: role: m.role,
1489: content: m.content,
1490: }));
1491: const cred = extractAuth(req.rawHeaders);
1492: const fingerprint = await fingerprintMessages(rawMessages, {
1493: authSuffix: cred ? authFingerprint(cred) : "",
1494: });
1495: const msgCount = ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
5270: }),
5271: {
5272: status,
5273: headers: { "content-type": "application/json" },
5274: },
5275: );
5276: }
5277:
5278: // ---------------------------------------------------------------------------
5279: // Main entry point
5280: // ---------------------------------------------------------------------------
5281:
5282: /**
5283: * Process an incoming gateway request through the full Lore pipeline.
5284: *
5285: * Returns a standard `Response` object — either a streaming SSE response
5286: * or a JSON response, depending on the client's `stream` setting.
5287: */
5288: export async function handleRequest(
5289: req: GatewayRequest,
5290: config: GatewayConfig,
5291: ): Promise<Response> {
5292: try {
5293: // Capture auth credentials early for background workers
5294: const earlyAuth = extractAuth(req.rawHeaders);
5295: if ...
Now let me read the session identification tiers more carefully, particularly Tier 1 and Tier 2, and the SessionState type definition:
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
1340: async function identifySession(
1341: req: GatewayRequest,
1342: _projectPath: string,
1343: ): Promise<{ sessionID: string; isNew: boolean; tier: 1 | 2 | 2.5 | 3 }> {
1344: const headers = req.rawHeaders;
1345:
1346: // --- Tier 1: Known headers ---
1347: // Sub-agent requests (carrying x-parent-session-id) are NOT merged into the
1348: // parent session. They carry their own x-session-affinity nanoid and get
1349: // independent sessions, benefiting from the full Lore pipeline (LTM,
1350: // gradient, distillation) on their own state without corrupting the parent.
1351:
1352: const known = extractKnownSessionHeader(headers);
1353: if (known) {
1354: const indexKey = `${known.headerName}:${known.sessionId}`;
1355: const existingSid = headerSessionIndex.get(indexKey);
1356: if (existingSid) {
1357: // Session may only exist in DB (after ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/translate/types.ts</path> <type>file</type> <content> 290: /** Uncompressed byte length of lastRequestBody (for prefix match %). */ 291: lastRequestBodyLength: number; 292: /** cache_read_input_tokens from last API response. */ 293: lastCacheRead: number; 294: /** cache_creation_input_tokens from last API response. */ 295: lastCacheCreation: number; 296: /** Total turns observed. */ 297: turnCount: number; 298: /** Confirmed busts (API returned cacheRead=0 with cacheCreation>0). */ 299: bustCount: number; 300: }; 301: 302: /** Routing snapshot captured from the last successful session request. 303: * Workers (distillation, curation) and the cache warmer use this 304: * to route through the same upstream with matching credentials. 305: * Single source of truth — replaces lastModel, lastProtocol, 306: * lastProviderID, lastUpstreamUrl, lastAnthropicBeta. ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
880: }
881:
882: log.info(`gateway pipeline initialized: ${projectPath}`);
883: }
884:
885: function getLLMClient(config: GatewayConfig): LLMClient {
886: if (!llmClient) {
887: const cfg = loreConfig();
888: const defaultModel = cfg.model ?? {
889: providerID: "anthropic",
890: modelID: "claude-sonnet-4-6",
891: };
892:
893: // Worker-specific auth: when LORE_WORKER_API_KEY is set, workers use a
894: // dedicated credential instead of the session's client key. This enables
895: // routing workers to a different provider (e.g. MiniMax) while sessions
896: // continue using Anthropic. Falls back to session auth when not set.
897: const workerApiKey = config.workerApiKey;
898: const getWorkerAuth: (sessionID?: string) => AuthCredential | null =
899: workerApiKey
900: ? ...
Now let me look at the postResponse function where lastUpstream is written, and the getOrCreateSession function to understand session sharing:
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
2700: temporal.store({
2701: projectPath,
2702: info: assistantMsg.info,
2703: parts: assistantMsg.parts,
2704: });
2705: }
2706: // Always record structured tool-call traces — even when the assistant
2707: // content is empty after recall-marker stripping, or when partsToText
2708: // would produce empty content (tool-only / all-failed turns). Tool parts
2709: // survive the text-only recall-marker filter above.
2710: temporal.recordToolCalls({
2711: projectPath,
2712: info: assistantMsg.info,
2713: parts: assistantMsg.parts,
2714: });
2715: }
2716:
2717: // Update session state (persisted in the batched save after messageCount update)
2718: sessionState.turnsSinceCuration =
2719: (sessionState.turnsSinceCuration ?? ...
Found 1 matches /home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts: Line 1134: function getOrCreateSession(
Found 4 matches
/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts:
Line 89: KNOWN_SESSION_HEADERS,
Line 1367: for (const fallbackName of KNOWN_SESSION_HEADERS) {
/home/byk/Code/opencode-lore/packages/gateway/src/session.ts:
Line 267: export const KNOWN_SESSION_HEADERS = [
Line 282: for (const name of KNOWN_SESSION_HEADERS) {
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
1134: function getOrCreateSession(
1135: sessionID: string,
1136: projectPath: string,
1137: pathSource: ProjectPathResult["source"] = "cwd",
1138: ): SessionState {
1139: let state = sessions.get(sessionID);
1140: if (!state) {
1141: // Restore persisted tracking state from DB (survives process restarts)
1142: const persisted = loadSessionTracking(sessionID);
1143: state = {
1144: sessionID,
1145: projectPath,
1146: // A freshly-seeded path from the cwd fallback is NOT a confident binding.
1147: // Mark it provisional so a later header/inferred turn can overwrite it
1148: // (and self-heal any rows stored under the provisional path). Only
1149: // header/inferred seeds are confident. `projectPath` itself is not
1150: // persisted in session_state, so this is purely in-memory bootstrap. ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/session.ts</path>
<type>file</type>
<content>
250: */
251: export function detectClientType(
252: rawHeaders: Record<string, string>,
253: ): ClientType {
254: if (rawHeaders["x-claude-code-session-id"]) return "claude-code";
255: if (rawHeaders["x-session-affinity"]) return "opencode";
256: return "generic";
257: }
258:
259: // ---------------------------------------------------------------------------
260: // Session identification
261: // ---------------------------------------------------------------------------
262:
263: /**
264: * Well-known HTTP headers that carry a persistent, unique session ID.
265: * Checked in order — first match wins. ...
Found 7 matches /home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts: Line 90: extractKnownSessionHeader, Line 1352: const known = extractKnownSessionHeader(headers); Line 5017: const known = extractKnownSessionHeader(req.rawHeaders); Line 5066: const known = extractKnownSessionHeader(req.rawHeaders); Line 5132: const known = extractKnownSessionHeader(req.rawHeaders); Line 5302: const known = extractKnownSessionHeader(req.rawHeaders); /home/byk/Code/opencode-lore/packages/gateway/src/session.ts: Line 279: export function extractKnownSessionHeader(
Now let me look at the idle handler's warmup execution loop to see how it iterates over sessions, and check the worker model resolution:
<path>/home/byk/Code/opencode-lore/packages/gateway/src/idle.ts</path> <type>file</type> <content> 140: // is still active, not genuinely idle. Distillation/curation should 141: // wait for the actual idle period after the tool-use turn completes. 142: if (state.lastStopReason === "tool_use") continue; 143: 144: // Skip sessions with stale auth credentials — background LLM calls 145: // (distillation, curation) would just 401, flooding Sentry with 146: // events every 30s. Auth refreshes when the next client request 147: // arrives via setSessionAuth(), which clears the stale flag. 148: if (isAuthStale(sessionID) && !resolveAuth(sessionID)) continue; 149: 150: // Skip background work for OAuth accounts near quota exhaustion — preserve 151: // remaining entitlement for user-facing conversation turns. 152: if (isQuotaPaused(resolveAuth(sessionID))) continue; 153: 154: inProgress.add(sessionID); 155: ...
Found 18 matches
/home/byk/Code/opencode-lore/packages/gateway/src/worker-model.ts:
Line 437: export function getWorkerModel(
/home/byk/Code/opencode-lore/packages/gateway/src/cost-tracker.ts:
Line 8: * All estimates use the synchronous pricing cache from worker-model.ts —
Line 13: import { getModelEntrySync, getWorkerModel } from "./worker-model";
Line 1053: const workerResult = getWorkerModel();
/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts:
Line 148: getWorkerModel,
Line 154: } from "./worker-model";
Line 2871: getWorkerModel(
Line 2894: getWorkerModel(
Line 2939: const model = getWorkerModel(sessionProvider);
Line 3010: getModelEntrySync(getWorkerModel(sessionProvider)?.modelID ?? ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/cache-warmer.ts</path>
<type>file</type>
<content>
1428: const warmupCost = readCost + writeCost;
1429: const costStr = `$${warmupCost.toFixed(4)}`;
1430:
1431: // Log the outcome with full cache statistics
1432: const sid = state.sessionID.slice(0, 16);
1433: const hitRate =
1434: totalInput > 0
1435: ? `${((cacheReadTokens / totalInput) * 100).toFixed(0)}%`
1436: : "N/A";
1437:
1438: if (cacheReadTokens > 0 && cacheCreationTokens === 0) {
1439: log.info(
1440: `cache-warmer: ✓ refresh session=${sid} ` +
1441: `input=${totalInput} cacheRead=${cacheReadTokens} hit=${hitRate} cost=${costStr}`,
1442: );
1443: } else if (cacheReadTokens > 0 && cacheCreationTokens > 0) {
1444: // Partial hit — some breakpoints read, some written (e.g. conversation
1445: // breakpoint expired but system/tools still cached). This is fine.
1446: ...
Now let me check the handleConversationTurn function to see the full flow — where postResponse is called and whether there's any async gap where lastUpstream could be overwritten:
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
3720: // this seeds the first round of candidate collection.
3721: if (!sessionState.headerSessionId) {
3722: const result = learnHeaders(
3723: sessionState.candidateHeaders,
3724: req.rawHeaders,
3725: );
3726: sessionState.candidateHeaders = result.updatedCandidates;
3727: }
3728:
3729: // Re-check knowledge files on new session start. The file watcher
3730: // covers live edits, but this catches cases where:
3731: // - The watcher wasn't set up (file didn't exist at startup)
3732: // - The watcher missed an event (e.g. ...
Found 1 matches /home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts: Line 2541: function postResponse(
Let me now read the postResponse function from its start, and also check where it's called from:
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
2541: function postResponse(
2542: req: GatewayRequest,
2543: resp: GatewayResponse,
2544: sessionState: SessionState,
2545: config: GatewayConfig,
2546: /** Serialized JSON body sent upstream — for cache prefix comparison. */
2547: requestBody?: string,
2548: /** Active gen_ai.chat span to finalize with usage attributes. */
2549: genAiSpan?: Sentry.Span,
2550: ): void {
2551: const { sessionID, projectPath } = sessionState;
2552:
2553: try {
2554: // --- Calibrate overhead from real token counts ---
2555: const actualInput =
2556: (resp.usage.inputTokens ?? 0) +
2557: (resp.usage.cacheReadInputTokens ?? 0) +
2558: (resp.usage.cacheCreationInputTokens ?? 0);
2559: calibrate(actualInput, sessionID, getLastTransformedCount(sessionID));
2560:
2561: // --- Sentry cache context + cost metric ---
2562: ...
Found 9 matches /home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts: Line 1837: // The accumulator sees the original (un-offset) data so postResponse() Line 2485: // postResponse() has already consumed the real values for calibration/bustRate. Line 2541: function postResponse( Line 4382: // The span is ended in postResponse() after usage attributes are set. Line 4511: postResponse( Line 4556: postResponse( Line 4577: postResponse( Line 4621: postResponse( Line 4663: postResponse(req, resp, sessionState, config, requestBody, genAiSpan),
Let me now look at all the postResponse call sites and the streaming path to understand if postResponse can run for two concurrent sessions on the same sessionState:
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
4370: // Track throttle event on session costs
4371: const costs = getSessionCosts(sessionID);
4372: if (costs) {
4373: costs.throttle.events++;
4374: costs.throttle.totalDelayMs += actualDelay * 1000;
4375: }
4376: }
4377: }
4378: }
4379:
4380: // Start gen_ai.chat span before the upstream call so it captures real
4381: // wall-clock duration (including network latency and streaming time).
4382: // The span is ended in postResponse() after usage attributes are set.
4383: const genAiSpan = Sentry.startInactiveSpan({
4384: op: "gen_ai.chat",
4385: name: `chat ${req.model}`,
4386: attributes: {
4387: "gen_ai.operation.name": "chat",
4388: "gen_ai.request.model": req.model,
4389: "gen_ai.provider.name": (() => {
4390: if (req.protocol === "openai-responses") return ...
Now let me check the fingerprintMessages function to understand whether two different sessions (Anthropic + MiniMax) could collide on fingerprint:
Found 1 matches /home/byk/Code/opencode-lore/packages/gateway/src/session.ts: Line 180: export async function fingerprintMessages(
Found 7 matches /home/byk/Code/opencode-lore/packages/gateway/src/session.ts: Line 7: * `x-lore-session-id` (Lore plugins: OpenCode, Pi — stable, deterministic), Line 8: * `x-claude-code-session-id` (Claude Code), `x-session-affinity` Line 235: * - "opencode": uses x-session-affinity, currently sends no max_tokens Line 245: * 2. x-session-affinity → "opencode" Line 255: if (rawHeaders["x-session-affinity"]) return "opencode"; Line 268: "x-lore-session-id", // Lore plugins (stable, deterministic) — checked first Line 270: "x-session-affinity", // OpenCode (nanoid, volatile — regenerated on restart)
Found 2 matches /home/byk/Code/opencode-lore/packages/gateway/src/translate/types.ts: Line 487: "x-lore-provider", /home/byk/Code/opencode-lore/packages/gateway/src/config.ts: Line 392: const raw = headers["x-lore-provider"];
<path>/home/byk/Code/opencode-lore/packages/gateway/src/session.ts</path>
<type>file</type>
<content>
180: export async function fingerprintMessages(
181: messages: Array<{ role: string; content: unknown }>,
182: extras?: { authSuffix?: string },
183: ): Promise<string> {
184: let firstUserContent = "";
185: for (const msg of messages) {
186: if (msg.role === "user") {
187: const texts = extractTextParts(msg.content);
188: firstUserContent = texts.join("");
189: break;
190: }
191: }
192:
193: const material = firstUserContent + (extras?.authSuffix ?? "");
194: const encoded = new TextEncoder().encode(material);
195: const hash = await crypto.subtle.digest("SHA-256", encoded);
196: const bytes = new Uint8Array(hash);
197:
198: // First 16 hex chars (8 bytes)
199: let hex = "";
200: for (let i = 0; i < 8; i++) {
201: hex += bytes[i].toString(16).padStart(2, "0");
202: }
203: return hex;
204: }
205:
206: // ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/config.ts</path>
<type>file</type>
<content>
380: /** Maximum allowed length for a provider ID header value. */
381: const MAX_PROVIDER_ID_LENGTH = 64;
382:
383: /**
384: * Extract and validate the `X-Lore-Provider` header from a request.
385: *
386: * Returns the sanitized provider ID (lowercase, alphanumeric + hyphens only)
387: * or `undefined` when the header is absent or invalid.
388: */
389: export function extractProviderHeader(
390: headers: Record<string, string>,
391: ): string | undefined {
392: const raw = headers["x-lore-provider"];
393: if (!raw) return undefined;
394:
395: // Sanitize: strip control characters, trim, lowercase.
396: // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional control-character sanitization
397: const cleaned = raw.replace(/[\x00-\x1f\x7f]/g, "");
398: const sanitized = cleaned.trim().toLowerCase();
399: if (!sanitized || sanitized.length > ...
Now I have a thorough picture. Let me verify one more thing — does OpenCode send x-session-affinity and x-lore-session-id per conversation, or per OpenCode instance? And does switching models within OpenCode change the session header?
Found 1 matches /home/byk/Code/opencode-lore/packages/opencode/src/index.ts: Line 235: // unlike x-session-affinity (nanoid regenerated per process).
Found 1 matches /home/byk/Code/opencode-lore/packages/opencode/src/index.ts: Line 236: output.headers["x-lore-session-id"] = input.sessionID;
<path>/home/byk/Code/opencode-lore/packages/opencode/src</path> <type>directory</type> <entries> index.ts (1 entries) </entries>
<path>/home/byk/Code/opencode-lore/packages/opencode/src/index.ts</path>
<type>file</type>
<content>
210: ...(cfg.agent as Record<string, unknown> | undefined),
211: "lore-distill": {
212: hidden: true,
213: description: "Lore memory distillation worker",
214: },
215: "lore-curator": {
216: hidden: true,
217: description: "Lore knowledge curator worker",
218: },
219: "lore-query-expand": {
220: hidden: true,
221: description: "Lore query expansion worker",
222: },
223: };
224: },
225:
226: tool: {},
227:
228: // Inject per-request identifiers so the gateway can distinguish meta
229: // requests (title generation, summary agents, etc.) from real
230: // conversation turns and route by provider. ...
Now let me check a critical detail — does OpenCode use the same sessionID across model switches within a conversation, or does it create a new one? And what does the input.sessionID map to?
<path>/home/byk/Code/opencode-lore/packages/opencode/src/index.ts</path>
<type>file</type>
<content>
1: import type { Plugin, Hooks } from "@opencode-ai/plugin";
2: import {
3: log,
4: getGitRemote,
5: discoverWorkspaceRoot,
6: installFetchInterceptor,
7: } from "@loreai/core";
8:
9: /**
10: * Lore plugin for OpenCode — transparent LLM proxy routing.
11: *
12: * Instead of overwriting provider baseURLs (which loses original auth and
13: * URL context), this plugin installs a fetch-level interceptor that
14: * transparently reroutes outgoing LLM API calls through the Lore gateway.
15: * The SDK builds requests normally (correct auth, correct URL for each
16: * provider), and the interceptor redirects them while preserving all
17: * original headers. The gateway forwards non-managed headers upstream.
18: *
19: * Per-request context (session ID, agent name, provider ID) is injected
20: * via the `chat.headers` hook. ...
Now I have all the information needed to provide a comprehensive analysis. Let me verify one final detail — whether the lastSeenAuth global is really a single variable (not per-session), which is the crux of the auth contamination:
<path>/home/byk/Code/opencode-lore/packages/gateway/src/auth.ts</path>
<type>file</type>
<content>
130:
131: // ---------------------------------------------------------------------------
132: // Global fallback (replaces lastSeenApiKey)
133: // ---------------------------------------------------------------------------
134:
135: let lastSeenAuth: AuthCredential | null = null;
136:
137: export function setLastSeenAuth(cred: AuthCredential): void {
138: lastSeenAuth = cred;
139: }
140:
141: export function getLastSeenAuth(): AuthCredential | null {
142: return lastSeenAuth;
143: }
144:
145: // ---------------------------------------------------------------------------
146: // Two-level lookup
147: // ---------------------------------------------------------------------------
148:
149: /**
150: * Resolve auth credentials for a given session.
151: *
152: * 1. If `sessionID` is provided, check the per-session registry first. ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
5288: export async function handleRequest(
5289: req: GatewayRequest,
5290: config: GatewayConfig,
5291: ): Promise<Response> {
5292: try {
5293: // Capture auth credentials early for background workers
5294: const earlyAuth = extractAuth(req.rawHeaders);
5295: if (earlyAuth) {
5296: setLastSeenAuth(earlyAuth);
5297: }
5298:
5299: // --- Quick Tier-1 session lookup for structural compaction detection ---
5300: // O(1) header + map lookup — lets us compare message counts before routing.
5301: let priorState: SessionState | undefined;
5302: const known = extractKnownSessionHeader(req.rawHeaders);
(Showing lines 5288-5302 of 5355. Use offset=5303 to continue.)
</content>
Now I have all the pieces. Here is the comprehensive analysis:
lastUpstream Cross-Contamination Between SessionsFile: /home/byk/Code/opencode-lore/packages/gateway/src/auth.ts
The auth system has two layers:
sessionAuth Map, line 85): keyed by Lore session ID, stores the credential from each session's last request.lastSeenAuth, line 135): a single AuthCredential variable, overwritten by every incoming request from any session.The global is set in two places in pipeline.ts:
Line 5294-5296 (handleRequest entry point) -- before session identification:
const earlyAuth = extractAuth(req.rawHeaders);
if (earlyAuth) {
setLastSeenAuth(earlyAuth); // GLOBAL — any session's key
}
Line 3613-3615 (handleConversationTurn) -- after session identification:
const cred = extractAuth(req.rawHeaders);
if (cred) {
setLastSeenAuth(cred); // GLOBAL — overwrites again
}
The per-session credential is set at line 3689:
if (cred) {
setSessionAuth(sessionID, cred); // PER-SESSION — correct credential
}
Under normal operation, resolveAuth(sessionID) at line 163-177 of auth.ts correctly returns the per-session credential first:
export function resolveAuth(sessionID?: string): AuthCredential | null {
if (sessionID) {
const cred = getSessionAuth(sessionID);
if (cred && !staleSessionAuth.has(sessionID)) return cred; // <-- happy path
// ... ...