Skip to content

Commit 4ff7e8e

Browse files
committed
Merge branch 'main' into cl-6904-grokopenai-responses-adapters-omit-prompt_cache_key-xai
2 parents 2de4f8e + 7a122ab commit 4ff7e8e

4 files changed

Lines changed: 88 additions & 19 deletions

File tree

src/auth/codex/instructions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ const PROMPT_SENTINEL = "You are Codex";
2121
const MIN_PROMPT_LENGTH = 1000;
2222

2323
// Bounds both network calls below so a black-holed connection can never hang
24-
// exec boot, which awaits refreshCodexInstructions before first inference.
24+
// exec boot or the TUI's first deliver, both of which wait on
25+
// refreshCodexInstructions settling before first inference.
2526
const CODEX_INSTRUCTIONS_TIMEOUT_MS = 10_000;
2627

2728
export function isValidCodexPrompt(text: string): boolean {

src/tui/deliver-agent-message.test.ts

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { describe, expect, test } from "bun:test";
22
import { deliverAgentMessage } from "./deliver-agent-message.js";
33

44
describe("deliverAgentMessage", () => {
5-
test("surfaces a not-delivered notice instead of throwing when the agent is mid-rebuild", () => {
5+
test("surfaces a not-delivered notice instead of throwing when the agent is mid-rebuild", async () => {
66
const notices: string[] = [];
77
const fatal = new Error("agent rebuild failed: provider unreachable");
88
let delivered = false;
99

10-
deliverAgentMessage({
10+
await deliverAgentMessage({
1111
getFatalBuildError: () => fatal,
1212
deliverToLiveAgent: () => {
1313
delivered = true;
@@ -24,10 +24,10 @@ describe("deliverAgentMessage", () => {
2424
expect(notices[0]).toContain("provider unreachable");
2525
});
2626

27-
test("surfaces a not-delivered notice when the live agent throws on delivery", () => {
27+
test("surfaces a not-delivered notice when the live agent throws on delivery", async () => {
2828
const notices: string[] = [];
2929

30-
deliverAgentMessage({
30+
await deliverAgentMessage({
3131
getFatalBuildError: () => null,
3232
deliverToLiveAgent: () => {
3333
throw new Error("agent is closed");
@@ -40,11 +40,11 @@ describe("deliverAgentMessage", () => {
4040
expect(notices[0]).toContain("agent is closed");
4141
});
4242

43-
test("delivers normally and stays silent when the agent is healthy", () => {
43+
test("delivers normally and stays silent when the agent is healthy", async () => {
4444
const notices: string[] = [];
4545
let delivered = false;
4646

47-
deliverAgentMessage({
47+
await deliverAgentMessage({
4848
getFatalBuildError: () => null,
4949
deliverToLiveAgent: () => {
5050
delivered = true;
@@ -55,4 +55,53 @@ describe("deliverAgentMessage", () => {
5555
expect(delivered).toBe(true);
5656
expect(notices).toHaveLength(0);
5757
});
58+
59+
test("holds delivery until ready settles so the first request cannot race startup work", async () => {
60+
// Models the Codex instructions refresh: the request body prefix pins the
61+
// instructions text, so delivery — and therefore request building — must
62+
// wait for the in-flight refresh instead of letting it swap the value
63+
// between turn 1 and turn 2.
64+
let instructions = "cached";
65+
let observedAtDelivery = "";
66+
let resolveReady!: () => void;
67+
const ready = new Promise<void>((resolve) => {
68+
resolveReady = resolve;
69+
});
70+
71+
const pending = deliverAgentMessage({
72+
getFatalBuildError: () => null,
73+
ready,
74+
deliverToLiveAgent: () => {
75+
observedAtDelivery = instructions;
76+
},
77+
onDeliverFailure: () => undefined,
78+
});
79+
80+
// Refresh still in flight: nothing delivered yet.
81+
await Promise.resolve();
82+
expect(observedAtDelivery).toBe("");
83+
84+
instructions = "refreshed";
85+
resolveReady();
86+
await pending;
87+
88+
// The first request observed the post-refresh value, so a later-settling
89+
// refresh can never change the prefix of an already-started session.
90+
expect(observedAtDelivery).toBe("refreshed");
91+
});
92+
93+
test("delivers immediately when ready is already settled", async () => {
94+
let delivered = false;
95+
96+
await deliverAgentMessage({
97+
getFatalBuildError: () => null,
98+
ready: Promise.resolve(),
99+
deliverToLiveAgent: () => {
100+
delivered = true;
101+
},
102+
onDeliverFailure: () => undefined,
103+
});
104+
105+
expect(delivered).toBe(true);
106+
});
58107
});

src/tui/deliver-agent-message.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,20 @@
66
*/
77
export interface DeliverAgentMessageDeps {
88
getFatalBuildError: () => Error | null;
9+
/**
10+
* Settles before any delivery. Gates the session's first request on startup
11+
* work that must not race request building — the Codex instructions refresh:
12+
* a late in-memory swap would change the request prefix and forfeit the
13+
* provider prompt cache for the rest of the session. Must never reject
14+
* (callers attach their own fallback handling).
15+
*/
16+
ready?: Promise<void>;
917
deliverToLiveAgent: () => void;
1018
onDeliverFailure: (message: string) => void;
1119
}
1220

13-
export function deliverAgentMessage(deps: DeliverAgentMessageDeps): void {
21+
export async function deliverAgentMessage(deps: DeliverAgentMessageDeps): Promise<void> {
22+
if (deps.ready !== undefined) await deps.ready;
1423
const fatal = deps.getFatalBuildError();
1524
if (fatal !== null) {
1625
deps.onDeliverFailure(`Message not delivered: ${fatal.message}`);

src/tui/runner.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,6 +1319,25 @@ export async function runTUI(initialConfig: Config): Promise<number> {
13191319
{ providerName: config.providerName, model: config.model },
13201320
);
13211321

1322+
const initialCodexProfile = codexProfileFromProviderName(config.providerName);
1323+
const initialXaiProfile = xaiProfileFromProviderName(config.providerName);
1324+
1325+
// Refresh the pinned Codex instructions without blocking TUI startup. Every
1326+
// deliver below awaits settlement, so the first request never races the
1327+
// refresh: codex-responses-adapter places the instructions at the top of
1328+
// every request body, and an in-memory swap after inference #1 would change
1329+
// the whole prefix and forfeit the provider prompt cache mid-session.
1330+
// Best-effort, same as exec boot: on failure the session runs on
1331+
// cached/bundled instructions.
1332+
const codexInstructionsRefreshed: Promise<void> =
1333+
initialCodexProfile === undefined
1334+
? Promise.resolve()
1335+
: refreshCodexInstructions().catch((err: unknown) => {
1336+
tuiLogger.warn("Codex instructions refresh failed: {error}", {
1337+
error: err instanceof Error ? err.message : String(err),
1338+
});
1339+
});
1340+
13221341
// Reload, interrupt, compaction continuation, and proxy deliver share one queue
13231342
// so a rebuild never races an in-flight deliver.
13241343
const sessionOps = createSessionOperationQueue();
@@ -1327,8 +1346,9 @@ export async function runTUI(initialConfig: Config): Promise<number> {
13271346
// The shell already popped the queue item and painted it as delivered
13281347
// by the time this runs, so a failed rebuild must be surfaced here —
13291348
// otherwise the message silently never reaches the agent.
1330-
deliverAgentMessage({
1349+
await deliverAgentMessage({
13311350
getFatalBuildError: () => fatalBuildError,
1351+
ready: codexInstructionsRefreshed,
13321352
deliverToLiveAgent,
13331353
onDeliverFailure: systemNotice,
13341354
});
@@ -1381,16 +1401,6 @@ export async function runTUI(initialConfig: Config): Promise<number> {
13811401
// When the session starts on a Codex profile, seed the agent with a Responses
13821402
// source (account id pulled from the resolved catalog entry, session id from
13831403
// the run) rather than the OpenAI-compatible one.
1384-
const initialCodexProfile = codexProfileFromProviderName(config.providerName);
1385-
const initialXaiProfile = xaiProfileFromProviderName(config.providerName);
1386-
if (initialCodexProfile !== undefined) {
1387-
void refreshCodexInstructions().catch((err: unknown) => {
1388-
// Best-effort; agent still starts with cached/default instructions.
1389-
tuiLogger.warn("Codex instructions refresh failed: {error}", {
1390-
error: err instanceof Error ? err.message : String(err),
1391-
});
1392-
});
1393-
}
13941404
const initialCodexAccountId = config.providers.find(
13951405
(p) => p.name === config.providerName,
13961406
)?.codexAccountId;

0 commit comments

Comments
 (0)