Dashboard › Code › Session s7880c9649c7
s7880c9649c757ca9[ci/docs-preview-and-skip b3df88f] fix: address review findings 2 files changed, 28 insertions(+), 12 deletions(-) To https://github.com/BYK/loreai.git 418504b..b3df88f ci/docs-preview-and-skip -> ci/docs-preview-and-skip
Now let me watch CI:
Refreshing checks status every 10 seconds. Press Ctrl+C to quit. test pending 0 https://github.com/BYK/loreai/actions/runs/26978896563/job/79612889092 changes pass 8s https://github.com/BYK/loreai/actions/runs/26978896563/job/79612856051 preview pass 9s https://github.com/BYK/loreai/actions/runs/26978896598/job/79612856208 Refreshing checks status every 10 seconds. Press Ctrl+C to quit. test pending 0 https://github.com/BYK/loreai/actions/runs/26978896563/job/79612889092 changes pass 8s https://github.com/BYK/loreai/actions/runs/26978896563/job/79612856051 preview pass 9s https://github.com/BYK/loreai/actions/runs/26978896598/job/79612856208 Refreshing checks status every 10 seconds. Press Ctrl+C to quit. test pending 0 https://github.com/BYK/loreai/actions/runs/26978896563/job/79612889092 changes pass 8s https://github.com/BYK/loreai/actions/runs/26978896563/job/79612856051 preview pass 9s https://github.com/BYK/loreai/actions/runs/26978896598/job/79612856208 Refreshing ...
All green. Now squash merge and run the post-merge commands:
(no output)
Auto-merge enabled. Let me wait for it to merge and then check:
[{"conclusion":"cancelled","createdAt":"2026-06-04T20:56:56Z","databaseId":26979114941,"headBranch":"main","status":"completed"}]
The docs-preview workflow on main was cancelled β likely by the concurrency group since CI also triggered on main push. Let me check what happened and whether the gh-pages branch was created (it was already created by the PR run earlier):
From https://github.com/BYK/loreai * [new branch] gh-pages -> origin/gh-pages ed07369..206492e main -> origin/main origin/gh-pages
The gh-pages branch exists (created during the PR run). But the docs-preview on main was cancelled, so the production docs haven't been deployed to gh-pages yet. Let me check what's on gh-pages currently and re-run the workflow:
1cf2343 Remove preview for PR 544 π¬ 50e7ec7 Deploy preview for PR 544 π« 17e8c50 Initialize gh-pages with .nojekyll
Good β the gh-pages branch exists with .nojekyll and the PR preview was deployed and cleaned up correctly. But the production docs (_preview/pr-main) weren't deployed because the main push workflow was cancelled. Let me re-run it:
could not create workflow dispatch event: HTTP 422: Workflow does not have 'workflow_dispatch' trigger (https://api.github.com/repos/BYK/loreai/actions/workflows/289455033/dispatches)
Right, the workflow doesn't have workflow_dispatch. The production deploy will happen on the next push to main that touches docs/. For now, the gh-pages branch exists and has .nojekyll. Let me switch the Pages source now β once switched, the next docs push will deploy production docs:
My wife (Seylan) who has been testing lore with Codex reported the following error (it showed up once):
[lore] worker upstream auth error: 401 Unauthorized β {"type":"error","error":{"type":"authentication_error","message":"Invalid bearer token"},"request_id":"req_011CbiRYVnArrxK8C543zDSy"}
Is this harmless or something we should be fixing? Also check Sentry for this error as I expect these to be reported there.
No plan file exists yet. You should create your plan at /home/byk/Code/opencode-lore/.opencode/plans/1780606832195-neon-orchid.md using the write tool. You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the explore subagent type.
Focus on understanding the user's request and the code associated with their request
Launch up to 3 explore agents IN PARALLEL (single message, multiple tool calls) to efficiently explore the codebase.
I'll investigate this error to understand if it's harmless or needs fixing. Let me explore the relevant code paths and check Sentry.
Explore the codebase to understand how 401 authentication errors are handled in the worker/background LLM call path. Specifically:
llm-adapter.ts for auth error retry logic (AUTH_ERROR_CODES, markAuthStale, credentialChanged)packages/gateway/src/instrument.ts for TRANSIENT_ERROR_PATTERNS β does it include auth errors?packages/core/src/log.ts for how errors flow to Sentry via sinkpackages/gateway/src/pipeline.ts for how worker auth is resolved (resolveAuth, getLastSeenAuth, isAuthStale)packages/gateway/src/idle.ts or equivalent for whether isAuthStale is checked before scheduling workReturn: exact file paths, line numbers, and the full error handling flow for worker 401s. Note whether the error gets sent to Sentry, whether it causes retries, and whether there's a known gotcha (LOREAI-GATEWAY-Z) about OAuth token expiry causing 401 storms.
I'll systematically trace the 401 error handling path for worker/background LLM calls. Let me start with multiple parallel searches.
(no output)
Found 1 matches
/home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts:
Line 523: `worker upstream auth error: ${response.status} ${response.statusText} β ${text}`,
Found 15 matches
/home/byk/Code/opencode-lore/packages/gateway/test/llm-adapter.test.ts:
Line 6: AUTH_ERROR_CODES,
Line 245: // AUTH_ERROR_CODES β auth failure detection
Line 248: describe("AUTH_ERROR_CODES", () => {
Line 250: expect(AUTH_ERROR_CODES.has(401)).toBe(true);
Line 254: expect(AUTH_ERROR_CODES.has(403)).toBe(true);
Line 259: expect(AUTH_ERROR_CODES.has(429)).toBe(false);
Line 260: expect(AUTH_ERROR_CODES.has(500)).toBe(false);
Line 261: expect(AUTH_ERROR_CODES.has(502)).toBe(false);
Line 262: expect(AUTH_ERROR_CODES.has(503)).toBe(false);
Line 263: expect(AUTH_ERROR_CODES.has(529)).toBe(false);
Line 267: expect(AUTH_ERROR_CODES.has(200)).toBe(false);
Line 268: expect(AUTH_ERROR_CODES.has(400)).toBe(false);
Line 269: expect(AUTH_ERROR_CODES.has(404)).toBe(false);
/home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts:
Line 44: export const AUTH_ERROR_CODES = new Set([401, 403]);
...
Found 2 matches /home/byk/Code/opencode-lore/packages/gateway/instrument.ts: Line 96: const TRANSIENT_ERROR_PATTERNS = [ Line 141: return TRANSIENT_ERROR_PATTERNS.some((re) => re.test(msg));
Found 20 matches
/home/byk/Code/opencode-lore/.lore.md:
Line 14: * **llm-adapter.ts: retry logic, backoff schedule, and circuit breaker integration**: llm-adapter.ts retry/backoff: \`TRANSIENT\_CODES={429,500,502,503,529}\`; \`AUTH\_ERROR\_CODES={401,403}\`. \`MAX\_RETRIES\_RATE\_LIMIT=3\`; \`MAX\_RETRIES\_SERVER=3\`; \`MAX\_RETRIES\_URGENT=2\`. \`backoffMs()\`: Retry-After present β \`min(retryAfterMs, cap)\`; urgent (no header) β \`min(1000Γ2^attempt, 4000)\`; 429 non-urgent β \`min(60\_000+attemptΓ60\_000, 180\_000)\`; 5xx β \`min(1000Γ2^attempt, 8000)\`. On 429 non-urgent β \`tripCircuitBreaker(pauseSec)\` before retry. Auth errors β \`markAuthStale\`, re-resolve, retry once if \`credentialChanged && attempt===0\`. \`buildAnthropicWorkerRequest()\`: injects billing block only when \`cred.scheme === 'bearer'\`; system sent as block array with \`cache\_control: { type: 'ephemeral', ttl: '1h' }\`; body signed via \`signBody()\`. \`opts.thinking\` intentionally NOT forwarded in bare ...
Found 20 matches /home/byk/Code/opencode-lore/.lore.md: Line 58: * **LOREAI-GATEWAY-Z: OAuth token expiry causes 401 storm β resolveAuth returns same stale token**: LOREAI-GATEWAY-Z: OAuth token expiry causes 401 storm. \`resolveAuth(sessionID)\` marks session stale and falls back to \`getLastSeenAuth()\` β same expired token. \`credentialChanged=false\` so retry-once path never taken. Idle scheduler fires 4+ LLM calls/tick (distill, meta-distill, curation, consolidation), each 401ing independently. \`isAuthStale()\` exported but NEVER called in non-test source. Fix: (1) check \`isAuthStale(sessionID)\` in idle.ts before scheduling LLM work; (2) \`resolveAuth\` returns null when global fallback matches stale credential; (3) add \`/Worker upstream auth error/\` to \`TRANSIENT\_ERROR\_PATTERNS\` in \`instrument.ts\`. Circuit breaker (background-limiter.ts) only trips on 429, not 401. OAuth bearer sessions skip Batch API queue (PR #502). ...
Found 5 matches
/home/byk/Code/opencode-lore/.lore.md:
Line 14: * **llm-adapter.ts: retry logic, backoff schedule, and circuit breaker integration**: llm-adapter.ts retry/backoff: \`TRANSIENT\_CODES={429,500,502,503,529}\`; \`AUTH\_ERROR\_CODES={401,403}\`. \`MAX\_RETRIES\_RATE\_LIMIT=3\`; \`MAX\_RETRIES\_SERVER=3\`; \`MAX\_RETRIES\_URGENT=2\`. \`backoffMs()\`: Retry-After present β \`min(retryAfterMs, cap)\`; urgent (no header) β \`min(1000Γ2^attempt, 4000)\`; 429 non-urgent β \`min(60\_000+attemptΓ60\_000, 180\_000)\`; 5xx β \`min(1000Γ2^attempt, 8000)\`. On 429 non-urgent β \`tripCircuitBreaker(pauseSec)\` before retry. Auth errors β \`markAuthStale\`, re-resolve, retry once if \`credentialChanged && attempt===0\`. \`buildAnthropicWorkerRequest()\`: injects billing block only when \`cred.scheme === 'bearer'\`; system sent as block array with \`cache\_control: { type: 'ephemeral', ttl: '1h' }\`; body signed via \`signBody()\`. \`opts.thinking\` intentionally NOT forwarded in bare ...
Found 3 matches /home/byk/Code/opencode-lore/.lore.md: Line 58: * **LOREAI-GATEWAY-Z: OAuth token expiry causes 401 storm β resolveAuth returns same stale token**: LOREAI-GATEWAY-Z: OAuth token expiry causes 401 storm. \`resolveAuth(sessionID)\` marks session stale and falls back to \`getLastSeenAuth()\` β same expired token. \`credentialChanged=false\` so retry-once path never taken. Idle scheduler fires 4+ LLM calls/tick (distill, meta-distill, curation, consolidation), each 401ing independently. \`isAuthStale()\` exported but NEVER called in non-test source. Fix: (1) check \`isAuthStale(sessionID)\` in idle.ts before scheduling LLM work; (2) \`resolveAuth\` returns null when global fallback matches stale credential; (3) add \`/Worker upstream auth error/\` to \`TRANSIENT\_ERROR\_PATTERNS\` in \`instrument.ts\`. Circuit breaker (background-limiter.ts) only trips on 429, not 401. OAuth bearer sessions skip Batch API queue (PR #502). ...
Good, I have the key locations. Now let me read the actual source files in detail.
<path>/home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts</path>
<type>file</type>
<content>
1: /**
2: * Gateway LLM adapter: implements LLMClient via direct API calls.
3: * Used by Lore's background workers (distillation, curation, query expansion)
4: * running inside the gateway process.
5: *
6: * Supports both Anthropic Messages API and OpenAI Chat Completions API.
7: * The provider is selected at call time based on `model.providerID`:
8: * - "anthropic" β POST /v1/messages (Anthropic wire format)
9: * - "openai" β POST /v1/chat/completions (OpenAI wire format)
10: *
11: * Retry logic, Sentry instrumentation, worker call tracking, and error
12: * handling are shared across both providers.
13: */
14:
15: import type { LLMClient } from "@loreai/core";
16: import { log } from "@loreai/core";
17: import * as Sentry from "@sentry/bun";
18: import type { AuthCredential } from "./auth";
19: import { authHeaders, markAuthStale } from "./auth";
20: import { ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts</path>
<type>file</type>
<content>
460: );
461: }
462:
463: // Enrich span with retry metadata on eventual success
464: if (retryCount > 0) {
465: span.setAttribute("lore.retry.count", retryCount);
466: span.setAttribute("lore.retry.total_delay_ms", totalDelayMs);
467: if (lastRetryAfterMs != null) {
468: span.setAttribute(
469: "lore.retry.last_retry_after_ms",
470: lastRetryAfterMs,
471: );
472: }
473: span.setAttribute("lore.retry.final_status", finalStatus);
474: }
475:
476: return parsed.text;
477: }
478:
479: // --- Auth error: 401/403 β mark stale, re-resolve, retry once ---
480: if ...
<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/instrument.ts</path>
<type>file</type>
<content>
85: ? false
86: : sentryEnvVar === "1"
87: ? true
88: : sentryEnvVar === "0"
89: ? false
90: : !isDev;
91:
92: if (sentryEnabled && !Sentry.isInitialized()) {
93: // Transient network errors that are expected in a long-running LLM proxy.
94: // These are not actionable bugs β they occur when clients disconnect,
95: // upstreams are temporarily unavailable, or network conditions degrade. ...
<path>/home/byk/Code/opencode-lore/packages/core/src/log.ts</path>
<type>file</type>
<content>
1: /**
2: * Lightweight logger that suppresses informational messages by default.
3: *
4: * In TUI mode, all stderr output renders as red "error" text β confusing
5: * for routine status messages like "incremental distillation" or "pruned
6: * temporal messages". Only actual errors should be visible by default.
7: *
8: * Set LORE_DEBUG=1 to see informational messages (useful when debugging
9: * the plugin itself).
10: *
11: * ## Sink registration
12: *
13: * An optional {@link LogSink} can be registered via {@link registerSink}.
14: * When registered, every log call (regardless of `isDebug`) also forwards
15: * to the sink. This is used by the gateway to bridge logs β Sentry without
16: * adding a Sentry dependency to `@loreai/core`.
17: *
18: * ## File logging
19: *
20: * All log calls (info, warn, error) are written to a persistent log file
21: * at ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/idle.ts</path>
<type>file</type>
<content>
40: isWarmupAuthDisabled,
41: clearWarmupAuthDisabled,
42: resolveProfile,
43: blendedHistogramForSession,
44: shouldWarm,
45: executeWarmup,
46: loadGlobalHistograms,
47: flushGlobalHistograms,
48: MIN_TURNS_FOR_WARMING,
49: MIN_INPUT_TOKENS_FOR_WARMING,
50: } from "./cache-warmer";
51: import * as Sentry from "@sentry/bun";
52: import { runBackground } from "./background-limiter";
53: import {
54: isAuthStale,
55: resolveAuth,
56: deleteSessionAuth,
57: clearAuthStale,
58: authFingerprint,
59: } from "./auth";
60: import {
61: emitWarmupMetric,
62: emitSessionCostMetrics,
63: emitCurationMetrics,
64: } from "./sentry";
65: import {
66: getSessionCosts,
67: totalWorkerCost,
68: deleteSessionCosts,
69: } from "./cost-tracker";
70: import { deleteBillingPrefix } from "./cch";
71: import {
72: maybeFetchQuota,
73: isQuotaPaused,
74: ...
Now let me read the remaining parts of the key files to complete the trace.
<path>/home/byk/Code/opencode-lore/packages/core/src/log.ts</path>
<type>file</type>
<content>
151: sink?.info(msg);
152: writeToFile("info", msg);
153: }
154:
155: /** Log a warning. Suppressed unless LORE_DEBUG=1. */
156: export function warn(...args: unknown[]): void {
157: if (isDebug) console.error("[lore] WARN:", ...args);
158: const msg = formatArgs(args);
159: sink?.warn(msg);
160: writeToFile("warn", msg);
161: }
162:
163: /** Log an error. Always visible β these indicate real failures. */
164: export function error(...args: unknown[]): void {
165: console.error("[lore]", ...args);
166: const msg = formatArgs(args);
167: sink?.error(msg);
168: writeToFile("error", msg);
169:
170: const err = findError(args);
171: if (err) sink?.captureException(err);
172: }
(End of file - total 172 lines)
</content>
<path>/home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts</path>
<type>file</type>
<content>
60: /** Cap Retry-After server hints separately for urgent vs background calls.
61: * Urgent paths cannot afford a 60-120s pause even if the server asks. */
62: const RETRY_AFTER_CAP_URGENT_MS = 8_000;
63: const RETRY_AFTER_CAP_BACKGROUND_MS = 120_000;
64:
65: export function maxRetriesFor(
66: status: number | null,
67: urgent: boolean = false,
68: ): number {
69: if (urgent) return MAX_RETRIES_URGENT;
70: if (status === 429) return MAX_RETRIES_RATE_LIMIT;
71: return MAX_RETRIES_SERVER;
72: }
73:
74: /** Parse the Retry-After header into milliseconds, or null if absent/invalid. */
75: export function parseRetryAfter(response: Response): number | null {
76: const header = response.headers.get("retry-after");
77: if (!header) return null;
78: const seconds = Number(header);
79: if (!Number.isNaN(seconds)) return seconds * 1000;
80: const date = ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts</path>
<type>file</type>
<content>
580: urgent,
581: );
582: retryCount++;
583: totalDelayMs += delay;
584: if (retryAfter != null) lastRetryAfterMs = retryAfter;
585: log.warn(
586: `worker upstream ${response.status} (attempt ${attempt + 1}/${maxRetries + 1}), ` +
587: `retrying in ${delay}ms` +
588: (retryAfter != null
589: ? ` (retry-after: ${Math.round(retryAfter / 1000)}s)`
590: : ""),
591: );
592: await sleep(delay);
593: continue;
594: }
595:
596: // Exhausted retries β log, capture Sentry error, enrich span
597: const text = await response.text().catch(() => "(no body)");
598: log.error(
599: ...
Found 9 matches /home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts: Line 133: setLastSeenAuth, Line 134: setSessionAuth, Line 135: resolveAuth, Line 889: : resolveAuth; Line 2787: const quotaPaused = isQuotaPaused(resolveAuth(sessionID)); Line 3276: setLastSeenAuth(cred); Line 3350: setSessionAuth(sessionID, cred); Line 3987: const quotaSnapshot = getQuotaForCredential(resolveAuth(sessionID)); Line 4916: setLastSeenAuth(earlyAuth);
Found 4 matches
/home/byk/Code/opencode-lore/packages/gateway/src/background-limiter.ts:
Line 12: * `tripCircuitBreaker` is called from `llm-adapter.ts` on any
Line 72: export function tripCircuitBreaker(retryAfterSeconds?: number): void {
Line 185: * `tripCircuitBreaker()`.
Line 187: * Unlike `tripCircuitBreaker`, this unconditionally sets the deadline
Let me read the relevant sections of pipeline.ts for auth binding, and background-limiter.ts for the circuit breaker details.
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
3265: // This lets getProjectPath() pick up [lore:project=...] via the existing
3266: // header resolution path without modifying config.ts.
3267: if (!req.rawHeaders["x-lore-project"]) {
3268: const markerProject = extractProjectMarker(req.messages);
3269: if (markerProject) req.rawHeaders["x-lore-project"] = markerProject;
3270: }
3271: const pathResult = getProjectPath(req.system, req.rawHeaders);
3272:
3273: // --- 2. Capture auth credentials for background workers ---
3274: const cred = extractAuth(req.rawHeaders);
3275: if (cred) {
3276: setLastSeenAuth(cred);
3277: }
3278:
3279: // --- 3. Session identification ---
3280: const { sessionID, isNew, tier } = await identifySession(
3281: req,
3282: pathResult.path,
3283: );
3284:
3285: // Strip [lore:session-id=...] and [lore:project=...] context markers from
3286: // user ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/background-limiter.ts</path> <type>file</type> <content> 1: /** 2: * Global concurrency limiter for background LLM work. 3: * 4: * Wraps fire-and-forget background LLM calls (idle distillation, 5: * curation, pipeline-triggered incremental distillation) through a 6: * single p-limit(2) so at most 2 background LLM operations run 7: * concurrently across all sessions. 8: * 9: * Note: auto-import extraction (`import-auto.ts`) is NOT wrapped here β 10: * it creates its own LLM client and runs sequentially per-process. 11: * The circuit breaker still provides protection for import because 12: * `tripCircuitBreaker` is called from `llm-adapter.ts` on any 13: * non-urgent 429, and import uses the same adapter. 14: * 15: * Also provides a circuit breaker that trips on upstream 429 responses, 16: * pausing all background work for the Retry-After period. ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts</path>
<type>file</type>
<content>
880:
881: // Worker-specific auth: when LORE_WORKER_API_KEY is set, workers use a
882: // dedicated credential instead of the session's client key. This enables
883: // routing workers to a different provider (e.g. MiniMax) while sessions
884: // continue using Anthropic. Falls back to session auth when not set.
885: const workerApiKey = config.workerApiKey;
886: const getWorkerAuth: (sessionID?: string) => AuthCredential | null =
887: workerApiKey
888: ? () => ({ scheme: "api-key", value: workerApiKey })
889: : resolveAuth;
890:
891: // Worker-specific upstream: when LORE_WORKER_UPSTREAM is set, all worker
892: // calls route to this URL instead of the default upstream URLs.
893: const workerUpstreams = config.workerUpstream
894: ? { anthropic: config.workerUpstream, openai: config.workerUpstream }
895: : { ...
<path>/home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts</path>
<type>file</type>
<content>
120: }
121:
122: // ---------------------------------------------------------------------------
123: // OpenAI response types & usage normalization (exported for testing)
124: // ---------------------------------------------------------------------------
125:
126: /** OpenAI Chat Completions response shape (subset we need). */
127: type OpenAIChatResponse = {
128: choices?: Array<{ message?: { content?: string } }>;
129: model?: string;
130: usage?: {
131: prompt_tokens?: number;
132: completion_tokens?: number;
133: prompt_tokens_details?: { cached_tokens?: number };
134: };
135: };
136:
137: /**
138: * Normalize OpenAI usage to the AnthropicUsage shape for unified cost tracking. ...
Now let me read the createGatewayLLMClient function signature and the getAuth parameter usage in the retry loop:
<path>/home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts</path>
<type>file</type>
<content>
330: * @param upstreams Base URLs for each provider
331: * @param getAuth Callback to resolve auth credentials (per-session β global fallback)
332: * @param defaultModel Default model to use when no override is specified
333: */
334: export function createGatewayLLMClient(
335: upstreams: { anthropic: string; openai: string },
336: getAuth: (sessionID?: string) => AuthCredential | null,
337: defaultModel: { providerID: string; modelID: string },
338: ): LLMClient {
339: return {
340: async prompt(system, user, opts) {
341: const cred = getAuth(opts?.sessionID);
342: if (!cred) {
343: log.warn("no auth credentials available for worker call");
344: return null;
345: }
346:
347: const model = opts?.model ?? defaultModel;
348: const isOpenAI = model.providerID === "openai";
349: const target = ...
I now have all the information needed to provide a comprehensive trace. Here is the complete analysis.
Exact locations (two variants with different casing):
/home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts, line 523 (lowercase, via log.error()):
`worker upstream auth error: ${response.status} ${response.statusText} β ${text}`
/home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts, line 527 (title-case, via Sentry.captureException()):
`Worker upstream auth error: ${response.status} ${response.statusText}`
When a background worker LLM call (distillation, curation, query expansion) gets a 401 or 403:
/home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts, line 341createGatewayLLMClient() calls getAuth(opts?.sessionID) to get the credential./home/byk/Code/opencode-lore/packages/gateway/src/pipeline.ts, lines 886-889getAuth function is either resolveAuth (default) or a static closure returning { scheme: "api-key", value: workerApiKey } when LORE_WORKER_API_KEY is set./home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts, line 480AUTH_ERROR_CODES.has(response.status) matches β AUTH_ERROR_CODES = new Set([401, 403]) (line 44)./home/byk/Code/opencode-lore/packages/gateway/src/llm-adapter.ts, lines 484-486markAuthStale(opts.sessionID), which adds the session ID to staleSessionAuth set./home/byk/Code/opencode-lore/packages/gateway/src/auth.ts, lines 117-119