Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e0b3cfe
fix(web): image and file chips underline their name on hover again (#…
flamboh Oct 28, 2026
2de4762
fix(mobile): tab bar hides with the push, logo fits the iPad sidebar,…
juliusmarminge Oct 28, 2026
91992bd
fix(web): update dialog shows one row per machine (#26210)
t3dotgg Oct 28, 2026
7c6446b
chore(mobile): e2e review skips behaviour confirmed as intended
juliusmarminge Oct 28, 2026
36aea58
fix(mobile): trust user-installed Android CA certificates (#25730)
juliusmarminge Oct 28, 2026
8947f3e
fix(web): server rows keep their source visible, cmd-click opens your…
t3dotgg Oct 28, 2026
f120c51
fix(mobile): list bullets keep a gap before their text at every text …
juliusmarminge Oct 28, 2026
949929d
fix(mobile): Android split content keeps a gap at the screen edge (#2…
juliusmarminge Oct 28, 2026
c2fbf0a
feat(server,web,mobile): thread links name their environment and neve…
juliusmarminge Oct 28, 2026
fe55faf
fix(clients): # suggestions put linked pull requests first for text q…
juliusmarminge Oct 28, 2026
49a9e80
fix(terminal): keep shared terminal grids in sync (#24352)
juliusmarminge Oct 28, 2026
0c80751
feat: sign in to GitHub through the T3 Code for GitHub app [internal …
juliusmarminge Oct 28, 2026
ff71186
test(mobile): vp run mobile:behavior grades app behavior on pool devices
juliusmarminge Oct 28, 2026
9910c0e
perf: reduce remote reconnect and command delays (#25754)
Bil0000 Oct 28, 2026
822fc15
fix(relay): deploys plan again after the Worker lost its default expo…
juliusmarminge Oct 28, 2026
3ef1610
fix(web): compaction row says "Compacting for 12s" instead of "after …
t3dotgg Oct 28, 2026
9a8bc04
test(task-cache): remove flaky native runner integration test
juliusmarminge Oct 28, 2026
c4606e5
fix(server): Codex shadow homes share thread writer locks with the ma…
t3dotgg Oct 28, 2026
28da8c8
fix(mobile): sidebar connection status uses the header's free width (…
juliusmarminge Oct 28, 2026
9c6429d
fix(web): threads without a project start on the primary environment …
juliusmarminge Oct 28, 2026
cafdcda
fix(clients): give image attachment chips a blue accent (#26306)
Yash-Singh1 Oct 28, 2026
afc2334
fix(web): prevent image thumbnails from warping during repaints (#26308)
Yash-Singh1 Oct 28, 2026
49582b7
test(web): attachment font and large-text preview tests wait for the …
juliusmarminge Oct 28, 2026
8031991
fix(opencode): subagent sessions follow runtime mode changes (#26302)
juliusmarminge Oct 28, 2026
1f8530e
feat(web): pending approvals show on their timeline row (#26255)
maria-rcks Oct 28, 2026
9b052a2
fix(web): show the platform modifier in the send tooltip (#22436)
UtkarshUsername Oct 28, 2026
feb49e7
fix(web): let theme color pickers use popover glass (#22348)
UtkarshUsername Oct 28, 2026
9bc0419
fix(relay): concurrent link requests can no longer exceed the managed…
juliusmarminge Oct 28, 2026
003ccd3
fix(client-runtime): pairing links can no longer add routes to a save…
juliusmarminge Oct 28, 2026
feb2e32
fix(web): align sidebar button geometry with shared tokens (#22287)
UtkarshUsername Oct 28, 2026
c0f9e36
fix(mobile): pairing deep links can no longer target a saved environm…
juliusmarminge Oct 28, 2026
8e76f94
feat(relay): T3 Chat Pro and Premier accounts get more tunnels (#25931)
t3dotgg Oct 28, 2026
0deb383
test(mobile): pairing link test runs the app's real link parsing (#26…
juliusmarminge Oct 28, 2026
50d3a31
fix: guests see the screenshots the agent shows in its replies (#26323)
t3dotgg Oct 28, 2026
a9418a1
ci(release): releases no longer reinstall Vite+ in every job (#26305)
t3dotgg Oct 28, 2026
f6d3700
ci: server test shards survive a throttled task cache (#26332)
t3dotgg Oct 28, 2026
4e4d4ba
feat(web): boring sidebar groups threads into project folders (#21039)
t3dotgg Oct 28, 2026
9ebca5f
fix(relay): tunnel limit counts abandoned reprovisions and frees fail…
juliusmarminge Oct 28, 2026
cadbdaf
test(web): attachment preview tests wait long enough for cold workers…
t3dotgg Oct 28, 2026
2349c81
refactor(mobile): app link parsing reads top to bottom with pipe (#26…
juliusmarminge Oct 28, 2026
ce66aab
ci(release): Windows CLI builds retry the Node.js download (#26345)
t3dotgg Oct 28, 2026
990e9e0
fix(web): preserve handoffs before superseded attempt folds (#26333)
Yash-Singh1 Oct 28, 2026
441a4eb
feat(web): open tool call diffs in the diff panel (#26304)
Yash-Singh1 Oct 28, 2026
cfc8be9
ci: a stalled Node.js download retries after 2 minutes, not 10 (#26343)
t3dotgg Oct 28, 2026
c3f5e36
fix(server): avoid replaying stale Codex usage limits (#26331)
Yash-Singh1 Oct 28, 2026
ff42480
feat(web): mark PR files viewed with shift-click (#26349)
Yash-Singh1 Oct 28, 2026
b33849e
Merge upstream/main ff424802ed into coder-only
henrychu04 Oct 29, 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
30 changes: 22 additions & 8 deletions apps/server/src/mcp/OrchestratorMcpService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
CommandId,
type EnvironmentId,
type RunId,
isProviderAvailable,
MessageId,
Expand Down Expand Up @@ -747,13 +748,14 @@ function threadPullRequests(
}

function listItemFromShell(
environmentId: EnvironmentId,
shell: OrchestrationV2ThreadShell,
nowMs: number,
): OrchestratorMcpThreadListItem {
return {
threadId: shell.id,
title: shell.title,
link: formatThreadLink(shell.id, shell.title),
link: formatThreadLink({ environmentId, threadId: shell.id }),
createdBy: shell.createdBy,
creationSource: shell.creationSource,
status: shell.activityRunStatus ?? shell.status,
Expand All @@ -775,6 +777,7 @@ function listItemFromShell(
}

function threadDetail(
environmentId: EnvironmentId,
projection: Pick<OrchestrationV2ThreadProjection, "thread" | "runs" | "runtimeRequests">,
itemCount: number,
shell: OrchestrationV2ThreadShell,
Expand All @@ -786,7 +789,7 @@ function threadDetail(
threadId: projection.thread.id,
projectId: projection.thread.projectId,
title: projection.thread.title,
link: formatThreadLink(projection.thread.id, projection.thread.title),
link: formatThreadLink({ environmentId, threadId: projection.thread.id }),
createdBy: projection.thread.createdBy,
creationSource: projection.thread.creationSource,
status: active?.status ?? latest?.status ?? "idle",
Expand Down Expand Up @@ -1517,7 +1520,10 @@ const make = Effect.gen(function* () {
const result = {
taskId: task.id,
childThreadId: task.childThreadId,
link: formatThreadLink(task.childThreadId, childControls.thread.title),
link: formatThreadLink({
environmentId: scope.environmentId,
threadId: task.childThreadId,
}),
childRunId: childRun?.id ?? null,
childNodeId: task.id,
status,
Expand Down Expand Up @@ -1673,7 +1679,9 @@ const make = Effect.gen(function* () {
return {
taskId: task.id,
childThreadId: null,
...(task.remoteChild === undefined ? {} : { remoteChild: task.remoteChild }),
...(task.remoteChild === undefined
? {}
: { remoteChild: task.remoteChild, link: formatThreadLink(task.remoteChild) }),
childRunId: null,
childNodeId: task.id,
status,
Expand Down Expand Up @@ -2738,7 +2746,7 @@ const make = Effect.gen(function* () {
);
return {
threadId,
link: formatThreadLink(threadId, projection.thread.title),
link: formatThreadLink({ environmentId: scope.environmentId, threadId }),
runId: run?.id ?? null,
status: run?.status ?? "idle",
title: projection.thread.title,
Expand Down Expand Up @@ -2791,7 +2799,7 @@ const make = Effect.gen(function* () {
return {
projectId,
currentThreadId: parent?.thread.id ?? null,
threads: page.map((shell) => listItemFromShell(shell, nowMs)),
threads: page.map((shell) => listItemFromShell(scope.environmentId, shell, nowMs)),
nextCursor,
total: filtered.length,
} satisfies OrchestratorMcpThreadListResult;
Expand Down Expand Up @@ -2871,7 +2879,13 @@ const make = Effect.gen(function* () {
}
}
return {
thread: threadDetail(target, timeline.totalItems, shell, yield* Clock.currentTimeMillis),
thread: threadDetail(
scope.environmentId,
target,
timeline.totalItems,
shell,
yield* Clock.currentTimeMillis,
),
recentRuns: target.runs
.toSorted((left, right) => right.ordinal - left.ordinal)
.slice(0, input.runLimit ?? DEFAULT_THREAD_RUN_LIMIT)
Expand Down Expand Up @@ -2930,7 +2944,7 @@ const make = Effect.gen(function* () {
);
return {
threadId: input.threadId,
link: formatThreadLink(input.threadId, target.thread.title),
link: formatThreadLink({ environmentId: scope.environmentId, threadId: input.threadId }),
messageId,
runId: result.run.id,
...(result.run.delegatedTaskId === undefined ? {} : { taskId: result.run.delegatedTaskId }),
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/mcp/toolkits/project/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ it.effect(
expect(belowCeiling.result).toMatchObject({
projectId: belowCeiling.projectId,
modelSelection: belowCeiling.modelSelection,
link: `[Fix](t3-thread://v1/${belowCeiling.launched[0]?.threadId})`,
link: `<t3-thread://v2/environment/${belowCeiling.launched[0]?.threadId}>`,
});
expect(belowCeiling.launched[0]?.runtimeMode).toBe("approval-required");
expect(belowCeiling.launched[0]?.initialMessage?.senderThreadId).toBeUndefined();
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/mcp/toolkits/project/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ export const layer = McpToolAccess.toLayer(ProjectToolkit, {
const run = result.projection.runs.find((run) => run.userMessageId === messageId);
return {
threadId: thread.id,
link: formatThreadLink(thread.id, thread.title),
link: formatThreadLink({ environmentId: scope.environmentId, threadId: thread.id }),
projectId: thread.projectId,
modelSelection: thread.modelSelection,
runId: run?.id ?? null,
Expand Down
6 changes: 5 additions & 1 deletion apps/server/src/mcp/toolkits/thread/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import * as Effect from "effect/Effect";
import { modelSelectionCommandType } from "@t3tools/shared/model";
import { formatThreadLink } from "@t3tools/shared/threadLinks";

import * as McpInvocationContext from "../../McpInvocationContext.ts";
import * as McpToolAccess from "../../McpToolAccess.ts";
import {
dispatchFailure,
Expand Down Expand Up @@ -103,9 +104,11 @@ export const layer = McpToolAccess.toLayer(ThreadToolkit, {
const { task, threadId } = yield* scheduler
.runNow({ id: input.taskId })
.pipe(Effect.mapError(unavailable));
const { environmentId } = yield* McpInvocationContext.McpInvocationContext;
return {
taskId: task.id,
threadId,
...(threadId === null ? {} : { link: formatThreadLink({ environmentId, threadId }) }),
lastRunStatus: task.lastRunStatus,
runCount: task.runCount,
nextRunAt: task.nextRunAt,
Expand All @@ -115,14 +118,15 @@ export const layer = McpToolAccess.toLayer(ThreadToolkit, {
t3_thread_search: McpToolAccess.reads((input) =>
Effect.gen(function* () {
const { caller } = yield* readCaller();
const { environmentId } = yield* McpInvocationContext.McpInvocationContext;
const threadSearch = yield* ThreadSearch.ThreadSearch;
const { matches } = yield* threadSearch
.searchThreads({ ...input, projectId: input.projectId ?? caller?.projectId })
.pipe(Effect.mapError(unavailable));
return {
matches: matches.map((match) => ({
...match,
link: formatThreadLink(match.threadId, match.title),
link: formatThreadLink({ environmentId, threadId: match.threadId }),
})),
};
}),
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/mcp/toolkits/thread/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ const ScheduledTaskRunTool = Tool.make("run_scheduled_task_now", {
success: Schema.Struct({
taskId: ScheduledTaskId,
threadId: ScheduledTask.fields.threadId,
link: Schema.optional(Schema.String),
lastRunStatus: ScheduledTask.fields.lastRunStatus,
runCount: NonNegativeInt,
nextRunAt: ScheduledTask.fields.nextRunAt,
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4275,7 +4275,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (

yield* client.handleServerNotification("account/rateLimits/updated", (payload) =>
Effect.gen(function* () {
yield* Ref.update(rateLimitSnapshot, (previous) =>
const previous = yield* Ref.getAndUpdate(rateLimitSnapshot, (previous) =>
mergeCodexRateLimits(previous, payload.rateLimits),
);
const resetAt = codexUsageLimitResetAt(yield* Ref.get(rateLimitSnapshot));
Expand All @@ -4295,7 +4295,7 @@ export const makeCodexAdapterV2 = Effect.fn("makeCodexAdapterV2")(function* (
turnItem: updated,
});
}
const update = codexRateLimitsToUpdate(payload.rateLimits);
const update = codexRateLimitsToUpdate(payload.rateLimits, previous);
if (update && adapterOptions.onUsageLimits) {
const checkedAt = DateTime.formatIso(yield* DateTime.now);
yield* adapterOptions.onUsageLimits({ ...update, checkedAt });
Expand Down
9 changes: 9 additions & 0 deletions apps/server/src/orchestration-v2/WireProjection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,15 @@ describe("orchestration V2 wire projection", () => {
expect(projected).not.toHaveProperty("newStr");
expect(projected).toMatchObject({ fileName: "src/main.ts", additions: 3, deletions: 1 });
expect(item.diffStr).toBe("+new code");
expect(projectTurnItemForDetail(item)).toMatchObject({ diffStr: "+new code" });
expect(projectTurnItemForDetail(item)).not.toHaveProperty("oldStr");
expect(projectTurnItemForDetail(item)).not.toHaveProperty("newStr");
const { diffStr: _diff, ...contentOnly } = item;
expect(projectTurnItemForDetail(contentOnly)).toMatchObject({
oldStr: "old code",
newStr: "new code",
});
expect(projectTurnItemForWire(contentOnly)).not.toHaveProperty("newStr");
const failed = projectTurnItemForWire({
...item,
status: "failed",
Expand Down
14 changes: 13 additions & 1 deletion apps/server/src/orchestration-v2/WireProjection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,20 @@ export function projectTurnItemForDetail(item: OrchestrationV2TurnItem): Orchest
result:
item.result === null ? null : (truncateDetail(item.result, MAX_ON_DEMAND_BYTES) ?? null),
};
case "handoff":
case "file_change":
return {
...projectTurnItemForWire(item),
...(item.diffStr === undefined
? {}
: { diffStr: truncateDetail(item.diffStr, MAX_ON_DEMAND_BYTES) }),
...(!item.diffStr?.trim() && item.oldStr !== undefined
? { oldStr: truncateDetail(item.oldStr, MAX_ON_DEMAND_BYTES) }
: {}),
...(!item.diffStr?.trim() && item.newStr !== undefined
? { newStr: truncateDetail(item.newStr, MAX_ON_DEMAND_BYTES) }
: {}),
};
case "handoff":
return projectTurnItemForWire(item);
default:
return item;
Expand Down
40 changes: 40 additions & 0 deletions apps/server/src/provider/Drivers/CodexHomeLayout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,10 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => {
'{"models":["shared"]}\n',
);
yield* writeTextFile(path.join(sharedHome, "auth.json"), '{"shared":true}\n');
yield* writeTextFile(path.join(sharedHome, "auth.lock"), "");
yield* fileSystem.makeDirectory(shadowHome, { recursive: true });
yield* writeTextFile(path.join(shadowHome, "auth.json"), '{"shadow":true}\n');
yield* writeTextFile(path.join(shadowHome, "auth.lock"), "");
yield* fileSystem.symlink(
path.join(sharedHome, "models_cache.json"),
path.join(shadowHome, "models_cache.json"),
Expand All @@ -149,13 +151,17 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => {
.readLink(path.join(shadowHome, "auth.json"))
.pipe(Effect.result);
const authContents = yield* fileSystem.readFileString(path.join(shadowHome, "auth.json"));
const authLockLinkResult = yield* fileSystem
.readLink(path.join(shadowHome, "auth.lock"))
.pipe(Effect.result);

expect(sessionsTarget).toBe(path.join(sharedHome, "sessions"));
expect(configTarget).toBe(path.join(sharedHome, "config.toml"));
expect(mcpOauthLocksTarget).toBe(path.join(sharedHome, "mcp-oauth-locks"));
expect(modelsCacheExists).toBe(false);
expect(authLinkResult._tag).toBe("Failure");
expect(authContents).toContain("shadow");
expect(authLockLinkResult._tag).toBe("Failure");
}),
);

Expand Down Expand Up @@ -221,6 +227,40 @@ it.layer(NodeServices.layer)("CodexHomeLayout", (it) => {
}),
);

it.effect.skipIf(!symlinksSupported)(
"shares local thread writer locks once no writer still holds one",
() =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const sharedHome = yield* makeTempDir("t3code-codex-shared-");
const shadowRoot = yield* makeTempDir("t3code-codex-shadow-root-");
const shadowHome = path.join(shadowRoot, "shadow");
const shadowLocks = path.join(shadowHome, "thread-writer-locks");
const threadLock = path.join(shadowLocks, "019a0000-0000-7000-8000-000000000000.lock");

yield* writeTextFile(path.join(shadowLocks, ".coordination.lock"), "");
yield* writeTextFile(path.join(shadowLocks, ".DS_Store"), "");
yield* writeTextFile(threadLock, "");

const layout = yield* resolveCodexHomeLayout(
decodeCodexSettings({
homePath: sharedHome,
shadowHomePath: shadowHome,
}),
);

yield* materializeCodexShadowHome(layout);
expect(yield* fileSystem.exists(threadLock)).toBe(true);

yield* fileSystem.remove(threadLock);
yield* materializeCodexShadowHome(layout);
expect(yield* fileSystem.readLink(shadowLocks)).toBe(
path.join(sharedHome, "thread-writer-locks"),
);
}),
);

it.effect.skipIf(!symlinksSupported)(
"accepts Codex-created shadow-local runtime directories",
() =>
Expand Down
34 changes: 32 additions & 2 deletions apps/server/src/provider/Drivers/CodexHomeLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@ const KNOWN_SHARED_DIRECTORIES = [
"cache",
"logs",
"mcp-oauth-locks",
"thread-writer-locks",
] as const;

const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "models_cache.json"]);
// auth.lock guards the shadow home's private auth.json.
const PRIVATE_ENTRY_NAMES = new Set(["auth.json", "auth.lock", "models_cache.json"]);
// Finder maintains metadata separately for each directory, including shadow homes.
const SHADOW_LOCAL_ENTRY_NAMES = new Set(["log", "memories", "tmp", ".DS_Store"]);
// Shadow homes may create local runtime locks first; replace them with shared links because they guard shared state.
const REPLACEABLE_SHARED_RUNTIME_ENTRIES = new Set(["mcp-oauth-locks", ".sqlite-maintenance.lock"]);
const REPLACEABLE_SHARED_RUNTIME_ENTRIES = new Set([
"mcp-oauth-locks",
"thread-writer-locks",
".sqlite-maintenance.lock",
]);

function resolveHomePath(path: Path.Path, home: string, value: string | undefined): string {
const expanded =
Expand Down Expand Up @@ -253,6 +259,30 @@ const ensureSymlink = Effect.fn("CodexHomeLayout.ensureSymlink")(function* (inpu
});
}

if (input.entryName === "thread-writer-locks") {
const lockNames = yield* input.fileSystem.readDirectory(link).pipe(
Effect.catchTags({
PlatformError: (cause) =>
new CodexShadowHomeFileSystemError({
sharedHomePath: input.sharedHomePath,
effectiveHomePath: input.effectiveHomePath,
operation: "readDirectory",
path: link,
entryName: input.entryName,
cause,
}),
}),
);
// Codex deletes a thread lock on release, so one that is left may still guard a live
// writer. Keep the local directory until a later setup finds none.
if (lockNames.some((name) => name.endsWith(".lock") && name !== ".coordination.lock")) {
return yield* Effect.logWarning(
"Codex shadow home still has thread writer locks; it will share them on a later setup.",
{ path: link },
);
}
}

yield* input.fileSystem.remove(link, { recursive: true }).pipe(
Effect.catchTags({
PlatformError: (cause) =>
Expand Down
23 changes: 23 additions & 0 deletions apps/server/src/provider/codexUsageLimits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,29 @@ describe("codexRateLimitsToLimits", () => {
});

describe("codexRateLimitsToUpdate", () => {
it("does not republish cached windows over a newer account probe", () => {
const cached = {
primary: { usedPercent: 0, resetsAt: 1_784_000_000, windowDurationMins: 300 },
secondary: { usedPercent: 20, resetsAt: 1_784_500_000, windowDurationMins: 10080 },
};
expect(codexRateLimitsToUpdate(cached)?.windows).toHaveLength(2);
expect(codexRateLimitsToUpdate(structuredClone(cached), cached)).toBeUndefined();

const changed = { ...cached, secondary: { ...cached.secondary, usedPercent: 21 } };
expect(codexRateLimitsToUpdate(changed, cached)?.windows.map((window) => window.id)).toEqual([
"secondary",
]);
expect(codexRateLimitsToUpdate({ primary: cached.primary }, cached)).toBeUndefined();
expect(
codexRateLimitsToUpdate({ primary: { ...cached.primary, resetsAt: 1_784_001_000 } }, cached)
?.windows,
).toHaveLength(1);
expect(
codexRateLimitsToUpdate({ primary: { ...cached.primary, usedPercent: 76 } }, cached)
?.windows[0]?.usedPercent,
).toBe(76);
});

it("carries only the windows the notification names", () => {
expect(
codexRateLimitsToUpdate({
Expand Down
14 changes: 13 additions & 1 deletion apps/server/src/provider/codexUsageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,20 @@ export function codexRateLimitsToLimits(input: {

export function codexRateLimitsToUpdate(
snapshot: CodexRateLimitSnapshot,
previous?: CodexRateLimitSnapshot,
): ProviderUsageLimitsUpdate | undefined {
const windows = codexRateLimitsToWindows(snapshot);
const previousWindows = previous ? codexRateLimitsToWindows(previous) : [];
// Token ticks repeat session-cached limits that may predate an account probe or reset.
const windows = codexRateLimitsToWindows(snapshot).filter((window) => {
const existing = previousWindows.find((candidate) => candidate.id === window.id);
return (
!existing ||
existing.kind !== window.kind ||
existing.usedPercent !== window.usedPercent ||
existing.resetsAt !== window.resetsAt ||
existing.windowDurationMins !== window.windowDurationMins
);
});
return windows.length > 0 ? { windows } : undefined;
}

Expand Down
Loading