mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-18 18:21:40 +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.
125 lines
4.2 KiB
TypeScript
125 lines
4.2 KiB
TypeScript
// Codex tests cover physical-client rate-limit snapshot ownership and rolling merges.
|
|
import { describe, expect, it } from "vitest";
|
|
import type { CodexAppServerClient } from "./client.js";
|
|
import {
|
|
mergeCodexRateLimitsUpdate,
|
|
readCodexRateLimitsRevision,
|
|
readRecentCodexRateLimits,
|
|
rememberCodexRateLimitsRead,
|
|
} from "./rate-limit-cache.js";
|
|
|
|
function clientIdentity(): CodexAppServerClient {
|
|
return {} as unknown as CodexAppServerClient;
|
|
}
|
|
|
|
describe("Codex rate-limit cache", () => {
|
|
it("isolates snapshots by physical client", () => {
|
|
const first = clientIdentity();
|
|
const second = clientIdentity();
|
|
expect(readCodexRateLimitsRevision(first)).toBe(0);
|
|
rememberCodexRateLimitsRead(first, { rateLimits: { limitId: "first" } }, 100);
|
|
rememberCodexRateLimitsRead(second, { rateLimits: { limitId: "second" } }, 200);
|
|
expect(readCodexRateLimitsRevision(first, "first")).toBe(1);
|
|
expect(readCodexRateLimitsRevision(second, "second")).toBe(1);
|
|
|
|
expect(readRecentCodexRateLimits(first, { nowMs: 250 })).toEqual({
|
|
rateLimits: { limitId: "first" },
|
|
});
|
|
expect(readRecentCodexRateLimits(second, { nowMs: 250 })).toEqual({
|
|
rateLimits: { limitId: "second" },
|
|
});
|
|
expect(readRecentCodexRateLimits(first, { nowMs: 301, maxAgeMs: 200 })).toBeUndefined();
|
|
expect(readRecentCodexRateLimits(second, { nowMs: 301, maxAgeMs: 200 })).toEqual({
|
|
rateLimits: { limitId: "second" },
|
|
});
|
|
});
|
|
|
|
it("merges sparse rolling updates without clearing account metadata", () => {
|
|
const client = clientIdentity();
|
|
const codexSnapshot = {
|
|
limitId: "codex",
|
|
limitName: "Codex",
|
|
primary: { usedPercent: 10, windowDurationMins: 300, resetsAt: 1000 },
|
|
secondary: { usedPercent: 20, windowDurationMins: 10_080, resetsAt: 2000 },
|
|
credits: { hasCredits: true, unlimited: false, balance: "5" },
|
|
individualLimit: {
|
|
limit: "25000",
|
|
used: "8000",
|
|
remainingPercent: 68,
|
|
resetsAt: 3000,
|
|
},
|
|
planType: "pro",
|
|
rateLimitReachedType: "rate_limit_reached",
|
|
};
|
|
const otherSnapshot = {
|
|
limitId: "codex_other",
|
|
limitName: "Other",
|
|
primary: { usedPercent: 30, windowDurationMins: 60, resetsAt: 4000 },
|
|
secondary: null,
|
|
credits: null,
|
|
individualLimit: null,
|
|
planType: "pro",
|
|
rateLimitReachedType: null,
|
|
};
|
|
rememberCodexRateLimitsRead(client, {
|
|
rateLimits: codexSnapshot,
|
|
rateLimitsByLimitId: { codex: codexSnapshot, codex_other: otherSnapshot },
|
|
});
|
|
|
|
mergeCodexRateLimitsUpdate(client, {
|
|
rateLimits: {
|
|
limitId: null,
|
|
limitName: null,
|
|
primary: { usedPercent: 90, windowDurationMins: 300, resetsAt: 5000 },
|
|
secondary: null,
|
|
credits: null,
|
|
individualLimit: null,
|
|
planType: null,
|
|
rateLimitReachedType: null,
|
|
},
|
|
});
|
|
mergeCodexRateLimitsUpdate(client, {
|
|
rateLimits: {
|
|
limitId: "codex_other",
|
|
limitName: null,
|
|
primary: { usedPercent: 75, windowDurationMins: 60, resetsAt: 6000 },
|
|
secondary: null,
|
|
credits: null,
|
|
individualLimit: null,
|
|
planType: null,
|
|
rateLimitReachedType: null,
|
|
},
|
|
});
|
|
expect(readCodexRateLimitsRevision(client)).toBe(2);
|
|
expect(readCodexRateLimitsRevision(client, "codex_other")).toBe(2);
|
|
|
|
const mergedCodexSnapshot = {
|
|
limitId: "codex",
|
|
limitName: null,
|
|
primary: { usedPercent: 90, windowDurationMins: 300, resetsAt: 5000 },
|
|
secondary: null,
|
|
credits: codexSnapshot.credits,
|
|
individualLimit: codexSnapshot.individualLimit,
|
|
planType: "pro",
|
|
rateLimitReachedType: null,
|
|
};
|
|
const mergedOtherSnapshot = {
|
|
limitId: "codex_other",
|
|
limitName: null,
|
|
primary: { usedPercent: 75, windowDurationMins: 60, resetsAt: 6000 },
|
|
secondary: null,
|
|
credits: codexSnapshot.credits,
|
|
individualLimit: codexSnapshot.individualLimit,
|
|
planType: "pro",
|
|
rateLimitReachedType: null,
|
|
};
|
|
expect(readRecentCodexRateLimits(client)).toEqual({
|
|
rateLimits: mergedCodexSnapshot,
|
|
rateLimitsByLimitId: {
|
|
codex: mergedCodexSnapshot,
|
|
codex_other: mergedOtherSnapshot,
|
|
},
|
|
});
|
|
});
|
|
});
|