From 343e5d7a37aceca2917d1d01644b785a1bd2d841 Mon Sep 17 00:00:00 2001 From: chilung Date: Sat, 15 Aug 2026 13:41:21 +0800 Subject: [PATCH 1/8] fix(google): allow direct Gemini wire rename opt-out Keep the existing -tiered mapping by default while allowing AI Studio providers that still serve bare Gemini Flash ids to opt out. Cover both Flash generations and document the provider setting. --- .../docs/reference/configuration/providers.md | 1 + src/adapters/google.ts | 14 +++++----- src/config.ts | 1 + src/types.ts | 6 +++++ tests/google-adapter.test.ts | 26 +++++++++++++++++++ 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 2743d81961..e522fe2a4e 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -122,6 +122,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids. | `escapeBuiltinToolNames?` | `boolean` | Escape built-in tool names for Anthropic-compatible gateways and restore them in returned calls. | | `anthropicEofTolerance?` | `boolean` | Let an Anthropic-compatible gateway complete a stream that ends before `message_stop`, only when visible text or a complete JSON-object tool input was received. Off by default. | | `googleMode?` | `"ai-studio" \| "vertex" \| "cloud-code-assist"` | Google transport/auth mode. Default `ai-studio`. | +| `directGeminiWireRenames?` | `boolean` | Google only. When `false`, the AI Studio (direct) path sends Gemini Flash ids to the wire unchanged instead of applying the `-tiered` suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Defaults to the rename; set `false` when the configured upstream still serves the bare ids. | | `project?` | `string` | Vertex or Antigravity Cloud Code Assist project id. | | `location?` | `string` | Vertex location; environment fallback is `GOOGLE_CLOUD_LOCATION`. | | `mcpServers?` | `Record` | Cursor only: stdio or Streamable HTTP MCP servers. | diff --git a/src/adapters/google.ts b/src/adapters/google.ts index ab27089e7d..d19edc1291 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -48,19 +48,17 @@ const GOOGLE_BREVITY_INSTRUCTION = [ ].join("\n"); /** - * Google renamed the current Gemini Flash generations on the Generative Language API, - * appending a `-tiered` suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). The - * old `gemini-3.7-flash` path 404s, so a saved config or registry entry naming the base - * id must be resolved here before it reaches the URL. The user-facing id is deliberately - * left alone: the picker, the catalog, the usage log and the price overlays all stay - * keyed on the base id, and only the wire path learns the new spelling. + * Some Google direct deployments expose current Gemini Flash generations with a `-tiered` + * wire suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Keep the picker-visible id + * stable and make the mapping configurable for deployments that still serve the bare id. */ const GEMINI_DIRECT_WIRE_RENAMES: Record = { "gemini-3.7-flash": "gemini-3.7-flash-tiered", "gemini-3.6-flash": "gemini-3.6-flash-tiered", }; -function resolveDirectGeminiWireModelId(modelId: string): string { +function resolveDirectGeminiWireModelId(modelId: string, applyRenames: boolean): string { + if (!applyRenames) return modelId; return Object.hasOwn(GEMINI_DIRECT_WIRE_RENAMES, modelId) ? GEMINI_DIRECT_WIRE_RENAMES[modelId]! : modelId; @@ -396,7 +394,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte parsed.modelId, mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning), ).wireModelId - : resolveDirectGeminiWireModelId(parsed.modelId); + : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false); const { systemInstruction, contents } = messagesToGeminiFormat(parsed, routedModelId); const tools = toolsToGeminiFormat(parsed); diff --git a/src/config.ts b/src/config.ts index d4c0a3a0f3..d879195e88 100644 --- a/src/config.ts +++ b/src/config.ts @@ -741,6 +741,7 @@ const providerConfigSchema = z.object({ upstreamHttpVersion: z.enum(UPSTREAM_HTTP_VERSION_VALUES) .nullish() .transform(value => value ?? undefined), + directGeminiWireRenames: z.boolean().optional(), noStructuredOutputModels: z.array(z.string().min(1)) .transform(normalizeNonBlankStringArray) .optional(), diff --git a/src/types.ts b/src/types.ts index 565118b287..ea0f526e2a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1394,6 +1394,12 @@ export interface OcxProviderConfig { * (current behavior unchanged). Only meaningful for https: base URLs. */ upstreamHttpVersion?: UpstreamHttpVersion; + * Google only. When `false`, the AI Studio (direct) path sends Gemini Flash ids + * unchanged to the wire instead of applying the `-tiered` suffix (`gemini-3.7-flash` + * -> `gemini-3.7-flash-tiered`). Set this to `false` when the configured upstream still + * serves the bare ids. Absent (default) keeps the rename. + */ + directGeminiWireRenames?: boolean; /** Keep provider settings on disk but exclude it from routing and model/catalog listings. */ disabled?: boolean; /** diff --git a/tests/google-adapter.test.ts b/tests/google-adapter.test.ts index 467cf19c37..076f746b57 100644 --- a/tests/google-adapter.test.ts +++ b/tests/google-adapter.test.ts @@ -278,3 +278,29 @@ describe("google adapter — tool_choice on the wire", () => { }); }); }); + +describe("google adapter — direct -tiered wire renames", () => { + function renamedParsed(modelId: string): OcxParsedRequest { + return { + modelId, + stream: false, + options: {}, + context: { messages: [{ role: "user", content: "hi" }], tools: [] }, + } as unknown as OcxParsedRequest; + } + + test("default maps the picker id to the -tiered wire id", async () => { + for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { + const { url } = await createGoogleAdapter(provider).buildRequest(renamedParsed(modelId)); + expect(url).toContain(`/v1beta/models/${modelId}-tiered:generateContent`); + } + }); + + test("directGeminiWireRenames: false keeps the bare wire id", async () => { + const adapter = createGoogleAdapter({ ...provider, directGeminiWireRenames: false }); + for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { + const { url } = await adapter.buildRequest(renamedParsed(modelId)); + expect(url).toContain(`/v1beta/models/${modelId}:generateContent`); + } + }); +}); From d62cc402992df3f6bb6c058dc924f4eca8215bc3 Mon Sep 17 00:00:00 2001 From: chilung Date: Sat, 15 Aug 2026 14:33:22 +0800 Subject: [PATCH 2/8] fix(google): scope direct Gemini rename opt-out --- src/adapters/google.ts | 4 +++- tests/config.test.ts | 32 ++++++++++++++++++++++++++++++++ tests/google-adapter.test.ts | 12 ++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/adapters/google.ts b/src/adapters/google.ts index d19edc1291..ccd5ba9579 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -394,7 +394,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte parsed.modelId, mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning), ).wireModelId - : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false); + : provider.googleMode === "vertex" + ? parsed.modelId + : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false); const { systemInstruction, contents } = messagesToGeminiFormat(parsed, routedModelId); const tools = toolsToGeminiFormat(parsed); diff --git a/tests/config.test.ts b/tests/config.test.ts index b949b9bd8b..c67ff8846d 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -814,6 +814,38 @@ describe("opencodex config defaults", () => { expect(readConfigDiagnostics().error).toContain("responsesSnapshotRepair"); }); + test("direct Gemini wire rename opt-out is a boolean and round-trips", () => { + const base = { + port: 12345, + providers: { + google: { + adapter: "google", + baseUrl: "https://generativelanguage.googleapis.com", + }, + }, + defaultProvider: "google", + }; + writeConfig({ + ...base, + providers: { + google: { ...base.providers.google, directGeminiWireRenames: false }, + }, + }); + const config = loadConfig(); + expect(config.providers.google.directGeminiWireRenames).toBe(false); + saveConfig(config); + expect(loadConfig().providers.google.directGeminiWireRenames).toBe(false); + + writeConfig({ + ...base, + providers: { + google: { ...base.providers.google, directGeminiWireRenames: "false" }, + }, + }); + expect(readConfigDiagnostics().source).toBe("fallback"); + expect(readConfigDiagnostics().error).toContain("directGeminiWireRenames"); + }); + test("accepts a relative responsesPath", () => { writeResponsesPathConfig("/responses"); diff --git a/tests/google-adapter.test.ts b/tests/google-adapter.test.ts index 076f746b57..e24b113ec6 100644 --- a/tests/google-adapter.test.ts +++ b/tests/google-adapter.test.ts @@ -303,4 +303,16 @@ describe("google adapter — direct -tiered wire renames", () => { expect(url).toContain(`/v1beta/models/${modelId}:generateContent`); } }); + + test("directGeminiWireRenames does not affect Vertex requests", async () => { + const vertexProvider = { ...provider, googleMode: "vertex" as const }; + for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { + const parsed = renamedParsed(modelId); + const defaultRequest = await createGoogleAdapter(vertexProvider).buildRequest(parsed); + const optOutRequest = await createGoogleAdapter({ ...vertexProvider, directGeminiWireRenames: false }) + .buildRequest(parsed); + expect(optOutRequest.url).toBe(defaultRequest.url); + expect(optOutRequest.body).toBe(defaultRequest.body); + } + }); }); From 812255d3a2f3efe40033b8ed62ed494d442fe573 Mon Sep 17 00:00:00 2001 From: chilung Date: Sat, 15 Aug 2026 22:10:56 +0800 Subject: [PATCH 3/8] fix(google): use public model id in system identity; cover rename opt-out - identifyRoutedModel now names parsed.modelId so a -tiered wire rename never leaks into the system prompt identity (CodeRabbit finding) - document that directGeminiWireRenames affects only AI Studio direct requests; Vertex and Cloud Code Assist are unchanged - add adapter tests for explicit true and Cloud Code Assist opt-out --- .../docs/reference/configuration/providers.md | 2 +- src/adapters/google.ts | 11 ++++---- tests/google-adapter.test.ts | 28 +++++++++++++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index e522fe2a4e..7cf4710497 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -122,7 +122,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids. | `escapeBuiltinToolNames?` | `boolean` | Escape built-in tool names for Anthropic-compatible gateways and restore them in returned calls. | | `anthropicEofTolerance?` | `boolean` | Let an Anthropic-compatible gateway complete a stream that ends before `message_stop`, only when visible text or a complete JSON-object tool input was received. Off by default. | | `googleMode?` | `"ai-studio" \| "vertex" \| "cloud-code-assist"` | Google transport/auth mode. Default `ai-studio`. | -| `directGeminiWireRenames?` | `boolean` | Google only. When `false`, the AI Studio (direct) path sends Gemini Flash ids to the wire unchanged instead of applying the `-tiered` suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Defaults to the rename; set `false` when the configured upstream still serves the bare ids. | +| `directGeminiWireRenames?` | `boolean` | Google only. This affects only direct AI Studio requests. When `false`, the AI Studio path sends Gemini Flash ids to the wire unchanged instead of applying the `-tiered` suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Vertex preserves the requested model ID, and Cloud Code Assist routing is unchanged. Defaults to the rename; set `false` when the configured upstream still serves the bare ids. | | `project?` | `string` | Vertex or Antigravity Cloud Code Assist project id. | | `location?` | `string` | Vertex location; environment fallback is `GOOGLE_CLOUD_LOCATION`. | | `mcpServers?` | `Record` | Cursor only: stdio or Streamable HTTP MCP servers. | diff --git a/src/adapters/google.ts b/src/adapters/google.ts index ccd5ba9579..0b7df429ec 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -143,10 +143,7 @@ function geminiToolResultText(content: string | OcxContentPart[]): string { return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER; } -function messagesToGeminiFormat( - parsed: OcxParsedRequest, - routedModelId = parsed.modelId, -): { systemInstruction?: unknown; contents: unknown[] } { +function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: unknown; contents: unknown[] } { // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream. const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice); @@ -154,7 +151,7 @@ function messagesToGeminiFormat( ...(parsed.context.systemPrompt ?? []), ...(toolCatalogNudge ? [toolCatalogNudge] : []), GOOGLE_BREVITY_INSTRUCTION, - ].join("\n\n"), routedModelId); + ].join("\n\n"), parsed.modelId); const systemInstruction = { parts: [{ text: systemText }] }; const contents: unknown[] = []; @@ -397,7 +394,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte : provider.googleMode === "vertex" ? parsed.modelId : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false); - const { systemInstruction, contents } = messagesToGeminiFormat(parsed, routedModelId); + // System identity names the public model id (parsed.modelId) so the model never + // self-reports as its -tiered wire spelling; routedModelId is only the wire id in the URL. + const { systemInstruction, contents } = messagesToGeminiFormat(parsed); const tools = toolsToGeminiFormat(parsed); const body: Record = { contents }; diff --git a/tests/google-adapter.test.ts b/tests/google-adapter.test.ts index e24b113ec6..a7e64bc8e6 100644 --- a/tests/google-adapter.test.ts +++ b/tests/google-adapter.test.ts @@ -304,6 +304,34 @@ describe("google adapter — direct -tiered wire renames", () => { } }); + test("directGeminiWireRenames: true still maps to the -tiered wire id", async () => { + const adapter = createGoogleAdapter({ ...provider, directGeminiWireRenames: true }); + for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { + const { url } = await adapter.buildRequest(renamedParsed(modelId)); + expect(url).toContain(`/v1beta/models/${modelId}-tiered:generateContent`); + } + }); + + test("directGeminiWireRenames does not affect Cloud Code Assist requests", async () => { + const ccaProvider = { + ...provider, + googleMode: "cloud-code-assist", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + project: "proj-123", + } as const; + for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { + const parsed = renamedParsed(modelId); + const defaultRequest = await createGoogleAdapter(ccaProvider).buildRequest(parsed); + const optOutRequest = await createGoogleAdapter({ ...ccaProvider, directGeminiWireRenames: false }) + .buildRequest(parsed); + expect(optOutRequest.url).toBe(defaultRequest.url); + // The envelope's requestId/sessionId are minted per request; compare the wire model only. + const defaultModel = JSON.parse(defaultRequest.body).model as string; + const optOutModel = JSON.parse(optOutRequest.body).model as string; + expect(optOutModel).toBe(defaultModel); + } + }); + test("directGeminiWireRenames does not affect Vertex requests", async () => { const vertexProvider = { ...provider, googleMode: "vertex" as const }; for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { From 8ae045144772054044cf286d8d2dc6e73569c256 Mon Sep 17 00:00:00 2001 From: chilung Date: Sun, 16 Aug 2026 10:29:57 +0800 Subject: [PATCH 4/8] test(google): complete Gemini rename review coverage --- .../docs/reference/configuration/providers.md | 2 +- tests/google-adapter.test.ts | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 7cf4710497..2ba92c2b26 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -122,7 +122,7 @@ differing backup and rewrites known legacy namespaced selected ids to bare ids. | `escapeBuiltinToolNames?` | `boolean` | Escape built-in tool names for Anthropic-compatible gateways and restore them in returned calls. | | `anthropicEofTolerance?` | `boolean` | Let an Anthropic-compatible gateway complete a stream that ends before `message_stop`, only when visible text or a complete JSON-object tool input was received. Off by default. | | `googleMode?` | `"ai-studio" \| "vertex" \| "cloud-code-assist"` | Google transport/auth mode. Default `ai-studio`. | -| `directGeminiWireRenames?` | `boolean` | Google only. This affects only direct AI Studio requests. When `false`, the AI Studio path sends Gemini Flash ids to the wire unchanged instead of applying the `-tiered` suffix (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`). Vertex preserves the requested model ID, and Cloud Code Assist routing is unchanged. Defaults to the rename; set `false` when the configured upstream still serves the bare ids. | +| `directGeminiWireRenames?` | `boolean` | Google only. Applies only to direct AI Studio requests. Omitted or `true` keeps the `-tiered` wire rename for Gemini Flash ids (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`); `false` sends the requested bare ids to the wire unchanged. Vertex preserves the requested model ID, and Cloud Code Assist routing is unchanged. Set `false` when the configured upstream still serves the bare ids. | | `project?` | `string` | Vertex or Antigravity Cloud Code Assist project id. | | `location?` | `string` | Vertex location; environment fallback is `GOOGLE_CLOUD_LOCATION`. | | `mcpServers?` | `Record` | Cursor only: stdio or Streamable HTTP MCP servers. | diff --git a/tests/google-adapter.test.ts b/tests/google-adapter.test.ts index a7e64bc8e6..59a3a19bb7 100644 --- a/tests/google-adapter.test.ts +++ b/tests/google-adapter.test.ts @@ -289,6 +289,19 @@ describe("google adapter — direct -tiered wire renames", () => { } as unknown as OcxParsedRequest; } + function identityParsed(modelId: string): OcxParsedRequest { + return { + modelId, + stream: false, + options: {}, + context: { + systemPrompt: ["You are Codex, a coding agent based on GPT-5."], + messages: [{ role: "user", content: "hi" }], + tools: [], + }, + } as unknown as OcxParsedRequest; + } + test("default maps the picker id to the -tiered wire id", async () => { for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { const { url } = await createGoogleAdapter(provider).buildRequest(renamedParsed(modelId)); @@ -307,8 +320,15 @@ describe("google adapter — direct -tiered wire renames", () => { test("directGeminiWireRenames: true still maps to the -tiered wire id", async () => { const adapter = createGoogleAdapter({ ...provider, directGeminiWireRenames: true }); for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { - const { url } = await adapter.buildRequest(renamedParsed(modelId)); + const request = await adapter.buildRequest(identityParsed(modelId)); + const { url } = request; expect(url).toContain(`/v1beta/models/${modelId}-tiered:generateContent`); + const body = JSON.parse(request.body) as { + systemInstruction?: { parts?: Array<{ text?: string }> }; + }; + const systemText = body.systemInstruction?.parts?.[0]?.text ?? ""; + expect(systemText).toContain(`powered by the ${modelId}`); + expect(systemText).not.toContain(`${modelId}-tiered`); } }); From f6c88febfa8faedc6a52ef2fed69fa0175369537 Mon Sep 17 00:00:00 2001 From: chilung Date: Sun, 16 Aug 2026 12:31:07 +0800 Subject: [PATCH 5/8] fix(google): preserve routed identity for CCA aliases --- src/adapters/google.ts | 13 ++++++++----- tests/google-adapter.test.ts | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 0b7df429ec..72914d9559 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -143,7 +143,10 @@ function geminiToolResultText(content: string | OcxContentPart[]): string { return hasContent ? contentPartsToText(content) : GEMINI_EMPTY_TOOL_OUTPUT_PLACEHOLDER; } -function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: unknown; contents: unknown[] } { +function messagesToGeminiFormat( + parsed: OcxParsedRequest, + identityModelId: string, +): { systemInstruction?: unknown; contents: unknown[] } { // Neutralize Codex's GPT-5 identity line (Gemini/Antigravity share this path) so a routed model // never misreports as GPT-5/OpenAI, and never leaks the proxy identity upstream. const toolCatalogNudge = buildNonOpenAIToolCatalogNudgeForTools(parsed.context.tools, parsed.options.toolChoice); @@ -151,7 +154,7 @@ function messagesToGeminiFormat(parsed: OcxParsedRequest): { systemInstruction?: ...(parsed.context.systemPrompt ?? []), ...(toolCatalogNudge ? [toolCatalogNudge] : []), GOOGLE_BREVITY_INSTRUCTION, - ].join("\n\n"), parsed.modelId); + ].join("\n\n"), identityModelId); const systemInstruction = { parts: [{ text: systemText }] }; const contents: unknown[] = []; @@ -394,9 +397,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte : provider.googleMode === "vertex" ? parsed.modelId : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false); - // System identity names the public model id (parsed.modelId) so the model never - // self-reports as its -tiered wire spelling; routedModelId is only the wire id in the URL. - const { systemInstruction, contents } = messagesToGeminiFormat(parsed); + // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation. + const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId; + const { systemInstruction, contents } = messagesToGeminiFormat(parsed, identityModelId); const tools = toolsToGeminiFormat(parsed); const body: Record = { contents }; diff --git a/tests/google-adapter.test.ts b/tests/google-adapter.test.ts index 59a3a19bb7..546ad41d6c 100644 --- a/tests/google-adapter.test.ts +++ b/tests/google-adapter.test.ts @@ -352,6 +352,25 @@ describe("google adapter — direct -tiered wire renames", () => { } }); + test("Cloud Code Assist identity follows a migrated wire model", async () => { + const ccaProvider = { + ...provider, + googleMode: "cloud-code-assist", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + project: "proj-123", + } as const; + const request = await createGoogleAdapter(ccaProvider).buildRequest(identityParsed("gemini-3.6-flash")); + const envelope = JSON.parse(request.body) as { + model: string; + request: { systemInstruction?: { parts?: Array<{ text?: string }> } }; + }; + const systemText = envelope.request.systemInstruction?.parts?.[0]?.text ?? ""; + + expect(envelope.model).toBe("gemini-3.7-flash-tiered"); + expect(systemText).toContain("powered by the gemini-3.7-flash-tiered"); + expect(systemText).not.toContain("powered by the gemini-3.6-flash."); + }); + test("directGeminiWireRenames does not affect Vertex requests", async () => { const vertexProvider = { ...provider, googleMode: "vertex" as const }; for (const modelId of ["gemini-3.7-flash", "gemini-3.6-flash"]) { From e1c7ec85eeb98cc3de5930bc026ff36400e9c572 Mon Sep 17 00:00:00 2001 From: chilung Date: Mon, 17 Aug 2026 00:17:36 +0800 Subject: [PATCH 6/8] fix(google): retain provider option documentation after rebase --- src/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index ea0f526e2a..5564c514c4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1392,8 +1392,9 @@ export interface OcxProviderConfig { * HTTP/2 streaming responses (issue #1668). "http1.1" / "h1" forces HTTP/1.1, * "http2" / "h2" forces HTTP/2. Absent or "auto" keeps Bun's default negotiation * (current behavior unchanged). Only meaningful for https: base URLs. - */ + */ upstreamHttpVersion?: UpstreamHttpVersion; + /** * Google only. When `false`, the AI Studio (direct) path sends Gemini Flash ids * unchanged to the wire instead of applying the `-tiered` suffix (`gemini-3.7-flash` * -> `gemini-3.7-flash-tiered`). Set this to `false` when the configured upstream still From 9df45a977df413932881b8d64fda564d71c0d60e Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Mon, 17 Aug 2026 20:14:45 +0900 Subject: [PATCH 7/8] docs(devlog): plan the Wave 5A-5D + Wave 6 execution campaign Docs-only roadmap unit for the post-#1881 wave campaign, written against the verified Gate 0 baseline (dev 1208bd25c; #1881 and #1909 both ancestors). The unit carries two rounds of independent audit. Round 1 returned FAIL with nine blockers and all nine were folded in; the most consequential correction removed the campaign's only new production mechanism. The external audit that seeded this campaign asked for the direct-Google and Antigravity wire-id tables to be split apart for #1894. They are already separate - src/adapters/google.ts owns GEMINI_DIRECT_WIRE_RENAMES, and src/providers/antigravity-models.ts owns GEMINI_FLASH_WIRE_ID, with the resolver already chosen per googleMode. The real defect is that the direct rename is unconditional while the -tiered spelling is deployment-specific: a70bb78d4 and #1894 carry contradictory live captures from the same week, and both are credible. The first plan answered that with a 404-triggered retry onto the alternate spelling. The audit killed it: AI Studio installs no fetchResponse, so the adapter never sees the 404, and the only hosts are the core pre-stream recovery loop or the mid-stream terminal guard - the latter would splice two upstream turns into one client stream. WP1 is now #1739 alone, and the durable answer (resolve the spelling from /v1beta/models, which the tree already queries) is deferred to its own cycle rather than ridden in. Three further work-phases shrank once the tree was read rather than assumed: WP2 drops to one file, because #1881 already landed two of #1899's three and #1899 is CONFLICTING as a result; WP3 drops to a single -ErrorAction Stop, because the sentinel and unknown state it proposed already exist; WP4 keeps its key-completeness finding, which is real, but gains the constraint that the sibling cache's identities are process-local HMACs, so copying them into a durable key would silently break restart replay instead of fixing scope. Merge orders are corrected too: 5D leads with #1891 rather than the only red-CI PR, 5C names live-transport.ts as a four-way conflict surface with a rebase step per merge, and merge order is verified with rev-list --topo-order rather than --is-ancestor, which cannot observe order at all. --- .../260817_wave5_execution/000_research.md | 62 +++++++++++ .../001_audit_synthesis.md | 52 +++++++++ .../002_merge_order_corrections.md | 61 ++++++++++ .../010_1894_gemini_wire_id.md | 90 +++++++++++++++ .../020_1899_harden_ordering.md | 46 ++++++++ .../030_1876_windows_discovery.md | 77 +++++++++++++ .../040_thought_signature_scope.md | 105 ++++++++++++++++++ .../050_1849_1049_durability.md | 66 +++++++++++ .../060_wave5b_continuation.md | 51 +++++++++ .../070_wave5c_cursor.md | 51 +++++++++ .../080_wave5d_antigravity.md | 43 +++++++ .../090_wave6_closeout.md | 43 +++++++ 12 files changed, 747 insertions(+) create mode 100644 devlog/_plan/260817_wave5_execution/000_research.md create mode 100644 devlog/_plan/260817_wave5_execution/001_audit_synthesis.md create mode 100644 devlog/_plan/260817_wave5_execution/002_merge_order_corrections.md create mode 100644 devlog/_plan/260817_wave5_execution/010_1894_gemini_wire_id.md create mode 100644 devlog/_plan/260817_wave5_execution/020_1899_harden_ordering.md create mode 100644 devlog/_plan/260817_wave5_execution/030_1876_windows_discovery.md create mode 100644 devlog/_plan/260817_wave5_execution/040_thought_signature_scope.md create mode 100644 devlog/_plan/260817_wave5_execution/050_1849_1049_durability.md create mode 100644 devlog/_plan/260817_wave5_execution/060_wave5b_continuation.md create mode 100644 devlog/_plan/260817_wave5_execution/070_wave5c_cursor.md create mode 100644 devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md create mode 100644 devlog/_plan/260817_wave5_execution/090_wave6_closeout.md diff --git a/devlog/_plan/260817_wave5_execution/000_research.md b/devlog/_plan/260817_wave5_execution/000_research.md new file mode 100644 index 0000000000..8adfcc0a39 --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/000_research.md @@ -0,0 +1,62 @@ +# 260817 Wave 5 execution — research and Gate 0 baseline + +Campaign: execute the Wave 5A/5B/5C/5D + Wave 6 roadmap produced by the +2026-08-17 external audit, landing each item on `dev`, then promoting to +`preview` and `main`. + +## Gate 0 baseline (verified 2026-08-17) + +| Fact | Value | Evidence | +|------|-------|----------| +| local `dev` head | `1208bd25c` | `git rev-parse HEAD` after `--ff-only origin/dev` (44 commits fast-forwarded) | +| #1881 merge ancestry | present | `git merge-base --is-ancestor bb984ad47 HEAD` → exit 0 | +| #1909 ancestry | present | `b1708acc4 Merge pull request #1909` reachable | +| stable release | v2.24.2 | `origin/main` = `474584bcd Merge pull request #1914 from lidge-jun/release-2.24.2` | +| working tree | clean | `git status --porcelain` empty | + +## Roadmap PR inventory at exact head + +| PR | head | draft | review | checks | Wave | +|----|------|-------|--------|--------|------| +| #1899 | `8ab0aa8d0` | no | REVIEW_REQUIRED | 15 success | 5A | +| #1876 | `d5acd7414` | no | CHANGES_REQUESTED | 22 success, 1 skipped | 5A | +| #1888 | `cd3367193` | no | CHANGES_REQUESTED | 10 success | 5B | +| #1902 | `b8983c912` | no | REVIEW_REQUIRED | 10 success | 5B | +| #1884 | `99b0bbc38` | no | REVIEW_REQUIRED | 25 success | 5B | +| #1892 | `6b17d6233` | no | REVIEW_REQUIRED | 9 success | 5B | +| #1904 | `c603dcd83` | yes | REVIEW_REQUIRED | 6 success, 1 cancelled | 5B | +| #1898 | `7279aca7c` | yes | REVIEW_REQUIRED | 21 success, 1 cancelled | 5B | +| #1900 | `1824a0148` | no | REVIEW_REQUIRED | 10 success | 5C | +| #1895 | `8a4040384` | yes | CHANGES_REQUESTED | 12 success | 5C | +| #1887 | `ed4e87753` | yes | REVIEW_REQUIRED | 4 success | 5C | +| #1896 | `5d2aec482` | yes | REVIEW_REQUIRED | 7 success | 5C | +| #1903 | `54893ca6e` | no | REVIEW_REQUIRED | 10 success | 5C | +| #1889 | `ea64418a3` | yes | REVIEW_REQUIRED | **5 failure**, 6 success | 5D | +| #1891 | `10b88e155` | no | REVIEW_REQUIRED | 14 success | 5D | +| #1897 | `38c25aed8` | no | REVIEW_REQUIRED | 24 success, 1 cancelled | 5D | + +## Correction to the external audit (P-phase finding) + +The audit's #1894 remedy — "split direct Google mapping from Antigravity CCA +mapping" — describes a separation the tree **already has**: + +- `src/adapters/google.ts` owns `GEMINI_DIRECT_WIRE_RENAMES` / + `resolveDirectGeminiWireModelId()` for the direct AI Studio path. +- `src/providers/antigravity-models.ts` owns `GEMINI_FLASH_WIRE_ID` / + `ANTIGRAVITY_MODEL_ALIASES` for the CCA path. +- `src/adapters/google.ts:395-399` already branches on provider family before + choosing a resolver. + +So the real defect is not a shared alias table. It is that +`GEMINI_DIRECT_WIRE_RENAMES` is an **unconditional** rename applied to every +direct Google deployment, while the rename is only true for some of them. +Commit `a70bb78d4` added it from a live capture where bare ids 404'd and +`-tiered` returned 200; #1894 reports the exact opposite from another account +on the same day. Both reporters are credible and neither is universal. + +PR #1739 already implements the correct shape: a provider-level +`directGeminiWireRenames` boolean, defaulting to today's behavior. That makes +the deployment difference configurable but still ships a default that 404s for +the #1894 reporter. + +Wave-5 decision: the default must stop guessing. See `010`. diff --git a/devlog/_plan/260817_wave5_execution/001_audit_synthesis.md b/devlog/_plan/260817_wave5_execution/001_audit_synthesis.md new file mode 100644 index 0000000000..1329ba64dc --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/001_audit_synthesis.md @@ -0,0 +1,52 @@ +# A-phase synthesis — round 1 (VERDICT: FAIL, 9 blockers) + +An independent reviewer audited `000`-`090` against the tree at `1208bd25c` +and live GitHub state. Verdict FAIL. Every blocker was re-verified by the main +agent before disposition; all nine are accepted. + +## Accepted blockers and their amendments + +| # | Blocker | Disposition | +|---|---------|-------------| +| 1 | WP1's 404 retry has no host: AI Studio installs no `fetchResponse` (`google.ts:384`), so the adapter never sees a `Response`; the fetch is core-owned (`core.ts:3586`) | **Folded** — retry dropped entirely | +| 2 | The retry is only safe in the pre-stream `recovery:` loop (`core.ts:3696`); the terminal-guard continuation (`core.ts:3960`) refetches mid-stream and would splice two upstream turns into one client stream | **Folded** — dropped; discovery replaces it | +| 3 | Memo key `(provider, model)` is too coarse and could silently override an explicit operator `false` | **Folded** — identity-keyed, and config always wins | +| 4 | #1739 is mis-scoped: real head `e1c7ec85e`, state CHANGES_REQUESTED/BLOCKED, and its diff also changes `messagesToGeminiFormat`'s identity argument — the subject of the open review finding | **Folded** — `010` rewritten with the real head and the identity change in the file map | +| 5 | WP2 is not implementable for 2 of 3 files: no effects recorder in `dsh-writer-lock.test.ts` or `native-main-claim.test.ts`, both already carry #1881's guards, and #1899 is CONFLICTING/DIRTY | **Folded** — WP2 narrowed to one file; #1899 reclassified | +| 6 | WP3's sentinel already exists (`app-server-processes.ts:372/377/392`) and `unknown` already exists (line 574); the plan described shipped work | **Folded** — WP3 narrowed to the one real gap | +| 7 | WP4's durability fix targets `google-antigravity-replay.ts`, which never calls the remember API; the seam is `thought-signature-replay.ts:190` and already returns `durable` | **Folded** — retargeted, with a caller-discovery step | +| 8 | WP4's key change silently invalidates the persisted store (`version: 2` at line 143 is keyed by `keyFor` output) | **Folded** — explicit version bump + migration decision | +| 9 | Wave 5C has no rebase plan for `src/adapters/cursor/live-transport.ts`, which four PRs modify | **Folded** — rebase-and-recheck step added | + +## Accepted medium findings + +- 5B's ordering rationale was false: #1904 already bundles #1892's characterization + tests verbatim (identical blob). Order kept, rationale corrected, rebase noted. +- 5D should run `#1891 -> #1897 -> #1889`; the original order put the only + red-CI PR first and held the train hostage to it. +- `080` had two state facts inverted: **#1836 is already CLOSED** and + **#1906 is OPEN (reopened)** — both verified via `gh`. +- `git merge-base --is-ancestor` cannot verify merge *order*; once both are on + `dev` each is an ancestor of the tip. Use `git rev-list --topo-order --first-parent`. +- `bun run test` already runs `bun test --isolate` (`scripts/test.ts:144`), so + `090` presented one command as two. + +## The finding that changes the campaign shape + +Blocker 2 is the important one. The proposed 404-triggered retry was the only +*new* production mechanism in Wave 5A, and it cannot be built where the plan put +it without either (a) touching the shared core recovery loop — which `AGENTS.md` +gates behind the full suite — or (b) risking a mid-stream splice. + +The alternative is strictly better: the repo **already** lists models per +account (`src/oauth/index.ts:811`, `/v1beta/models?pageSize=1000`). Reading what +the account actually advertises beats inferring from a 404, needs no request +replay, and cannot distort the attempt log. The retry is dropped. + +## Verified-correct plan claims (kept unchanged) + +- The #1894 separation finding in `000` is confirmed real; the external audit's + "split the tables" remedy is confirmed a no-op. +- WP4's key-completeness finding is confirmed exactly right: `keyFor` uses 5 + fields where the sibling `reasoning-replay-cache.ts:65` uses 7. +- WP3's two review blockers are confirmed live on #1876's head. diff --git a/devlog/_plan/260817_wave5_execution/002_merge_order_corrections.md b/devlog/_plan/260817_wave5_execution/002_merge_order_corrections.md new file mode 100644 index 0000000000..78f64303f9 --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/002_merge_order_corrections.md @@ -0,0 +1,61 @@ +# Merge-order corrections (folds blocker 9 + medium findings) + +Amends `060`, `070`, `080`, `090` after the round-1 audit. Read alongside them. + +## Verifying merge ORDER (correction to 060 and 090) + +`git merge-base --is-ancestor` cannot verify order: once both PRs are on `dev`, +each is an ancestor of the tip regardless of which merged first. Use +`git rev-list --topo-order --first-parent dev` and compare merge-commit positions. + +## Wave 5B (060) + +Order `#1888 -> #1902 -> #1884 -> #1892 -> #1904 -> #1898` is kept, but the stated +rationale was wrong. #1892 and #1904 both add the same two +`fastwire-characterization-*.test.ts` files as **byte-identical blobs** — #1904 +already bundles the characterization suite. So "#1904 without #1892 has no +baseline" is false. + +The real consequence: after whichever lands first, the other is an add/add conflict +or a no-op. Add an explicit rebase step between them and verify the surviving test +file once, rather than assuming both apply cleanly. + +## Wave 5C (070) — the conflict surface + +`src/adapters/cursor/live-transport.ts` is modified by **four** PRs in one train: +#1900, #1887, #1896, #1903. Further overlaps: + +| Pair | Shared files | +|------|--------------| +| #1900 ∩ #1895 | `tool-definitions.ts` + its test | +| #1900 ∩ #1896 | `src/responses/parser.ts` | +| #1900 ∩ #1903 | `live-models.ts`, `cursor-hardening.test.ts` | +| #1887 ∩ #1896 | five `native-exec*.ts` files | +| #1887 ∩ #1903 | two docs files | + +Every merge after the first will conflict textually. Mandatory per merge: +rebase onto the new `dev`, re-run the focused Cursor suite, and only then merge the +next. The #1887/#1896 consolidation removes one of the four, which is an additional +reason to do it before #1903. + +## Wave 5D (080) — reorder, and two inverted facts + +Corrected order: **`#1891 -> #1897 -> #1889`**. + +#1889 and #1891 both rewrite `src/adapters/client-fingerprint.ts` and its test, so +they conflict either way — and #1889 is the only PR in the campaign with red CI +(5 failing checks). Putting it first holds the whole train hostage to it. + +State corrections, both verified with `gh`: + +- **#1836 is already CLOSED.** `080`'s "close as superseded" is a no-op; the only + remaining question is whether its unique tests were migrated. +- **#1906 is OPEN (reopened).** `080` said it stays closed. Whether it should be + closed depends on the undocumented-`v1internal` policy decision, which belongs + to the user (see `090`). + +## Wave 6 (090) + +`bun run test` already runs `bun test --isolate` (`scripts/test.ts:144`), so the +two commands `090` distinguished are one command. The remote-execution preference +for the full suite stands on its own. diff --git a/devlog/_plan/260817_wave5_execution/010_1894_gemini_wire_id.md b/devlog/_plan/260817_wave5_execution/010_1894_gemini_wire_id.md new file mode 100644 index 0000000000..eb443426bf --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/010_1894_gemini_wire_id.md @@ -0,0 +1,90 @@ +# WP1 — #1894 direct Gemini wire id (Wave 5A-1) — rev 2 after audit + +> Rev 2 folds audit blockers 1-4. The 404-triggered retry proposed in rev 1 is +> **dropped**: AI Studio installs no `fetchResponse` (`src/adapters/google.ts:384`), +> so the adapter can never observe the 404; the only hosts are the core pre-stream +> recovery loop or the mid-stream terminal guard, and the latter would splice two +> upstream turns into one client stream. + +## Defect + +`GEMINI_DIRECT_WIRE_RENAMES` (src/adapters/google.ts:58-61) unconditionally +rewrites `gemini-3.7-flash` -> `gemini-3.7-flash-tiered` for every direct Google +deployment. Two live captures disagree: + +- `a70bb78d4` (2026-08-14): bare 404s, `-tiered` 200s. +- #1894 (2026-08-16): bare 200s, `-tiered` 404s, `models.list` has no `-tiered` row. + +Neither reporter is wrong; the spelling differs per account/rollout. A static +default therefore breaks one population whichever value it takes. + +## Decision (rev 2) + +**Land #1739 only.** It is the whole of WP1's production change. No new +mechanism ships in this work-phase. + +#1739 real state: head `e1c7ec85e`, OPEN, `MERGEABLE/BLOCKED`, +`reviewDecision=CHANGES_REQUESTED`. Its diff is larger than rev 1 recorded: + +| #1739 change | Note | +|---|---| +| `resolveDirectGeminiWireModelId(modelId, applyRenames)` | the config gate | +| new `googleMode === "vertex" ? parsed.modelId` branch | Vertex keeps requested identity | +| `messagesToGeminiFormat(parsed, identityModelId)` signature change | **the open review finding** | +| `identityModelId` split for CCA vs direct | stops the `-tiered` wire spelling leaking into the model identity line | + +The identity split is a real second bug fix — without it the system prompt tells +the model it is `gemini-3.7-flash-tiered`. The open reviewer finding is that the +fix is applied too broadly across Google modes; that must be resolved before +landing, not merged as-is. + +## Deferred to its own work-phase: discovery-resolved spelling + +The durable fix is to stop guessing and read what the account advertises. +`src/oauth/index.ts:811` already issues `/v1beta/models?pageSize=1000` per +provider. A later work-phase can resolve the wire spelling from that listing, +keyed on destination + credential identity exactly as +`src/responses/reasoning-replay-cache.ts:65` does, with an explicit +`directGeminiWireRenames` value always winning over any inferred spelling. +That is a separate PABCD cycle, not a rider on #1739. + +## File change map (WP1 as executed) + +| File | Change | +|------|--------| +| `src/types.ts` | `directGeminiWireRenames?: boolean` (from #1739) | +| `src/config.ts` | zod boolean + round-trip incl. explicit `false` (from #1739) | +| `src/adapters/google.ts` | config gate, Vertex identity branch, `identityModelId` split (from #1739, review finding resolved) | +| `tests/config.test.ts` | persisted `false` round-trip; non-boolean rejected (from #1739) | +| `tests/google-adapter.test.ts` | default/true/false wire ids; CCA unaffected; identity line uses the base id (from #1739) | +| `docs-site/.../providers.md` | document the setting (from #1739) | + +## Scope boundary + +IN: direct AI Studio wire id resolution and the identity string derived from it. +OUT: any new retry/recovery mechanism; `src/server/responses/core.ts`; +Antigravity/CCA resolution; picker/catalog/usage/price keys (all stay on the base id). + +## Accept criteria (with activation) + +1. Default (setting absent) sends the `-tiered` id. + *Activation:* `buildRequest` on a default provider; assert the URL path. +2. `directGeminiWireRenames: false` sends the bare id. + *Activation:* adapter built with the flag false; assert the URL path. +3. The system-instruction identity names the base id, never the `-tiered` spelling. + *Activation:* parse the built body `systemInstruction.parts[0].text` and assert + it contains `powered by the gemini-3.7-flash` and NOT `-tiered`. +4. Cloud Code Assist request paths are unchanged by the flag. + *Activation:* build with `googleMode: cloud-code-assist` and both flag values. +5. Config round-trips an explicit `false`; a non-boolean is rejected to fallback. + +Verifier: `bun test tests/google-adapter.test.ts tests/config.test.ts tests/gemini-37-flash-migration.test.ts` +— all three exist and read the change target. + +## Closure + +#1894 gets the `bug` label (it is a real regression, currently labeled +`provider-compatibility,provider` only). It closes only once #1739 is on +`origin/dev` with 1-5 green. Because the default still favors the `-tiered` +population, the closing comment must tell the #1894 reporter to set +`directGeminiWireRenames: false` — closing silently would leave them broken. diff --git a/devlog/_plan/260817_wave5_execution/020_1899_harden_ordering.md b/devlog/_plan/260817_wave5_execution/020_1899_harden_ordering.md new file mode 100644 index 0000000000..da0984d352 --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/020_1899_harden_ordering.md @@ -0,0 +1,46 @@ +# WP2 — #1899 / harden-before-publish ordering (Wave 5A-2) — rev 2 after audit + +> Rev 2 folds blocker 5. Rev 1 named three test files; two of them have no +> effects recorder to order, and both already carry #1881's guards on `dev`. + +## Real state of #1899 + +`mergeable=CONFLICTING`, `mergeStateStatus=DIRTY`, head `8ab0aa8d0`. It conflicts +precisely because #1881 already landed two of its three files: +`tests/dsh-writer-lock.test.ts:173` already has the `existsSync` + win32 guard and +`tests/native-main-claim.test.ts:174` already carries the POSIX-only guard. + +Its remaining unique value is one file: `tests/codex-catalog-writer.test.ts`, +where `dev` still uses unbound `effects.some(...)` checks (lines 242-246) that +would pass even if the harden and the publish touched different files. #1899 +binds all three effects to one temp path. + +## Correction to rev 1 + +Rev 1 claimed the ordering assertion was #1899 residue. It is not: #1899 asserts +set membership (`expect(effects).toContain(...)`), not index order. A writer that +published first and hardened after still passes #1899's diff. Index ordering is +therefore **new work**, and it is only implementable in the one file that has an +ordered `effects` array (recorder at `tests/codex-catalog-writer.test.ts:60-95`). + +## File change map + +| File | Change | +|------|--------| +| `tests/codex-catalog-writer.test.ts` | adopt #1899's temp-path binding, then add `indexOf(harden) < indexOf(publish)` for that same temp path | + +Building effects recorders for the other two files is out of scope for Wave 5A; +their Windows/POSIX split is already correct on `dev`. + +## Accept criteria + +1. The ordering assertion fails when harden and publish are swapped. + *Activation:* invert the order in a scratch edit, capture the red run, revert. +2. `bun test tests/codex-catalog-writer.test.ts` green afterwards. +3. No assertion duplicated from #1881. + +## Closure + +#1899 cannot merge as-is (DIRTY). Land the one-file residue as a direct commit on +`dev`, then close #1899 with a comment naming the commit, what was taken, and +what #1881 already covered. diff --git a/devlog/_plan/260817_wave5_execution/030_1876_windows_discovery.md b/devlog/_plan/260817_wave5_execution/030_1876_windows_discovery.md new file mode 100644 index 0000000000..86c8626402 --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/030_1876_windows_discovery.md @@ -0,0 +1,77 @@ +# WP3 — #1876 Windows discovery -> #1852 (Wave 5A-3) — rev 2 after audit + +> Rev 2 folds blocker 6. Most of what rev 1 proposed is already on `dev`. + +## What already exists (do not re-implement) + +- `__OCX_ENUM_INCOMPLETE__` is already emitted at `src/codex/app-server-processes.ts:372` + and `:377`. +- The parser already consumes it at `:392` — by **throwing** `windows_enum_incomplete`, + not by mapping to a state. +- The `unknown` state already exists in the type at `:574`. +- `collectCodexAppServerCatalogState` already converts an enumeration throw into + `{state:"unknown"}` via `enumerationFailed`, so guidance already fails closed. + +Rev 1's "parse the sentinel to unknown" and the `collaboration.ts` change described +shipped work. Both are dropped. + +## The one real gap + +`$ErrorActionPreference='SilentlyContinue'` (`:362`) plus a top-level +`Get-CimInstance Win32_Process` (`:364`) that sits **outside** the per-process +`try/catch`. A non-terminating top-level CIM failure emits nothing, no sentinel is +produced, the parser sees clean empty output, and the collector reports +`not_running` — an unknown state laundered into a positive claim. Confirmed present +on `dev` and on #1876's head `d5acd7414` (which only refactored the string into +`windowsSnapshotPowerShellCommand()`). + +## The two-consumer subtlety rev 1 missed + +One sentinel feeds two consumers with opposite contracts: + +- `collectCodexAppServerCatalogState` — must treat the throw as `unknown` (fail closed). +- `listCodexAppServerProcesses` — deliberately swallows the same throw into + `snapshots = []` at `:427-431` for the kill/restart contract (#476). + +So emitting the sentinel on top-level failure fixes guidance and simultaneously +means "no restart targets" on the restart path. That is acceptable (restarting +nothing is safe; claiming nothing runs is not), but it must be stated, not discovered. + +## File change map + +| File | Change | +|------|--------| +| `src/codex/app-server-processes.ts` | `-ErrorAction Stop` on the top-level query at `:364`, wrapped so a failure emits `__OCX_ENUM_INCOMPLETE__`; comment the restart-path consequence at `:427` | +| `src/codex/app-server-processes.ts` | `CATALOG_STATE_TTL_MS` (5_000, uniform today) must not cache `unknown`/`not_running` for the full TTL | +| `tests/codex-app-server-processes.test.ts` | regression for a failing **top-level** CIM query (the existing test only rejects the injected callback); TTL behavior per state | + +## Accept criteria (with activation) + +1. A failing top-level CIM query yields `unknown`, not `not_running`. + *Activation:* fixture whose PowerShell output is empty due to a top-level error; + assert `state === "unknown"` and that the sentinel path ran. +2. `unknown` never produces positive disk-derived v2 guidance. +3. An `unknown` result is not served from cache for the full 5s TTL. + +Verifier: `bun test tests/codex-app-server-processes.test.ts` — the change target. +`tests/multi-agent-compat.test.ts` is run as a **no-change regression guard** for +criterion 2 only; rev 2 dropped the `collaboration.ts` edit as already-shipped, so +that suite must stay green without being modified (round-2 audit blocker B). + +Wording precision (round-2 audit): the existing test at +`tests/codex-app-server-processes.test.ts:86` does exercise a *throwing* enumerator +(by swapping `platform` so the real enumerator fails on a missing binary) rather than +only rejecting an injected callback. The untested path is narrower and is exactly the +one this work-phase adds: a top-level CIM query that returns **cleanly empty**. + +## Windows proof (honest limitation) + +`platform-windows` is `workflow_dispatch`-only and the aggregate accepts it as +skipped, so normal PR CI proves nothing about a PowerShell/CIM change. Request the +dispatch on the exact merged head; if it cannot be obtained, record that gap rather +than implying platform coverage. + +## Closure + +#1876 merges after the top-level fix; #1852 closes citing the merge SHA plus the +top-level-failure regression test. diff --git a/devlog/_plan/260817_wave5_execution/040_thought_signature_scope.md b/devlog/_plan/260817_wave5_execution/040_thought_signature_scope.md new file mode 100644 index 0000000000..47990c37f7 --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/040_thought_signature_scope.md @@ -0,0 +1,105 @@ +# WP4 — thought-signature replay scope + durability (Wave 5A-4) — rev 2 after audit + +> Rev 2 folds blockers 7 and 8. + +## Confirmed defect: incomplete key + +`keyFor` (`src/responses/thought-signature-replay.ts:74-90`) derives from five +fields: `[clientThreadId, providerName, adapterName, modelId, callId]`. The sibling +`src/responses/reasoning-replay-cache.ts:65-85` uses seven — it also includes +`providerDestinationIdentity` and `credentialIdentity`. Both fields exist on the +type (`src/types.ts:7`, `:11`) and are populated in `src/server/responses/core.ts:305-341`. + +So two accounts, two endpoints, or a pre/post credential rotation sharing one +thread + provider name + model can read each other's opaque signature. + +## The constraint rev 1 missed (blocker 8) + +This store is **durable**, unlike the in-memory sibling. Its own comment at `:66-73` +explains why the extra fields were left out: the reasoning cache's identities are +process-local HMACs (`processLocalIdentity`, `reasoning-replay-cache.ts:91`) that do +not survive a restart. Adding them naively makes every key unstable across restarts +and defeats the store's entire purpose. + +And `keyFor`'s output IS the on-disk key: the snapshot is written with `version: 2` +at `:143-144`. Changing the derivation silently invalidates every persisted entry — +every resumed thread loses its signature on upgrade and hits the exact upstream 400 +this store exists to prevent. + +### Required approach + +1. Derive a **restart-stable** destination + credential identity for this store — + not the process-local HMAC. Destination can be the normalized base URL; the + credential needs a stable non-secret discriminator (e.g. a salted-but-persisted + digest, or the account id already used for pooling). If no restart-stable + credential discriminator exists, say so and scope the fix to destination only, + rather than shipping a key that silently stops surviving restarts. +2. Bump the snapshot to `version: 3` with an explicit drop-or-migrate decision. + Dropping is acceptable (a lost signature degrades to a normal turn); silently + mismatching is not. + +## Durability: retarget (blocker 7) + +Rev 1 named `src/adapters/google-antigravity-replay.ts`. That file never calls the +remember API — it owns a separate Antigravity session snapshot with its own persist +gate. The real seam is `thought-signature-replay.ts:190-226`, which **already** +returns `durable: Promise` so a caller can await commit before exposing an item. + +A repo-wide search finds no `src/` caller of `rememberAndSerializeExtraContent` / +`rememberExtraContentForReplay` outside the module and one test. So WP4 begins with +a discovery step: identify the live call path (or establish there is none yet). If +no caller exists, the emit-before-commit defect is **not live** and that half of the +work-phase is NOOP with evidence — not a fix invented to match the audit. + +The swallowed-failure half is real: `:149` discards persist errors. + +Minor: `src/responses/parser.ts` needs no change; `:39` calls +`lookupReplayThoughtSignature`, which derives the key internally. + +## File change map + +| File | Change | +|------|--------| +| `src/responses/thought-signature-replay.ts` | restart-stable identity in `keyFor`; `version: 3` + migration decision; typed persist failure instead of a silent discard | +| `src/server/responses/core.ts` | supply the restart-stable identities to the scope ref (read-only addition) | +| `tests/thought-signature-replay-scope.test.ts` | **new file** — cross-account / cross-destination / rotation isolation; restart-stability; upgrade test proving no stale-key hit after the version bump | + +## Accept criteria (with activation) + +1. Two accounts sharing thread+provider+model never read each other's signature. + *Activation:* two writes under different credential identities, one lookup each, + assert miss. +2. Same across two destinations for one provider name. +3. A key written before restart is still readable after a simulated restart + (reload from disk) — the regression the naive fix would cause. + *Activation:* write, drop the module cache, reload, assert hit. +4. A `version: 2` file on disk does not produce a stale-key hit under `version: 3`. +5. A persist failure surfaces a typed error rather than a silent success. + +Verifier: `bun test tests/google-signature-history-roundtrip.test.ts` — the existing +coverage of this module; there is no `tests/thought-signature-replay.test.ts` on disk +(round-2 audit blocker A). Add `tests/thought-signature-replay-scope.test.ts` as a new +file for criteria 1-5, and run `tests/reasoning-replay-identity.test.ts` to prove the +sibling in-memory cache is unaffected by the identity plumbing. + +## Credential-identity specifics (round-2 audit, non-blocking finding) + +The restart-stable discriminator exists for OAuth and does not for key auth: + +| Auth mode | Material | Restart-stable? | +|-----------|----------|-----------------| +| OAuth | `accountId` + `generation` (`reasoning-replay-cache.ts:150`) | yes, and already non-secret — use it directly | +| Key | derived from `provider.apiKey` (`:163`) | value is stable but is raw secret material; needs a persisted-salt digest, or scope to destination only | +| `local` | `credentialIdentity` is `undefined` (`core.ts:329`) | n/a | + +So the honest-scoping fallback binds only for key auth; do not discard OAuth +scoping because one mode is hard. And because `keyFor`'s guard is all-or-nothing, +a required credential field would make `authMode: "local"` providers stop +remembering entirely — the policy for that case must be stated in the +implementation, not left to the guard's default. + +## Closure + +File the issue with these conditions, implement, close citing the merge SHA and the +isolation + restart test output. If the discovery step shows no live caller, the +issue records that finding instead of claiming a fix. diff --git a/devlog/_plan/260817_wave5_execution/050_1849_1049_durability.md b/devlog/_plan/260817_wave5_execution/050_1849_1049_durability.md new file mode 100644 index 0000000000..51aeea185c --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/050_1849_1049_durability.md @@ -0,0 +1,66 @@ +# WP5 — #1849 updater transaction + #1049 coordinator adoption (Wave 5A-5) + +## #1849 — split, do not auto-close + +v2.24.2 shipped #1877, which stops the service restart loop when the install is +missing. That is the mitigation half. The root defect is the non-atomic update +transaction: download → delete existing files → copy new files, which leaves a +file-less package skeleton when it fails midway. + +Split into: + +- **A (shipped):** missing-install service restart loop → #1877, released. +- **B (open):** transactional updater with rollback. + +B's state machine: + +``` +download to staging + → verify executable/assets/version + → snapshot existing install + → atomic directory or launcher switch + → service health probe + → success: drop snapshot | failure: restore snapshot +``` + +Forbidden: deleting the existing install before the replacement is verified. + +| File | Change | +|------|--------| +| `src/update/job.ts` | staging + verify + snapshot + atomic switch + rollback | +| `tests/update-transaction.test.ts` | kill at every boundary; assert a working install survives each | + +## #1049 — coordinator adoption + +Pre-substrate Codex homes have no coordinator row. Writing `{0, null}` for them +erases the only evidence of routed residue or an interrupted transition, so the +fix is a classifier plus a two-phase transaction, not a widened fallback. + +States: `clean`, `routed-recoverable`, `adoption-pending`, `adopted`, +`ambiguous`, `invalid`. + +``` +BEGIN IMMEDIATE + read exact file snapshots B; classify; write pending row + B fingerprints +COMMIT + ... publish metadata atomically ... +BEGIN IMMEDIATE + confirm pending tx id; re-read snapshots C; require C == B or a named safe + transition; mark adopted/recoverable +COMMIT +``` + +A crash in `adoption-pending` resumes the SAME transaction; changed evidence +stops at `ambiguous` rather than guessing. + +## Accept criteria + +1. Updater: every injected kill boundary leaves a launchable install. +2. Adoption: each classifier state has a fixture; `ambiguous` fails closed. +3. Two-process contention cannot produce two adoptions. + +## Closure + +#1849 stays OPEN as the transactional-updater issue with A recorded as shipped. +#1049 closes only on landed adoption + crash-recovery evidence; otherwise it is +reported as a real terminal outcome, not silently dropped. diff --git a/devlog/_plan/260817_wave5_execution/060_wave5b_continuation.md b/devlog/_plan/260817_wave5_execution/060_wave5b_continuation.md new file mode 100644 index 0000000000..dcd340ee1d --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/060_wave5b_continuation.md @@ -0,0 +1,51 @@ +# WP6 — Wave 5B: continuation, provider, usage semantics + +Merge order is a dependency chain, not a priority list. + +``` +#1888 → #1902 → #1884 → #1892 → #1904 → #1898 +``` + +## #1888 — scope combo continuation replay (CHANGES_REQUESTED, head cd3367193) + +Restoring `previous_response_id` state must match on provider, adapter, model, +destination, credential/account, and an immutable parent snapshot. Anything +less lets a failover or rotation resume another context's continuation. Same +key-completeness principle as WP4 — land WP4's identity plumbing first where +they touch the same record. + +Gate: credential-rotation and failover destination-change tests before merge. + +## #1902 — ClinePass stale OMP reasoning tiers (head b8983c912, BLOCKED/mergeable) + +Narrow. Rebase on `1208bd25c`, exact-head CI, merge. + +## #1884 — DeepSeek V4 tool replay loops (head 99b0bbc38, 25 checks green) + +Narrow replay-loop fix. Gate: structured tool call and reasoning continuation +preserved on a live-shaped fixture. + +## #1892 → #1904 — FastWire (#1886) + +Order is load-bearing: #1892 is the A0 characterization that locks current +service-tier behavior; #1904 changes it. Merging #1904 first would leave the +production change with no red/characterization baseline. #1904 is still draft. + +## #1898 — pacing anchored to transport starts (draft, head 7279aca7c) + +Still design-stage. Required before merge: + +- queued time vs transport-start time distinguished +- retries do not double-advance the pacing clock +- a cancelled waiter does not consume a slot +- per-account pacing isolation +- deterministic fake-clock concurrency test + +If those are not met, #1898 defers with a recorded reason rather than shipping +a timing change on inference. + +## Accept criteria + +Each PR either lands with focused tests green on `origin/dev`, or carries a +recorded blocker disposition naming exactly what is missing. Merge order is +preserved and verified with `git merge-base --is-ancestor`. diff --git a/devlog/_plan/260817_wave5_execution/070_wave5c_cursor.md b/devlog/_plan/260817_wave5_execution/070_wave5c_cursor.md new file mode 100644 index 0000000000..11f472a7ad --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/070_wave5c_cursor.md @@ -0,0 +1,51 @@ +# WP7 — Wave 5C: Cursor train + +``` +#1900 → #1895 → (#1887 ⊕ #1896) → #1903 → #1866 +``` + +## #1900 — nested tools, clean EOF, GetUsableModels, cmd/command (head 1824a0148) + +Normalizes Cursor request/tool/result shape. Gates before merge: + +- non-loopback discovery is HTTPS-only; loopback HTTP is the only exception +- a Bearer credential is refused before it can leave over plain HTTP to a + remote endpoint +- clean EOF counts as success only with no open tool call; EOF with an + unfinished tool call is a protocol error +- #1866 stays open — #1900 explicitly scopes it out + +## #1895 — code-mode nested helpers in the shared catalog nudge (CHANGES_REQUESTED) + +Guidance must be generated from the actually advertised catalog. Hardcoding +`exec`, `read_file` and friends re-introduces per-provider drift, which is the +defect this PR exists to remove. + +## #1887 ⊕ #1896 — consolidate, never merge both + +Both touch the flat Responses Lite catalog, denied-native-tool routing through +`exec`, and fallback tool shape. Merging both re-splits the contract. Pick one +canonical PR, migrate the other's unique tests into it, close the loser as +superseded. + +Matrix the survivor must cover: flat function catalog; code-mode exec; alias; +no bridge; mixed catalog; fetch/read/shell; and — critically — no hardcoded +`exec` when the catalog does not advertise it. + +## #1903 — HTTP/1.1 compatibility transport (head 54893ca6e) + +Keep HTTP/2 the default and h1 opt-in. Enforce at the transport layer that +credentials cannot egress over remote plain HTTP. + +## #1866 — Computer Use / node_repl empty or truncated results + +Byte truncation is the wrong repair. The model needs a structured summary it can +act on: focused app/window, URL, current element identity, action error, an +explicit "re-query state" recovery instruction, and a screenshot/blob reference, +with the full payload in bounded separate storage. + +## Accept criteria + +Train order preserved; exactly one of #1887/#1896 lands; no credential reaches a +remote plain-HTTP endpoint in any test; #1866 either lands structured payloads or +is reported with its real terminal outcome. diff --git a/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md b/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md new file mode 100644 index 0000000000..42c07006d7 --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/080_wave5d_antigravity.md @@ -0,0 +1,43 @@ +# WP8 — Wave 5D: Antigravity fingerprint and discovery + +``` +#1889 → #1891 → #1897 (then close #1836 as superseded) +``` + +This wave touches auth/provider fingerprinting, so it carries the security +review expectation from `AGENTS.md`: no token, account, or project value may +appear in a snapshot, log, or test fixture. + +## #1889 — drop synthetic `x-goog-api-client` (draft, **5 failing checks**) + +Failing CI is the first thing to resolve; a fingerprint change with red checks +is not a merge candidate. Removes a synthesized header and an unverified fixed +`ide_version`. + +## #1891 — User-Agent token order + `auth_method` (head 10b88e155, 14 green) + +Aligns with the 2.5.5 decompilation. Keep the live `fetchAvailableModels` and +`generateContent` success evidence attached; the value of this PR is that it +matches an observed client, not a plausible one. + +## #1897 — match live agy model discovery (head 38c25aed8, 24 green) + +Removes hardcoded model injection and preserves CCA-discovered wire ids exactly. +Required cache contract: publish discovery with a generation; invalidate on +credential rotation, provider removal, and authorization failure/revocation; +never reuse one account's discovered models for another; stay fully separate +from the direct Google alias table (see WP1). + +## Accept criteria + +1. Captured `onboardUser` and `loadCodeAssist` requests show no synthetic + `x-goog-api-client` and the intended UA token order. +2. A UA override never leaks into body metadata. +3. Discovered wire ids round-trip byte-exact. +4. Every capture fixture is redacted. + +## Closure + +#1836 closes as superseded once #1889 and #1891 land and its unique tests are +migrated. #1906 stays closed unless policy changes to allow undocumented +`v1internal` inference. diff --git a/devlog/_plan/260817_wave5_execution/090_wave6_closeout.md b/devlog/_plan/260817_wave5_execution/090_wave6_closeout.md new file mode 100644 index 0000000000..0c8c825344 --- /dev/null +++ b/devlog/_plan/260817_wave5_execution/090_wave6_closeout.md @@ -0,0 +1,43 @@ +# WP9 — Wave 6: gate, closeout, promotion + +## Gate + +`bun run typecheck` and the full `bun run test` suite on the promotion +candidate, plus `bun run privacy:scan`. Remote execution (`ssh macmini-cf`) is +preferred for the full suite per the workspace convention; `bun test --isolate +tests` avoids the cross-file environment bleed that makes raw `bun test` +misleading in this checkout. + +## Closure rules (binding, from the audit's policy set) + +- Close an issue only for the acceptance case the landed change actually + satisfies. Partial fixes never auto-close an umbrella (#1849 is the model). +- **#1059** does not close without hosted Windows shard evidence — 4 shards, + green, on the exact post-#1881 head. A local 806/806 batch is good evidence + and still not the required gate. +- **#1795** does not close without a live SenseNova/Kimi canary showing zero + undeclared tool calls. +- **#1843** is closable now: fixed by #1860, released in v2.24.2. +- State is judged by merge commit and branch ancestry first, GitHub API second, + cached HTML badges last. #1881 showed an Open badge while merged. + +## Promotion + +`dev` → `preview` and `dev` → `main`, each verified with +`git merge-base --is-ancestor dev origin/` after push. Release +publication itself stays with the repository's canonical release workflow — +never a direct `npm publish`. + +## Terminal reporting + +Every roadmap item ends the campaign labeled with one of DONE / NOOP / BLOCKED / +UNSAFE / NEEDS_HUMAN / BUDGET_EXHAUSTED and the evidence behind that label. A +list of remaining independent features is not BLOCKED; it is the next work-phase. + +## Policy decisions reserved for the user + +The audit raised ten. These are not agent decisions and are reported, not +resolved: close-on-dev vs close-on-release; #1059 consecutive-green count; +Cursor non-loopback HTTP; Antigravity undocumented protocol posture; +needs-info lifetime; upstream-tracker accounting; #1795 recovery shape; +#1899 disposition; #1836 disposition; #1903 HTTP/1.1 default. From 247f1c8caeddfdb4d5bf91b3df191de4acd37364 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Mon, 17 Aug 2026 20:20:29 +0900 Subject: [PATCH 8/8] docs(devlog): fold the round-2 and round-3 audit findings Round 2 found both remaining defects in the plan's verifier commands, which is the same class of error round 1 caught in the file maps: an accept criterion pointing at something that does not exist. 040 named tests/thought-signature-replay.test.ts, which is not on disk. The module's real coverage is tests/google-signature-history-roundtrip.test.ts, and the scope test is new work, so both are now labelled for what they are. The same document gained the credential-identity split it was hedging around: OAuth already has a restart-stable discriminator in accountId+generation and should use it directly, key auth has stable-but-secret material that needs a persisted-salt digest, and authMode local has no credentialIdentity at all - which matters because keyFor's guard is all-or-nothing and would otherwise stop local providers remembering anything. 030 kept multi-agent-compat.test.ts in its verifier after rev 2 dropped the collaboration.ts change as already-shipped. It stays in the command as a no-change regression guard, now labelled as one. Its description of the existing coverage was also imprecise: that test does drive a throwing enumerator by swapping platform. The untested path is narrower and is exactly what this work-phase adds - a top-level CIM query that returns cleanly empty. Round 3 was a fresh reviewer confirming all of it against the tree, and cleared with two nits, both fixed here: 050's new test file now carries the same new-file marker 040 uses, and 030's superseded parenthetical is gone. --- .../_plan/260817_wave5_execution/030_1876_windows_discovery.md | 2 +- devlog/_plan/260817_wave5_execution/050_1849_1049_durability.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/devlog/_plan/260817_wave5_execution/030_1876_windows_discovery.md b/devlog/_plan/260817_wave5_execution/030_1876_windows_discovery.md index 86c8626402..5e2a2b69ae 100644 --- a/devlog/_plan/260817_wave5_execution/030_1876_windows_discovery.md +++ b/devlog/_plan/260817_wave5_execution/030_1876_windows_discovery.md @@ -43,7 +43,7 @@ nothing is safe; claiming nothing runs is not), but it must be stated, not disco |------|--------| | `src/codex/app-server-processes.ts` | `-ErrorAction Stop` on the top-level query at `:364`, wrapped so a failure emits `__OCX_ENUM_INCOMPLETE__`; comment the restart-path consequence at `:427` | | `src/codex/app-server-processes.ts` | `CATALOG_STATE_TTL_MS` (5_000, uniform today) must not cache `unknown`/`not_running` for the full TTL | -| `tests/codex-app-server-processes.test.ts` | regression for a failing **top-level** CIM query (the existing test only rejects the injected callback); TTL behavior per state | +| `tests/codex-app-server-processes.test.ts` | regression for a top-level CIM query that returns **cleanly empty** (see the wording note below for what existing coverage does and does not reach); TTL behavior per state | ## Accept criteria (with activation) diff --git a/devlog/_plan/260817_wave5_execution/050_1849_1049_durability.md b/devlog/_plan/260817_wave5_execution/050_1849_1049_durability.md index 51aeea185c..4e93d66425 100644 --- a/devlog/_plan/260817_wave5_execution/050_1849_1049_durability.md +++ b/devlog/_plan/260817_wave5_execution/050_1849_1049_durability.md @@ -28,7 +28,7 @@ Forbidden: deleting the existing install before the replacement is verified. | File | Change | |------|--------| | `src/update/job.ts` | staging + verify + snapshot + atomic switch + rollback | -| `tests/update-transaction.test.ts` | kill at every boundary; assert a working install survives each | +| `tests/update-transaction.test.ts` | **new file** — kill at every boundary; assert a working install survives each | ## #1049 — coordinator adoption