diff --git a/devlog/_plan/260912_cache_lane/040_hermes.md b/devlog/_plan/260912_cache_lane/040_hermes.md index cc75d79180..de79dc1d7e 100644 --- a/devlog/_plan/260912_cache_lane/040_hermes.md +++ b/devlog/_plan/260912_cache_lane/040_hermes.md @@ -7,3 +7,5 @@ MODIFY tests/responses/chat-completions-endpoint.test.ts or a registered adjacen MODIFY canonical inbound contract docs to distinguish stable client conversation identity, request-scoped lane and prompt prefix. Durable scratch evidence names public comment URLs, actual test command coverage and limitations. Real Hermes same-conversation/fresh-session identifier and outbound capture from its running client are unavailable unless provided by existing public evidence; synthetic regression proves transport contract only. Do not claim actual client identity was observed, cache hits improved or #3433 solved. C hosted final tip executes the contract; local suite NOT RUN. D records exactly what is proven and remaining controlled live-client comparison. + +Execution refinement: NEW tests/responses/chat-conversation-affinity.test.ts and register it in both layout maps. Invoke actual Chat handler with synthetic caller JWT against canonical ChatGPT Responses config in isolated homes. Mock only outbound fetch, record Headers/body. Two header shapes (underscore session_id, hyphen session-id/thread-id), key present/absent, A/A/B growing messages; explicit request-id differs per turn. Absent identity controls prove shared key never becomes a session. This fixture establishes OCX preservation, not actual Hermes emission. No runtime patch unless evidence finds loss. diff --git a/devlog/_plan/260912_cache_lane/041_hermes_ci_refresh.md b/devlog/_plan/260912_cache_lane/041_hermes_ci_refresh.md new file mode 100644 index 0000000000..5c7f1cb3d9 --- /dev/null +++ b/devlog/_plan/260912_cache_lane/041_hermes_ci_refresh.md @@ -0,0 +1,11 @@ +# Hermes hosted-CI refresh after shared fixture repair + +Resume preserves the same worktree and session. The host goal is blocked and persisted phase remains C; no goal/FSM reset, reactivation or completion is claimed. This file records the authorized remaining work, not a new completed cycle. + +The original Hermes head `b254efc8385ce2a9dc34b9a5ac7d2a449605d75d` failed gates on the Combo active-reactivation fixture, while four Linux and two macOS product shards succeeded. Shared repair #4390 is now integrated as `20861aebf56c6f8ec2b0d8d04d1d0b54441650bb` and its exact hosted CI `34688482827` succeeds. Affinity/prefix old runs failed restore/Cline fixtures subsequently repaired by that same PR. Old failed runs remain failed. + +Rebase the one owned Hermes test-only commit from `e4ee8c54` onto current `origin/dev` at `392e182a00` (record full SHA in handoff). Read-only merge-tree reports no conflict. Preserve the 99-line runtime-boundary test and both mappings; no Combo, Cline, restore or provider source edits. Compare old/new authored source deltas and obtain an inherited-model independent source audit. Append terminal evidence to `060_handoff.md`, preserving real Hermes acceptance as open. Push only existing Hermes branch with `--no-verify` and exact old-head lease; no new task/worktree or recreation of merged evidence PR #4377. Bind new final hosted CI to new Hermes SHA. Parent owns integration and chooses any source collision slot. + +Local suites/focused/GUI/build/typecheck/install remain NOT RUN. The successful shared integration CI proves its own cumulative source tree, not the old failed PR heads. A new Hermes tip must be independently verified remotely before a passing delivery claim. + +Parent integration-slot update: final pinned base is `c311f9bf7f5003af29fa8e7ebc2f2b5db20267f6`, including the subsequently integrated pnpm and Devin fixture corrections. Rebase the two owned commits from `392e182a` without runtime changes. Original 99-line test and mappings must remain byte-identical; source review is renewed for final head. Prior run `34693156321` at `524afd8d80` is superseded evidence only, never final-tip proof. Parent is holding the Hermes slot. Persisted phase C and blocked host goal stay unchanged. diff --git a/devlog/_plan/260912_cache_lane/060_handoff.md b/devlog/_plan/260912_cache_lane/060_handoff.md index 551d1b62ac..d6522bec9f 100644 --- a/devlog/_plan/260912_cache_lane/060_handoff.md +++ b/devlog/_plan/260912_cache_lane/060_handoff.md @@ -28,3 +28,13 @@ Local tests of every size, build, typecheck and install were **NOT RUN** by expl ## Final evidence-cycle record The final evidence B phase produces this tracked update as its documentation artifact. An earlier C transition was rejected by SOURCE-DELTA-01 because only scratch metadata and the PR body had changed; that rejected transition did not advance the FSM. The evidence branch now records the actual delivered source heads, parent integrations and unproven closure attribution. It changes no runtime code. Hosted terminal results must still be read before this cycle closes; an evidence document is not a product-test pass. + +## Resumed terminal-CI reconciliation + +The original final-tip results are now terminal: affinity run `34674962749` FAILED (native restore/injection fixtures); prefix run `34675829597` FAILED (the same restore family plus Cline registry/CLI/localization/icon/test-layout expectations); Hermes run `34674763850` FAILED (Combo active-reactivation GUI fixture). Their passing cache assertions do not make those runs green. Claim run `34673563105` remains SUCCESS and was not rerun. + +Shared fixture repair #4390 is merged at `20861aebf56c6f8ec2b0d8d04d1d0b54441650bb`, containing the delivered affinity and repaired prefix heads by verified Git ancestry. Its [hosted CI 34688482827](https://github.com/lidge-jun/opencodex/actions/runs/34688482827) succeeded: 19 jobs successful, 2 skipped. This is new cumulative integration evidence, not a relabeling of the old failed results. Cache runtime sources were not rewritten to fix another lane's failure. + +The remaining open Hermes PR #4365 is refreshed onto `392e182a004d61b38c7cf652642e63b9a11d9a65` without conflicts, preserving its test-only delta. Its new final head and hosted outcome are exported to that PR description and the scratch handoff after source audit and publication. Merged evidence PR #4377 is left intact. The host goal is blocked; persisted phase C is preserved and neither is claimed completed. Local product suites/build/typecheck/install remain NOT RUN. + +Final Hermes slot: the coordinator pinned `c311f9bf7f5003af29fa8e7ebc2f2b5db20267f6` after the pnpm/Devin fixture corrections. The test-only PR is rebased onto that fixed base without conflicts or runtime edits. Its exact final-head source review and hosted run replace the earlier `524afd8d80` candidate evidence in the PR description. The previous failed runs remain historical failures, and actual Hermes client-field acceptance remains open. diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 4f35987b54..50c91e31ab 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -279,6 +279,7 @@ "catalog-vision-sidecar-modalities.test.ts": "codex-integration", "catalog-zero-credit-picker.test.ts": "codex-integration", "chat-completions-endpoint.test.ts": "responses", + "chat-conversation-affinity.test.ts": "responses", "chat-json-sse-fallback.test.ts": "responses", "chat-refusal.test.ts": "responses", "chatgpt-device-auth.test.ts": "oauth", diff --git a/structure/data-planes/inbound-compat.md b/structure/data-planes/inbound-compat.md index 2d17c11875..ee1e7b69c0 100644 --- a/structure/data-planes/inbound-compat.md +++ b/structure/data-planes/inbound-compat.md @@ -33,6 +33,15 @@ request-signal cancellation contracts as routed Responses transport. Because call before it binds the adapter; the pick remains inert unless a strategy is configured and the committed key is cooling. See [`responses.md`](../transports/responses.md). +## Chat conversation identity forwarding + +`src/server/chat-completions.ts` preserves caller `prompt_cache_key` on the Chat-to-Responses +bridge. Canonical ChatGPT Responses forwarding preserves `session_id`, `session-id`, `thread-id` +and per-request `x-client-request-id` under their original names. Missing conversation identity +stays missing; a shared prefix/cache key is not converted into a session. The direct-mode +outbound contract is covered by `tests/responses/chat-conversation-affinity.test.ts`. +This transport contract does not prove a client's emission, Pool selection stability or cache hits. + ## Chat streaming client with a JSON upstream result The translated inbound path in `src/server/chat-completions.ts` may receive a complete JSON diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index b758e9e9d8..50191411d6 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -113,6 +113,7 @@ "catalog-vision-sidecar-modalities.test.ts": "codex-integration", "catalog-zero-credit-picker.test.ts": "codex-integration", "chat-completions-endpoint.test.ts": "responses", + "chat-conversation-affinity.test.ts": "responses", "chat-json-sse-fallback.test.ts": "responses", "chat-refusal.test.ts": "responses", "chatgpt-device-auth.test.ts": "oauth", diff --git a/tests/responses/chat-conversation-affinity.test.ts b/tests/responses/chat-conversation-affinity.test.ts new file mode 100644 index 0000000000..97c599b308 --- /dev/null +++ b/tests/responses/chat-conversation-affinity.test.ts @@ -0,0 +1,99 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { handleChatCompletions } from "../../src/server/chat-completions"; +import type { OcxConfig } from "../../src/types"; +import { fakeChatGptJwt } from "../helpers/fake-chatgpt-jwt"; +import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/isolated-codex-home"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; + +// #3433 transport contract only: these client-assigned fixture IDs are not a capture of Hermes. +const originalFetch = globalThis.fetch; +const identityHeaders = ["session_id", "session-id", "thread-id", "x-codex-parent-thread-id"]; +let isolated: IsolatedCodexHome; +let home: string; +let previousHome: string | undefined; + +beforeEach(() => { + isolated = installIsolatedCodexHome("ocx-chat-identity-"); + previousHome = process.env.OPENCODEX_HOME; + home = mkdtempSync(join(tmpdir(), "ocx-chat-identity-config-")); + process.env.OPENCODEX_HOME = home; +}); +afterEach(() => { + globalThis.fetch = originalFetch; + isolated.restore(); + if (previousHome === undefined) delete process.env.OPENCODEX_HOME; + else process.env.OPENCODEX_HOME = previousHome; + removeTreeWithRetry(home); +}); + +interface Capture { headers: Headers; body: Record } +function mockNativeWire(): Capture[] { + const seen: Capture[] = []; + globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => { + expect(String(input)).toBe("https://chatgpt.com/backend-api/codex/responses"); + seen.push({ headers: new Headers(init?.headers), body: JSON.parse(String(init?.body)) }); + return Response.json({ id: "resp_chat_identity", object: "response", status: "completed", + output: [{ type: "message", role: "assistant", content: [{ type: "output_text", text: "ok" }] }], + usage: { input_tokens: 10, output_tokens: 1, total_tokens: 11, input_tokens_details: { cached_tokens: 0 } } }); + }) as typeof fetch; + return seen; +} + +async function chat(headers: Record, cacheKey: string | undefined, continued: boolean) { + const token = fakeChatGptJwt({ chatgpt_account_id: "fixture-chat-caller", exp: Math.floor(Date.now() / 1000) + 86400 }); + const config = { defaultProvider: "openai", openaiProviderTierVersion: 2, providers: { + openai: { adapter: "openai-responses", authMode: "forward", codexAccountMode: "direct", + baseUrl: "https://chatgpt.com/backend-api/codex", models: ["gpt-5.6-luna"] }, + } } as OcxConfig; + const messages = [{ role: "system", content: "A shared prefix is not a conversation identity." }, + { role: "user", content: "first turn" }, + ...(continued ? [{ role: "assistant", content: "first answer" }, { role: "user", content: "next turn" }] : [])]; + const req = new Request("http://localhost/v1/chat/completions", { method: "POST", + headers: { "content-type": "application/json", authorization: `Bearer ${token}`, + "chatgpt-account-id": "fixture-chat-caller", ...headers }, + body: JSON.stringify({ model: "openai/gpt-5.6-luna", messages, stream: false, reasoning_effort: "medium", + ...(cacheKey === undefined ? {} : { prompt_cache_key: cacheKey }) }), + }); + const response = await handleChatCompletions(req, config, { model: "", provider: "" }); + await response.text(); + expect(response.status).toBe(200); +} + +describe("Chat conversation identity at canonical Responses outbound boundary", () => { + for (const keyPresent of [false, true]) { + for (const shape of ["underscore", "hyphen-pair"] as const) { + test(`${shape}, key=${keyPresent}: A/A/B retains caller identity and independent request IDs`, async () => { + const seen = mockNativeWire(); + const key = keyPresent ? "shared-cache-cohort" : undefined; + for (const [index, conversation] of ["a", "a", "b"].entries()) { + const identity: Record = shape === "underscore" + ? { session_id: `conversation-${conversation}` } + : { "session-id": `session-${conversation}`, "thread-id": `thread-${conversation}` }; + await chat({ ...identity, "x-client-request-id": `request-${index}` }, key, index === 1); + expect(seen).toHaveLength(index + 1); + const wire = seen[index]!; + for (const name of identityHeaders) expect(wire.headers.get(name)).toBe(identity[name] ?? null); + expect(wire.headers.get("x-client-request-id")).toBe(`request-${index}`); + expect(wire.body.prompt_cache_key).toBe(key); + expect(Object.hasOwn(wire.body, "prompt_cache_key")).toBe(keyPresent); + expect(wire.body.reasoning).toMatchObject({ effort: "medium" }); + } + expect(JSON.stringify(seen[1]!.body.input).length).toBeGreaterThan(JSON.stringify(seen[0]!.body.input).length); + expect(seen[0]!.body.input).toEqual(seen[2]!.body.input); + }); + } + + test(`identity absent, key=${keyPresent}: no session is synthesized`, async () => { + const seen = mockNativeWire(); + for (const continued of [false, true]) await chat({}, keyPresent ? "shared-cache-cohort" : undefined, continued); + expect(seen).toHaveLength(2); + for (const wire of seen) { + for (const name of identityHeaders) expect(wire.headers.has(name)).toBe(false); + expect(wire.body.prompt_cache_key).toBe(keyPresent ? "shared-cache-cohort" : undefined); + } + }); + } +});