Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3b58556
Add Codex usage indicator
lintowe May 4, 2026
bdedbd7
Preserve cached Codex usage on empty updates
lintowe May 4, 2026
880235a
Preserve fallback Codex rate limit state
lintowe May 4, 2026
c9a9be5
Sort Codex usage windows for display
lintowe May 5, 2026
36c3043
Handle direct Codex rate limit notifications
lintowe May 6, 2026
ee69117
Avoid Codex usage polling threads
lintowe May 6, 2026
0f373fe
Read Codex usage without opening threads
lintowe May 6, 2026
56e1f8c
Support named Codex rate limit buckets
lintowe May 6, 2026
ca86c15
Tighten Codex usage payload detection
lintowe May 6, 2026
9a87294
Fix codex usage rate limit fallback
lintowe May 7, 2026
4fa463f
Simplify Codex usage cache handling
lintowe May 7, 2026
0d1e852
Hide internal Codex usage comparator
lintowe May 8, 2026
c0b08c3
Share Codex usage provider resolution
lintowe May 8, 2026
06df77a
Respect locked provider fallback
lintowe May 8, 2026
5af6aa1
Preserve provider instance fallback ids
lintowe May 8, 2026
57d893a
Merge branch 'main' into feat/provider-usage-indicator
lintowe May 8, 2026
3d4194f
Align usage provider resolution
lintowe May 8, 2026
f6c1919
Refresh usage lock memo inputs
lintowe May 8, 2026
eacd735
Merge branch 'main' into feat/provider-usage-indicator
lintowe May 8, 2026
1667202
Merge branch 'main' into feat/provider-usage-indicator
lintowe May 8, 2026
d62e76a
Merge remote-tracking branch 'origin/main' into pr2484-bugbot-brancht…
lintowe May 9, 2026
a97e7c2
Merge branch 'main' into feat/provider-usage-indicator
lintowe May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import * as DesktopClientSettings from "./DesktopClientSettings.ts";

const clientSettings: ClientSettings = {
autoOpenPlanSidebar: false,
codexUsageIndicatorMode: "five-hour",
confirmThreadArchive: true,
confirmThreadDelete: false,
dismissedProviderUpdateNotificationKeys: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ function createProviderServiceHarness(
continuationKey: `${providerName}:instance:${instanceId}`,
},
}),
getCodexUsage: () => Effect.succeed(null),
rollbackConversation,
get streamEvents() {
return Stream.fromPubSub(runtimeEventPubSub);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ describe("ProviderCommandReactor", () => {
},
});
},
getCodexUsage: () => Effect.succeed(null),
rollbackConversation: () => unsupported(),
get streamEvents() {
return Stream.fromPubSub(runtimeEventPubSub);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ function createProviderServiceHarness() {
},
});
},
getCodexUsage: () => Effect.succeed(null),
rollbackConversation: () => unsupported(),
get streamEvents() {
return Stream.fromPubSub(runtimeEventPubSub);
Expand Down
213 changes: 212 additions & 1 deletion apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
import { it, vi } from "@effect/vitest";

import * as Context from "effect/Context";
import * as DateTime from "effect/DateTime";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as Fiber from "effect/Fiber";
Expand All @@ -33,6 +34,7 @@ import * as Schema from "effect/Schema";
import * as Scope from "effect/Scope";
import * as Stream from "effect/Stream";
import * as CodexErrors from "effect-codex-app-server/errors";
import type * as EffectCodexSchema from "effect-codex-app-server/schema";

import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
Expand Down Expand Up @@ -103,6 +105,15 @@ class FakeCodexRuntime implements CodexSessionRuntimeShape {
}),
);

public readonly readAccountRateLimitsImpl = vi.fn(
(): Promise<EffectCodexSchema.V2GetAccountRateLimitsResponse> =>
Promise.resolve({
rateLimits: {
primary: { usedPercent: 25, windowDurationMins: 300 },
},
}),
);

public readonly respondToRequestImpl = vi.fn(
(_requestId: ApprovalRequestId, _decision: ProviderApprovalDecision): Promise<void> =>
Promise.resolve(undefined),
Expand Down Expand Up @@ -141,6 +152,8 @@ class FakeCodexRuntime implements CodexSessionRuntimeShape {
return Effect.promise(() => this.rollbackThreadImpl(numTurns));
}

readAccountRateLimits = Effect.promise(() => this.readAccountRateLimitsImpl());

respondToRequest(requestId: ApprovalRequestId, decision: ProviderApprovalDecision) {
return Effect.promise(() => this.respondToRequestImpl(requestId, decision));
}
Expand All @@ -160,16 +173,20 @@ class FakeCodexRuntime implements CodexSessionRuntimeShape {
}
}

function makeRuntimeFactory() {
function makeRuntimeFactory(factoryOptions?: {
readonly configureRuntime?: (runtime: FakeCodexRuntime) => void;
}) {
const runtimes: Array<FakeCodexRuntime> = [];
const factory = vi.fn((options: CodexSessionRuntimeOptions) => {
const runtime = new FakeCodexRuntime(options);
factoryOptions?.configureRuntime?.(runtime);
runtimes.push(runtime);
return Effect.succeed(runtime);
});

return {
factory,
runtimes,
get lastRuntime(): FakeCodexRuntime | undefined {
return runtimes.at(-1);
},
Expand Down Expand Up @@ -359,6 +376,200 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
}),
);

it.effect("reads and normalizes account rate limits through the active runtime", () =>
Effect.gen(function* () {
const adapter = yield* CodexAdapter;
yield* adapter.startSession({
provider: ProviderDriverKind.make("codex"),
threadId: asThreadId("usage-thread"),
runtimeMode: "full-access",
});
const runtime = sessionRuntimeFactory.lastRuntime;
assert.ok(runtime);
runtime.readAccountRateLimitsImpl.mockResolvedValueOnce({
rateLimits: {
primary: { usedPercent: 30, windowDurationMins: 300 },
secondary: { usedPercent: 80, windowDurationMins: 10_080 },
},
});

const snapshot = yield* adapter.readCodexUsage!();

assert.equal(runtime.readAccountRateLimitsImpl.mock.calls.length, 1);
assert.deepStrictEqual(
snapshot?.windows.map((window) => ({
kind: window.kind,
remainingPercent: window.remainingPercent,
})),
[
{ kind: "five-hour", remainingPercent: 70 },
{ kind: "weekly", remainingPercent: 20 },
],
);
}),
);

it.effect("keeps cached account rate limits when an active read has no displayable windows", () =>
Effect.gen(function* () {
const adapter = yield* CodexAdapter;
yield* adapter.startSession({
provider: ProviderDriverKind.make("codex"),
threadId: asThreadId("usage-cache-thread"),
runtimeMode: "full-access",
});
const runtime = sessionRuntimeFactory.lastRuntime;
assert.ok(runtime);
runtime.readAccountRateLimitsImpl.mockResolvedValueOnce({
rateLimits: {
primary: { usedPercent: 45, windowDurationMins: 300 },
},
});
yield* adapter.readCodexUsage!();
runtime.readAccountRateLimitsImpl.mockResolvedValueOnce({
rateLimits: {},
});

const snapshot = yield* adapter.readCodexUsage!();

assert.equal(runtime.readAccountRateLimitsImpl.mock.calls.length, 2);
assert.equal(snapshot?.source, "cache");
assert.deepStrictEqual(snapshot?.windows[0], {
kind: "five-hour",
usedPercent: 45,
remainingPercent: 55,
resetsAt: null,
windowDurationMins: 300,
});
}),
);

it.effect("caches direct account rate-limit notification snapshots", () =>
Effect.gen(function* () {
const adapter = yield* CodexAdapter;
yield* adapter.startSession({
provider: ProviderDriverKind.make("codex"),
threadId: asThreadId("usage-notification-thread"),
runtimeMode: "full-access",
});
const runtime = sessionRuntimeFactory.lastRuntime;
assert.ok(runtime);
const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);

yield* runtime.emit({
id: asEventId("evt-rate-limit-direct"),
kind: "notification",
provider: ProviderDriverKind.make("codex"),
createdAt: DateTime.formatIso(yield* DateTime.now),
method: "account/rateLimits/updated",
threadId: asThreadId("usage-notification-thread"),
payload: {
primary: { usedPercent: 33, windowDurationMins: 300 },
},
} satisfies ProviderEvent);
const firstEvent = yield* Fiber.join(firstEventFiber);
runtime.readAccountRateLimitsImpl.mockResolvedValueOnce({
rateLimits: {},
});

const snapshot = yield* adapter.readCodexUsage!();

assert.equal(firstEvent._tag, "Some");
assert.equal(snapshot?.source, "cache");
assert.deepStrictEqual(snapshot?.windows[0], {
kind: "five-hour",
usedPercent: 33,
remainingPercent: 67,
resetsAt: null,
windowDurationMins: 300,
});
}),
);

it.effect("reads account rate limits before a Codex thread session exists", () =>
Effect.gen(function* () {
const adapter = yield* CodexAdapter;
yield* adapter.stopAll();
const snapshot = yield* adapter.readCodexUsage!();
const runtime = sessionRuntimeFactory.lastRuntime;

assert.ok(runtime);
assert.equal(runtime.options.threadId, asThreadId("codex-usage"));
assert.equal(runtime.startImpl.mock.calls.length, 0);
assert.equal(runtime.readAccountRateLimitsImpl.mock.calls.length, 1);
assert.equal(runtime.closeImpl.mock.calls.length, 1);
assert.deepStrictEqual(snapshot?.windows[0], {
kind: "five-hour",
usedPercent: 25,
remainingPercent: 75,
resetsAt: null,
windowDurationMins: 300,
});
}),
);

it.effect(
"keeps cached account rate limits when a no-session read has no displayable windows",
() => {
const isolatedRuntimeFactory = makeRuntimeFactory({
configureRuntime: (runtime) => {
if (runtime.options.threadId === asThreadId("codex-usage")) {
runtime.readAccountRateLimitsImpl.mockResolvedValue({
rateLimits: {},
});
}
},
});
const isolatedLayer = Layer.effect(
CodexAdapter,
Effect.gen(function* () {
const codexConfig = Schema.decodeSync(CodexSettings)({});
return yield* makeCodexAdapter(codexConfig, {
makeRuntime: isolatedRuntimeFactory.factory,
});
}),
).pipe(
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), process.cwd())),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(providerSessionDirectoryTestLayer),
Layer.provideMerge(NodeServices.layer),
);

return Effect.gen(function* () {
const adapter = yield* CodexAdapter;
yield* adapter.startSession({
provider: ProviderDriverKind.make("codex"),
threadId: asThreadId("usage-stopped-cache-thread"),
runtimeMode: "full-access",
});
const runtime = isolatedRuntimeFactory.lastRuntime;
assert.ok(runtime);
runtime.readAccountRateLimitsImpl.mockResolvedValueOnce({
rateLimits: {
primary: { usedPercent: 25, windowDurationMins: 300 },
},
});
yield* adapter.readCodexUsage!();
yield* adapter.stopAll();

const snapshot = yield* adapter.readCodexUsage!();
const usageRuntime = isolatedRuntimeFactory.lastRuntime;

assert.ok(usageRuntime);
assert.equal(usageRuntime.options.threadId, asThreadId("codex-usage"));
assert.equal(usageRuntime.startImpl.mock.calls.length, 0);
assert.equal(usageRuntime.readAccountRateLimitsImpl.mock.calls.length, 1);
assert.equal(snapshot?.source, "cache");
assert.deepStrictEqual(snapshot?.windows[0], {
kind: "five-hour",
usedPercent: 25,
remainingPercent: 75,
resetsAt: null,
windowDurationMins: 300,
});
}).pipe(Effect.provide(isolatedLayer));
},
);

it.effect("maps codex model options for the adapter's bound custom instance id", () => {
const customInstanceId = ProviderInstanceId.make("codex_personal");
const customRuntimeFactory = makeRuntimeFactory();
Expand Down
Loading
Loading