mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 02:33:53 +00:00
* feat(codex): scope app-server rate limits to the physical client Replace the process-global rate-limit cache with a WeakMap keyed by the physical app-server client, tracking per-limitId revisions. Rolling account/rateLimits/updated notifications merge sparsely per the protocol contract (credits/individualLimit/planType survive nulls), and usage-limit errors only trust a snapshot for auth-profile blocking when the same client observed a primary update during the failing turn's startup. Fixes cross-client rate-limit bleed in usage-limit error messages. A new client-runtime module installs the account/chatgptAuthTokens/refresh handler and the rate-limit observer once per physical client, replacing per-start inline handlers in shared-client, run-attempt, and side-question. * refactor(codex): split thread/resume subscription safety into thread-resume Move the thread/resume request out of thread-lifecycle into a dedicated thread-resume module that retires the exact physical client when resume acceptance is indeterminate: only a structured RPC rejection proves Codex holds no subscription, so any other failure abandons the client instead of returning it to the shared pool. Resume responses naming a different thread now fail closed (assertCodexThreadResumeSubscription), and the fresh-thread fallback requires a released subscription unless the resume was a proven RPC rejection. * refactor(codex): replace client-factory positional DI with shared-client factory Delete the lazy positional-argument CodexAppServerClientFactory and use the options-object factory type exported from shared-client. Callers in run-attempt, compact, bounded-turn, provider-capabilities, and the web-search provider now default to getLeasedSharedCodexAppServerClient directly; the lazy indirection was ineffective because those modules already import shared-client statically. * feat(codex): route app-server turn traffic through a keyed turn router Install one turn router per physical app-server client and replace the broad per-attempt notification/request fanout with explicit per-thread routes. Attempt startup reserves the thread route (before thread/resume on the resume path, so early notifications buffer instead of racing), run-attempt activates it with receive-time, queued, and request handlers, arms the route before turn/start, binds the accepted turn id to flush buffered traffic in wire order, and releases the route on cleanup. Requests for a pending turn wait for binding instead of being auto-declined, native turn completion waits use route state instead of scanning buffered notifications, and correlation readers now match the canonical v2 wire shapes only (top-level threadId, nested turn.id). The unscoped response-delta lease-count attribution and its client API are deleted along with the retired correlation predicates. * test(codex): reset the shared binding store between thread-lifecycle tests SQLite bindings are keyed by session identity rather than the per-test temp dir, so earlier tests leaked resumable threads into fresh-start expectations. The old silent resume-failure fallback masked the leak; subscription safety surfaces it. * test(codex): reset the binding store between delivery-hint iterations The loop reuses one session identity across iterations, so the previous iteration's thread would resume against a harness that cannot serve it.
107 lines
3.2 KiB
TypeScript
107 lines
3.2 KiB
TypeScript
/**
|
|
* Shared Codex app-server test helpers for model fixtures and in-memory client
|
|
* transports.
|
|
*/
|
|
import { EventEmitter } from "node:events";
|
|
import { PassThrough, Writable } from "node:stream";
|
|
import type { Model } from "openclaw/plugin-sdk/llm";
|
|
import { vi } from "vitest";
|
|
import { CodexAppServerClient } from "./client.js";
|
|
import type { CodexAppServerClientFactory, CodexAppServerClientOptions } from "./shared-client.js";
|
|
|
|
/** Positional naked-client injection contract confined to tests. */
|
|
export type CodexTestAppServerClientFactory = (
|
|
startOptions?: CodexAppServerClientOptions["startOptions"],
|
|
authProfileId?: string,
|
|
agentDir?: string,
|
|
config?: CodexAppServerClientOptions["config"],
|
|
options?: CodexAppServerClientOptions,
|
|
) => Promise<CodexAppServerClient>;
|
|
|
|
/** Adapts a positional test factory to the production options-object contract. */
|
|
export function adaptCodexTestClientFactory(
|
|
factory: CodexTestAppServerClientFactory,
|
|
): CodexAppServerClientFactory {
|
|
return (options) =>
|
|
factory(
|
|
options?.startOptions,
|
|
options?.authProfileId ?? undefined,
|
|
options?.agentDir,
|
|
options?.config,
|
|
options,
|
|
);
|
|
}
|
|
|
|
/** Builds a representative Codex-capable model fixture for app-server tests. */
|
|
export function createCodexTestModel(provider = "openai", input = ["text"]): Model {
|
|
return {
|
|
id: "gpt-5.4-codex",
|
|
name: "gpt-5.4-codex",
|
|
provider,
|
|
api: "openai-chatgpt-responses",
|
|
input,
|
|
reasoning: true,
|
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
contextWindow: 128_000,
|
|
maxTokens: 8_000,
|
|
} as Model;
|
|
}
|
|
|
|
/** Creates an in-memory Codex app-server client harness with writable stdout frames. */
|
|
export function createClientHarness() {
|
|
const stdout = new PassThrough();
|
|
const writes: string[] = [];
|
|
let stdinDestroyed = false;
|
|
let exitEmitted = false;
|
|
let emitProcessExit: () => void = () => undefined;
|
|
type HarnessProcess = EventEmitter & {
|
|
stdin: Writable;
|
|
stdout: PassThrough;
|
|
stderr: PassThrough;
|
|
killed: boolean;
|
|
kill: (signal?: NodeJS.Signals) => unknown;
|
|
};
|
|
const stdin = new Writable({
|
|
write(chunk, _encoding, callback) {
|
|
writes.push(chunk.toString());
|
|
callback();
|
|
},
|
|
});
|
|
const destroyStdin = stdin.destroy.bind(stdin);
|
|
stdin.destroy = ((error?: Error) => {
|
|
stdinDestroyed = true;
|
|
const result = destroyStdin(error);
|
|
if (!exitEmitted) {
|
|
exitEmitted = true;
|
|
// Let stdin surface pipe errors before the harness emits the fake child exit.
|
|
// Otherwise close-reason tests can race EPIPE against a synthetic clean exit.
|
|
setImmediate(emitProcessExit);
|
|
}
|
|
return result;
|
|
}) as typeof stdin.destroy;
|
|
const process: HarnessProcess = Object.assign(new EventEmitter(), {
|
|
stdin,
|
|
stdout,
|
|
stderr: new PassThrough(),
|
|
killed: false,
|
|
kill: vi.fn((_signal?: NodeJS.Signals) => {
|
|
process.killed = true;
|
|
}),
|
|
});
|
|
emitProcessExit = () => {
|
|
process.emit("exit", 0, null);
|
|
};
|
|
const client = CodexAppServerClient.fromTransportForTests(process);
|
|
return {
|
|
client,
|
|
process,
|
|
writes,
|
|
get stdinDestroyed() {
|
|
return stdinDestroyed;
|
|
},
|
|
send(message: unknown) {
|
|
stdout.write(`${JSON.stringify(message)}\n`);
|
|
},
|
|
};
|
|
}
|