-
Notifications
You must be signed in to change notification settings - Fork 514
[WRONG BRANCH] fix: stabilize cache inputs #626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3a0b88f
41deeef
23ec63f
ad58ac8
0215404
44d0294
152682b
627e43c
5823712
4430ed3
d9a2a55
9ce0464
00b3861
ff0596d
e20c21e
8298112
b001af9
7b0fb67
ba2085b
2f3a7dd
b4f95ae
bae00dd
4da468a
479390e
a0972c2
11cdee2
d637087
3af6d4a
bfda29d
d99a373
b8ddda0
dc72a4a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| /** | ||
| * Deterministic ordering of Claude Code's own tool/skills/deferred-tools listings on | ||
| * the native Anthropic passthrough (`anthropicNativePassthrough`, `claude-messages.ts`). | ||
| * Claude Code enumerates MCP tools/skills in whatever order its own reconnect/discovery | ||
| * race resolves them, so byte-identical conversations can arrive with different array | ||
| * order turn to turn — busting Anthropic's prompt-cache prefix for no reason. Sorting is | ||
| * safe: `tool_choice` targets tools by name, not position, and the two system-reminder | ||
| * blocks below are pure listings with no inherent order the model depends on. | ||
| * | ||
| * Ported (algorithm only, re-implemented in TypeScript) from `sort-stabilization.mjs`, | ||
| * MIT licensed, github.com/cnighswonger/claude-code-cache-fix. | ||
| */ | ||
|
Comment on lines
+1
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm whether stabilizeSystemAndToolOrder is ever imported/called
rg -n "stabilizeSystemAndToolOrder" --type=tsRepository: lidge-jun/opencodex Length of output: 157 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== File outline ==\n'
ast-grep outline src/adapters/anthropic-sort-stabilize.ts --view expanded || true
printf '\n== Search for related symbols and file imports ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
"anthropic-sort-stabilize|sort-stabilize|stabilizeSystemAndToolOrder|normalizeSystemReminderText|sortToolsByName|anthropicNativePassthrough|claude-messages" src
printf '\n== Key files that mention the native passthrough or inbound normalization ==\n'
for f in src/adapters/claude-messages.ts src/adapters/inbound.ts src/adapters/anthropic-sort-stabilize.ts; do
if [ -f "$f" ]; then
echo "--- $f ---"
wc -l "$f"
cat -n "$f" | sed -n '1,220p'
fi
doneRepository: lidge-jun/opencodex Length of output: 7115 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== Search for imports of the file by path ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
"from ['\"].*anthropic-sort-stabilize['\"]|require\\(['\"].*anthropic-sort-stabilize['\"]\\)" src
printf '\n== Search for any direct references to the function name in non-source files ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
"stabilizeSystemAndToolOrder" .Repository: lidge-jun/opencodex Length of output: 465 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '== Imports and passthrough body flow in src/server/claude-messages.ts =='
ast-grep outline src/server/claude-messages.ts --view expanded || true
echo
echo '== Relevant slices =='
cat -n src/server/claude-messages.ts | sed -n '1,140p'
echo
cat -n src/server/claude-messages.ts | sed -n '280,380p'
echo
cat -n src/server/claude-messages.ts | sed -n '540,620p'
echo
cat -n src/server/claude-messages.ts | sed -n '840,880p'Repository: lidge-jun/opencodex Length of output: 21360 Call 🤖 Prompt for AI Agents |
||
|
|
||
| type Rec = Record<string, unknown>; | ||
|
|
||
| function isRec(v: unknown): v is Rec { | ||
| return !!v && typeof v === "object" && !Array.isArray(v); | ||
| } | ||
|
|
||
| const SKILLS_BLOCK_RE = /^([\s\S]*?\n\n)(- [\s\S]+?)(\n<\/system-reminder>\s*)$/; | ||
| const DEFERRED_TOOLS_BLOCK_RE = /^(<system-reminder>\nThe following deferred tools are now available[^\n]*\n)([\s\S]+?)(\n<\/system-reminder>\s*)$/; | ||
|
|
||
| export function isSkillsBlockText(text: unknown): text is string { | ||
| return typeof text === "string" && text.includes("User-invocable skills"); | ||
| } | ||
|
|
||
| export function isDeferredToolsBlockText(text: unknown): text is string { | ||
| return typeof text === "string" && text.includes("deferred tools are now available"); | ||
| } | ||
|
|
||
| export function sortSkillsBlockText(text: string): string { | ||
| const match = text.match(SKILLS_BLOCK_RE); | ||
| if (!match) return text; | ||
| const [, header, entriesText, footer] = match; | ||
| const entries = entriesText.split(/\n(?=- )/); | ||
| entries.sort(); | ||
| return header + entries.join("\n") + footer; | ||
| } | ||
|
|
||
| export function sortDeferredToolsBlockText(text: string): string { | ||
| const match = text.match(DEFERRED_TOOLS_BLOCK_RE); | ||
| if (!match) return text; | ||
| const [, header, toolsList, footer] = match; | ||
| const tools = toolsList.split("\n").map(t => t.trim()).filter(Boolean); | ||
| tools.sort(); | ||
| return header + tools.join("\n") + footer; | ||
| } | ||
|
|
||
| /** Normalize known system-reminder listings; leave all other instructions untouched. */ | ||
| export function normalizeSystemReminderText(text: string): string { | ||
| if (isSkillsBlockText(text)) return sortSkillsBlockText(text); | ||
| if (isDeferredToolsBlockText(text)) return sortDeferredToolsBlockText(text); | ||
| return text; | ||
| } | ||
|
|
||
| /** Sort tool definitions by name; unnamed server tools sort first. */ | ||
| export function sortToolsByName(tools: unknown[]): void { | ||
| tools.sort((a, b) => { | ||
| const nameA = isRec(a) && typeof a.name === "string" ? a.name : ""; | ||
| const nameB = isRec(b) && typeof b.name === "string" ? b.name : ""; | ||
| return nameA.localeCompare(nameB); | ||
| }); | ||
| } | ||
|
|
||
| /** Sort `body.system` skills/deferred-tools listings and `body.tools` by name, in place. */ | ||
| export function stabilizeSystemAndToolOrder(body: Rec): void { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This helper is documented as stabilizing the native Anthropic passthrough, but Useful? React with 👍 / 👎. |
||
| if (Array.isArray(body.system)) { | ||
| const system = body.system as unknown[]; | ||
| for (let i = 0; i < system.length; i++) { | ||
| const block = system[i]; | ||
| if (!isRec(block) || block.type !== "text" || typeof block.text !== "string") continue; | ||
| const normalized = normalizeSystemReminderText(block.text); | ||
| if (normalized !== block.text) system[i] = { ...block, text: normalized }; | ||
| } | ||
| } | ||
|
|
||
| if (Array.isArray(body.tools)) sortToolsByName(body.tools as unknown[]); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -660,10 +660,14 @@ function usageFromResponsesPayload(payload: unknown): OcxUsage | undefined { | |||||||||||||||||||||||||||||||||||||||||||
| const inputTokens = typeof usage.input_tokens === "number" ? usage.input_tokens : 0; | ||||||||||||||||||||||||||||||||||||||||||||
| const outputTokens = typeof usage.output_tokens === "number" ? usage.output_tokens : 0; | ||||||||||||||||||||||||||||||||||||||||||||
| if (inputTokens === 0 && outputTokens === 0) return undefined; | ||||||||||||||||||||||||||||||||||||||||||||
| const inputDetails = isPlainObject(usage.input_tokens_details) ? usage.input_tokens_details : undefined; | ||||||||||||||||||||||||||||||||||||||||||||
| const outputDetails = isPlainObject(usage.output_tokens_details) ? usage.output_tokens_details : undefined; | ||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||
| inputTokens, | ||||||||||||||||||||||||||||||||||||||||||||
| outputTokens, | ||||||||||||||||||||||||||||||||||||||||||||
| ...(typeof usage.total_tokens === "number" ? { totalTokens: usage.total_tokens } : {}), | ||||||||||||||||||||||||||||||||||||||||||||
| ...(typeof inputDetails?.cached_tokens === "number" ? { cachedInputTokens: inputDetails.cached_tokens } : {}), | ||||||||||||||||||||||||||||||||||||||||||||
| ...(typeof outputDetails?.reasoning_tokens === "number" ? { reasoningOutputTokens: outputDetails.reasoning_tokens } : {}), | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+663
to
+670
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Preserve This parser reads the same Proposed fix- ...(typeof inputDetails?.cached_tokens === "number" ? { cachedInputTokens: inputDetails.cached_tokens } : {}),
+ ...(typeof inputDetails?.cached_tokens === "number"
+ ? {
+ cachedInputTokens: inputDetails.cached_tokens,
+ cacheReadInputTokens: inputDetails.cached_tokens,
+ }
+ : {}),📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -741,7 +745,16 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig): | |||||||||||||||||||||||||||||||||||||||||||
| outBody = stripUnsupportedForwardParams(outBody); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| else outBody = stripConflictingHostedTools(outBody); | ||||||||||||||||||||||||||||||||||||||||||||
| if (forward || parsed._previousResponseInputExpanded === true) { | ||||||||||||||||||||||||||||||||||||||||||||
| // Claude Code's Messages API never sets previousResponseId at all, so on a | ||||||||||||||||||||||||||||||||||||||||||||
| // custom apiKey-auth provider (non-forward) the original forward-only guard | ||||||||||||||||||||||||||||||||||||||||||||
| // never fired, and long tool-chains hit "Invalid 'input[N].call_id': string | ||||||||||||||||||||||||||||||||||||||||||||
| // too long" (call_id replay can exceed the Responses API's 64-char limit). | ||||||||||||||||||||||||||||||||||||||||||||
| // !unexpandedMiss is a safe superset of the old condition: it still covers | ||||||||||||||||||||||||||||||||||||||||||||
| // forward mode and already-expanded replays, but also covers any request with | ||||||||||||||||||||||||||||||||||||||||||||
| // no previousResponseId at all (every Claude-surfaced request) — while still | ||||||||||||||||||||||||||||||||||||||||||||
| // excluding the one genuinely risky case, a raw unexpanded native-Codex | ||||||||||||||||||||||||||||||||||||||||||||
| // continuation that might reference call ids stored upstream. | ||||||||||||||||||||||||||||||||||||||||||||
| if (!unexpandedMiss) { | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+748
to
+757
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win Add regression coverage for the replay-repair truth table.
No companion test file is included in the supplied review context. As per path instructions, adapter behavior changes should include a focused regression test near the existing tests for that subsystem. 🤖 Prompt for AI AgentsSource: Path instructions |
||||||||||||||||||||||||||||||||||||||||||||
| outBody = repairOversizedReplayCallIds(outBody); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| outBody = stripUnsupportedReasoningSummaryDelivery(outBody, parsed.modelId); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,10 @@ | |
| * - top_k is accepted and silently dropped (no Responses equivalent, CCR parity). | ||
| */ | ||
| import type { OcxClaudeCodeConfig } from "../types"; | ||
| import { | ||
| normalizeSystemReminderText, | ||
| sortToolsByName, | ||
| } from "../adapters/anthropic-sort-stabilize"; | ||
| import { resolveAlias } from "./alias"; | ||
| import { stripOneMillionMarker } from "./context-windows"; | ||
| import { resolveDesktop3pAlias } from "./desktop-3p"; | ||
|
|
@@ -69,11 +73,12 @@ export function effortFromOutputConfig(outputConfig: unknown): string | undefine | |
| } | ||
|
|
||
| function systemToInstructions(system: unknown): string | undefined { | ||
| if (typeof system === "string") return system.length > 0 ? system : undefined; | ||
| if (typeof system === "string") return system.length > 0 ? normalizeSystemReminderText(system) : undefined; | ||
| if (Array.isArray(system)) { | ||
| const parts: string[] = []; | ||
| for (const block of system) { | ||
| if (isRec(block) && block.type === "text" && typeof block.text === "string") parts.push(block.text); | ||
| if (!isRec(block) || block.type !== "text" || typeof block.text !== "string") continue; | ||
| parts.push(normalizeSystemReminderText(block.text)); | ||
| } | ||
| return parts.length > 0 ? parts.join("\n\n") : undefined; | ||
| } | ||
|
|
@@ -229,11 +234,13 @@ function blockedSkillCallIds(messages: readonly unknown[], blocked: readonly str | |
| * `instructions` is the only shape that works on every route. | ||
| */ | ||
| function systemMessageText(content: unknown): string { | ||
| if (typeof content === "string") return content; | ||
| if (typeof content === "string") return normalizeSystemReminderText(content); | ||
| if (!Array.isArray(content)) return ""; | ||
| const parts: string[] = []; | ||
| for (const raw of content) { | ||
| if (isRec(raw) && raw.type === "text" && typeof raw.text === "string") parts.push(raw.text); | ||
| if (isRec(raw) && raw.type === "text" && typeof raw.text === "string") { | ||
| parts.push(normalizeSystemReminderText(raw.text)); | ||
| } | ||
| } | ||
| return parts.join("\n\n"); | ||
| } | ||
|
|
@@ -340,7 +347,14 @@ function toolsToResponses(tools: unknown): Rec[] | undefined { | |
| } | ||
| // Other server tools (bash_*, text_editor_*, ...) have no routed equivalent: drop. | ||
| } | ||
| return out.length > 0 ? out : undefined; | ||
| if (out.length === 0) return undefined; | ||
| // Claude Code's MCP tool discovery races non-deterministically turn to turn, so this | ||
| // array can arrive in a different order for an otherwise-identical conversation. This | ||
| // feeds both the outgoing wire body and prompt_cache_key below, so an unstable order | ||
| // busts the cache prefix and the cache-key routing together. tool_choice targets by | ||
| // name (see toolChoiceToResponses), not position, so sorting is behavior-preserving. | ||
| sortToolsByName(out); | ||
| return out; | ||
| } | ||
|
|
||
| function toolChoiceToResponses(choice: unknown, body: Rec): void { | ||
|
|
@@ -443,26 +457,30 @@ export function anthropicToResponsesTranslation(raw: unknown, cc?: OcxClaudeCode | |
| let cacheKeySource: ClaudeCacheKeySource = null; | ||
| if (isRec(raw.metadata) && typeof raw.metadata.user_id === "string") { | ||
| body.user = raw.metadata.user_id; | ||
| // OpenAI-side prompt caching is routed by prompt_cache_key (Codex clients send | ||
| // their session id; without it consecutive /v1/messages turns reported | ||
| // cached_tokens: 0 on the ChatGPT backend — devlog 090). Claude Code's | ||
| // metadata.user_id embeds the session uuid, so hashing it yields a stable | ||
| // per-session key with a bounded length/charset. | ||
| body.prompt_cache_key = createHash("sha256").update(raw.metadata.user_id).digest("hex").slice(0, 32); | ||
| cacheKeySource = "metadata"; | ||
| } else if (systemParts.length > 0) { | ||
| // Claude Desktop sends no metadata.user_id (H1, devlog 130): without any key the | ||
| // ChatGPT/OpenAI backends reported cached_tokens:0 on every turn. Fall back to a | ||
| // cache-cohort hash (devlog 260712 B4 + Pro review 012): fingerprint what the | ||
| // upstream actually receives — resolved model, post-translation system, and the | ||
| // FULL translated tool definitions in WIRE ORDER (sorting the hash while sending | ||
| // a different order would break the key↔prefix correspondence). canonical JSON | ||
| // (recursive key sort) + a version field so future normalization changes never | ||
| // mix cohorts. system-only keys herded different models/toolsets into one key | ||
| // and burned OpenAI's ~15 RPM per-key routing budget (audit R1#4/R2#5/R1#10). | ||
| // Exact-prefix matching still isolates content; the key only steers routing | ||
| // affinity. Callers must NOT synthesize a session_id header from this fallback | ||
| // (audit 133 R2#3). | ||
| } | ||
| // OpenAI-side prompt caching is routed by prompt_cache_key, not pure byte-prefix | ||
| // matching (Codex clients send their session id; without any key, consecutive | ||
| // /v1/messages turns reported cached_tokens: 0 on the ChatGPT backend). | ||
| // | ||
| // Content-first: fingerprint what the upstream actually receives — resolved model, | ||
| // post-translation system, and the FULL translated tool definitions in WIRE ORDER | ||
| // (sorting the hash while sending a different order would break the key<->prefix | ||
| // correspondence). Canonical JSON (recursive key sort) + a version field so future | ||
| // normalization changes never mix cohorts. | ||
| // | ||
| // Deliberately preferred over metadata.user_id (Claude Code's session uuid) even | ||
| // when present: a session-scoped key means every NEW Claude Code session gets a | ||
| // fresh key, so a cache warmed by yesterday's session is unreachable today even for | ||
| // byte-identical system+tools content — this is a guaranteed cold rewrite on the | ||
| // first call of every session, not just cross-session drift. Content-based keying | ||
| // lets any session hit a cache any other session already warmed. Session-only keys | ||
| // were originally chosen because pure-content keying herds many different | ||
| // sessions/models/toolsets onto one key, burning OpenAI's per-key routing budget at | ||
| // multi-tenant scale; that risk is far smaller for a single-operator deployment | ||
| // where the actual pain is inter-session cache misses. Exact-prefix matching still | ||
| // isolates content; the key only steers routing affinity. Callers must NOT | ||
| // synthesize a session_id header from this fallback. | ||
| if (systemParts.length > 0) { | ||
| body.prompt_cache_key = createHash("sha256") | ||
| .update(canonicalJson({ | ||
| version: 2, | ||
|
Comment on lines
485
to
486
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a Claude request with system content is routed to provider Useful? React with 👍 / 👎. |
||
|
|
@@ -472,6 +490,11 @@ export function anthropicToResponsesTranslation(raw: unknown, cc?: OcxClaudeCode | |
| })) | ||
| .digest("hex").slice(0, 32); | ||
| cacheKeySource = "system"; | ||
| } else if (isRec(raw.metadata) && typeof raw.metadata.user_id === "string") { | ||
| // No system content to fingerprint: fall back to the session-scoped key so the | ||
| // request still gets SOME key rather than none. | ||
| body.prompt_cache_key = createHash("sha256").update(raw.metadata.user_id).digest("hex").slice(0, 32); | ||
| cacheKeySource = "metadata"; | ||
| } | ||
|
|
||
| const thinking = raw.thinking; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -387,6 +387,7 @@ async function handleEnsure() { | |
|
|
||
| /** Fixed tray action: start the proxy without depending on codexAutoStart. */ | ||
| async function handleTrayProxyStart(): Promise<void> { | ||
| const config = loadConfig(); | ||
| const ok = await runTrayProxyStart({ | ||
| findLive: findLiveProxy, | ||
| diagnoseService: () => { | ||
|
|
@@ -395,7 +396,6 @@ async function handleTrayProxyStart(): Promise<void> { | |
| }, | ||
| startService: () => serviceCommand("start"), | ||
| startDirect: () => { | ||
| const config = loadConfig(); | ||
| const port = (config.port ?? 10100) > 0 ? (config.port ?? 10100) : 10100; | ||
| const child = spawn(process.execPath, startArgv(port), { | ||
| detached: true, | ||
|
|
@@ -406,6 +406,19 @@ async function handleTrayProxyStart(): Promise<void> { | |
| child.unref(); | ||
| }, | ||
| waitForProxy, | ||
| onStarted: async port => { | ||
| await syncModelsToCodex(port).catch(e => { | ||
| console.error(`⚠️ Model sync skipped: ${e instanceof Error ? e.message : String(e)}`); | ||
| }); | ||
| try { | ||
| const { syncGrokConfig } = await import("../grok/sync"); | ||
| const g = await syncGrokConfig(port, config, config.hostname ? { hostname: config.hostname } : {}); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In the already-running tray start path, Useful? React with 👍 / 👎. |
||
| if (g.changed) console.log(" + Grok Build config updated (~/.grok/config.toml)"); | ||
| else if (!g.ok) console.error(`⚠️ ${g.message}`); | ||
| } catch (err) { | ||
| console.error(`⚠️ ${grokSyncFailureMessage(err)}`); | ||
| } | ||
| }, | ||
| info: message => console.log(message), | ||
| error: message => console.error(message), | ||
| }); | ||
|
|
@@ -887,7 +900,13 @@ switch (command) { | |
| case "restart": { | ||
| // A failed stop must not be followed by a re-inject: with a foreign service still running | ||
| // (ownership mismatch) we would rewrite shared config we just declined to touch. | ||
| if (await handleStop()) await handleEnsure(); | ||
| // | ||
| // handleEnsure() early-returns without relaunching when codexAutoStart is disabled — | ||
| // that flag governs whether opencodex registers itself with the Codex CLI on boot, an | ||
| // unrelated concern, but it silently ate `restart` too: stop succeeded, nothing ever | ||
| // came back up. handleTrayProxyStart() (already used by the tray's own restart action) | ||
| // relaunches the proxy unconditionally, independent of codexAutoStart. | ||
| if (await handleStop()) await handleTrayProxyStart(); | ||
| else console.error("↩️ Restart aborted: the proxy was not stopped cleanly."); | ||
| break; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,13 +14,22 @@ export interface TrayProxyStartIo { | |
| waitForProxy: () => Promise<TrayProxyLive | null>; | ||
| info: (message: string) => void; | ||
| error: (message: string) => void; | ||
| /** | ||
| * Called once a live proxy is confirmed on `port` — whether it was already running or | ||
| * this call just started it. `handleEnsure` performs the Grok-fence and Codex-model | ||
| * sync inline in both of its branches; this action skipped both entirely, so `ocx restart` | ||
| * silently lost them once restart was repointed at this action. Optional so tests that | ||
| * don't care about the sync can omit it. | ||
| */ | ||
| onStarted?: (port: number) => void | Promise<void>; | ||
|
Comment on lines
+17
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Preserve the live hostname in the post-start callback.
Also applies to: 32-32, 52-52 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** Side-effect coordinator for the tray's fixed proxy-start action. */ | ||
| export async function runTrayProxyStart(io: TrayProxyStartIo): Promise<boolean> { | ||
| const live = await io.findLive(); | ||
| if (live) { | ||
| io.info(`Proxy already running on port ${live.port}.`); | ||
| await io.onStarted?.(live.port); | ||
| return true; | ||
| } | ||
|
|
||
|
|
@@ -40,6 +49,7 @@ export async function runTrayProxyStart(io: TrayProxyStartIo): Promise<boolean> | |
| return false; | ||
| } | ||
| io.info(`Proxy running on port ${started.port}.`); | ||
| await io.onStarted?.(started.port); | ||
| return true; | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Doc omits the
metadata.user_idfallback forprompt_cache_key.This section states the key is unconditionally "content-scoped," but
anthropicToResponsesTranslation(src/claude/inbound.ts) only does that whensystemParts.length > 0; with no system content it falls back to hashingmetadata.user_id(cacheKeySource = "metadata"). Worth a one-line caveat so operators reading this don't assume every request gets a content-scoped key.Additionally, please confirm the
ja/ko/ru/zh-cntranslations of this guide are updated to match — they aren't included in this diff.📝 Suggested doc tweak
As per path instructions, "translated locale pages (ja, ko, ru, zh-cn) are not left contradicting the English source."
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Path instructions