mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-26 15:31:14 +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.
47 lines
1.7 KiB
TypeScript
47 lines
1.7 KiB
TypeScript
/**
|
|
* Correlates Codex app-server notifications with the active thread/turn so
|
|
* projectors can ignore global or stale events without losing diagnostics.
|
|
*/
|
|
import { isJsonObject, type JsonObject, type JsonValue } from "./protocol.js";
|
|
|
|
/** Returns true when a notification payload belongs to the exact active thread and turn. */
|
|
export function isCodexNotificationForTurn(
|
|
value: JsonValue | undefined,
|
|
threadId: string,
|
|
turnId: string,
|
|
): boolean {
|
|
if (!isJsonObject(value)) {
|
|
return false;
|
|
}
|
|
return (
|
|
readCodexNotificationThreadId(value) === threadId &&
|
|
readCodexNotificationTurnId(value) === turnId
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Reads a thread id from canonical top-level or nested thread payloads.
|
|
* The generated v2 schemas require top-level `threadId` on turn/item-scoped
|
|
* notifications and define `Turn` without one, so `turn.threadId` is not a
|
|
* wire shape and is deliberately not read here.
|
|
*/
|
|
export function readCodexNotificationThreadId(record: JsonObject): string | undefined {
|
|
const thread = isJsonObject(record.thread) ? record.thread : undefined;
|
|
return readString(record, "threadId") ?? (thread ? readString(thread, "id") : undefined);
|
|
}
|
|
|
|
/** Reads a turn id from either top-level notification params or nested turn payloads. */
|
|
export function readCodexNotificationTurnId(record: JsonObject): string | undefined {
|
|
return readNestedTurnId(record) ?? readString(record, "turnId");
|
|
}
|
|
|
|
function readNestedTurnId(record: JsonObject): string | undefined {
|
|
const turn = record.turn;
|
|
return isJsonObject(turn) ? readString(turn, "id") : undefined;
|
|
}
|
|
|
|
function readString(record: JsonObject, key: string): string | undefined {
|
|
const value = record[key];
|
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
}
|