diff --git a/apps/cli/src/agent/lody-acp-extension.test.ts b/apps/cli/src/agent/lody-acp-extension.test.ts
new file mode 100644
index 000000000..a8b30f0c1
--- /dev/null
+++ b/apps/cli/src/agent/lody-acp-extension.test.ts
@@ -0,0 +1,31 @@
+import { describe, expect, it } from 'vitest';
+import { LODY_EXTENSION_METHODS } from 'acp-extension-core';
+import { parseRateLimitsSnapshot, parseLodyExtensionMessage } from './lody-acp-extension';
+
+describe('rate-limit window labels', () => {
+ const window = { usedPercent: 0, windowDurationSeconds: 604_800, resetsAtEpochSeconds: null };
+ const snapshot = {
+ rateLimits: [
+ {
+ limitId: 'claude',
+ scope: { providerId: 'claude' },
+ windows: [window, { ...window, label: 'Fable' }],
+ },
+ ],
+ };
+
+ it('preserves same-duration labeled windows in query responses', () => {
+ expect(parseRateLimitsSnapshot(snapshot)).toEqual(snapshot);
+ });
+
+ it('preserves labels in proactive updates', () => {
+ expect(
+ parseLodyExtensionMessage({
+ method: LODY_EXTENSION_METHODS.rateLimitsUpdate,
+ params: snapshot,
+ provider: 'claude',
+ sessionId: 'synthetic-session',
+ })
+ ).toEqual({ type: 'rateLimits', snapshot });
+ });
+});
diff --git a/apps/cli/src/agent/lody-acp-extension.ts b/apps/cli/src/agent/lody-acp-extension.ts
index c98e1bbfd..97b88fd41 100644
--- a/apps/cli/src/agent/lody-acp-extension.ts
+++ b/apps/cli/src/agent/lody-acp-extension.ts
@@ -55,6 +55,7 @@ const SessionUsageUpdateSchema = z.object({
});
const RateLimitWindowSchema = z.object({
+ label: z.string().optional(),
usedPercent: z.number().min(0).max(100),
windowDurationSeconds: z.number().nonnegative().nullable(),
resetsAtEpochSeconds: z.number().int().positive().nullable(),
diff --git a/locales/en.json b/locales/en.json
index 8a35b7b50..d9d0ce135 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -846,6 +846,7 @@
"machines.rateLimits.resetUnknown": "Reset time unknown",
"machines.rateLimits.resetsAt": "Resets {{time}}",
"machines.rateLimits.sevenDay": "7 days usage",
+ "machines.rateLimits.namedWindow": "{{duration}} · {{label}}",
"machines.rateLimits.unlabeled": "Unlabeled limit",
"machines.rateLimits.usedPercent": "{{percent}}% used",
"machines.resourceStatus": "Resource Status",
diff --git a/locales/zh_CN.json b/locales/zh_CN.json
index e77a97fe8..730278567 100644
--- a/locales/zh_CN.json
+++ b/locales/zh_CN.json
@@ -846,6 +846,7 @@
"machines.rateLimits.resetUnknown": "重置时间未知",
"machines.rateLimits.resetsAt": "重置于{{time}}",
"machines.rateLimits.sevenDay": "7 天用量",
+ "machines.rateLimits.namedWindow": "{{duration}} · {{label}}",
"machines.rateLimits.unlabeled": "未命名额度",
"machines.rateLimits.usedPercent": "已使用 {{percent}}%",
"machines.resourceStatus": "资源状态",
diff --git a/packages/acp-extension-claude b/packages/acp-extension-claude
index d395b3dc6..414718e52 160000
--- a/packages/acp-extension-claude
+++ b/packages/acp-extension-claude
@@ -1 +1 @@
-Subproject commit d395b3dc69832c6566eb0da84a08486d16ba1e69
+Subproject commit 414718e5238a7ed5da0ff23bec31bff4450ffa5f
diff --git a/packages/acp-extension-core b/packages/acp-extension-core
index 23c792b91..7bc6332d3 160000
--- a/packages/acp-extension-core
+++ b/packages/acp-extension-core
@@ -1 +1 @@
-Subproject commit 23c792b910a903b74601e346473827106f991715
+Subproject commit 7bc6332d3f007876895b4a3a827be0060f4d5318
diff --git a/packages/components/src/components/sessions/session-usage-popover.tsx b/packages/components/src/components/sessions/session-usage-popover.tsx
index 60d3e4976..614dc4aa4 100644
--- a/packages/components/src/components/sessions/session-usage-popover.tsx
+++ b/packages/components/src/components/sessions/session-usage-popover.tsx
@@ -19,6 +19,7 @@ import { cn } from '@/lib/utils';
import {
FIVE_HOUR_WINDOW_SECONDS,
SEVEN_DAY_WINDOW_SECONDS,
+ formatAgentRateLimitWindowLabel,
formatRateLimitWindowShortLabel,
getAgentRateLimitWindows,
getContextWindowUsageData,
@@ -185,7 +186,11 @@ export const SessionUsagePopover = memo(function SessionUsagePopover({
rateLimitWindows.map((window, index) => (
diff --git a/packages/components/src/components/settings/machine-quota-compact.tsx b/packages/components/src/components/settings/machine-quota-compact.tsx
index 4fa77e680..780573e6c 100644
--- a/packages/components/src/components/settings/machine-quota-compact.tsx
+++ b/packages/components/src/components/settings/machine-quota-compact.tsx
@@ -15,6 +15,7 @@ import { cn } from '@/lib/utils';
import {
FIVE_HOUR_WINDOW_SECONDS,
SEVEN_DAY_WINDOW_SECONDS,
+ formatAgentRateLimitWindowLabel,
formatRateLimitWindowShortLabel,
getAgentRateLimitWindows,
} from '@/lib/session-usage';
@@ -121,8 +122,8 @@ export function MachineQuotaCompact({ raceLimits, filterCliType }: MachineQuotaC
return (
(
))}
diff --git a/packages/components/src/lib/AGENTS.md b/packages/components/src/lib/AGENTS.md
index 340ce1a54..996967679 100644
--- a/packages/components/src/lib/AGENTS.md
+++ b/packages/components/src/lib/AGENTS.md
@@ -51,6 +51,11 @@ the href parser. It needs a third kind that strips roots without decoding.
## ACP dispatch
+Rate-limit window labels are provider-supplied scope names, displayed alongside
+localized durations through `formatAgentRateLimitWindowLabel`. Preserve every
+window even when durations, utilization, and resets match: a model weekly
+sub-cap and the shared weekly pool are concurrent constraints, not alternatives.
+
Before creating a top-level or child session, call
`filterAcpSessionConfigOptionValues()` so cached values outside the current
selector schema are not dispatched or persisted again.
diff --git a/packages/components/src/lib/session-usage.ts b/packages/components/src/lib/session-usage.ts
index 497cc8078..e7ec1db2e 100644
--- a/packages/components/src/lib/session-usage.ts
+++ b/packages/components/src/lib/session-usage.ts
@@ -9,6 +9,7 @@ import {
type SessionContextWindowUsage,
} from '@lody/shared';
import { clamp } from './clamp';
+import type { TFunction } from 'i18next';
export const FIVE_HOUR_WINDOW_SECONDS = 5 * 60 * 60;
export const SEVEN_DAY_WINDOW_SECONDS = 7 * 24 * 60 * 60;
@@ -32,6 +33,7 @@ export type ContextWindowUsageData = {
};
export type AgentRateLimitWindow = {
+ label?: string;
usedPercent: number;
remainingPercent: number;
windowDurationSeconds: number | null;
@@ -56,6 +58,7 @@ export function getAgentRateLimitWindows(limits: MachineRateLimitUsage): AgentRa
if (usedPercent === null) return [];
return [
{
+ ...(window.label?.trim() ? { label: window.label.trim() } : {}),
usedPercent,
remainingPercent: clampPercentage(100 - usedPercent),
windowDurationSeconds:
@@ -79,6 +82,19 @@ export function formatRateLimitWindowShortLabel(windowDurationSeconds: number |
return `${windowDurationSeconds / 60}m`;
}
+export function formatAgentRateLimitWindowLabel(
+ window: AgentRateLimitWindow,
+ durationLabel: string,
+ t: TFunction
+): string {
+ return window.label
+ ? t('machines.rateLimits.namedWindow', '{{duration}} · {{label}}', {
+ duration: durationLabel,
+ label: window.label,
+ })
+ : durationLabel;
+}
+
export function getContextWindowUsageData(
usage: SessionContextWindowUsage | null | undefined
): ContextWindowUsageData | null {
diff --git a/packages/components/src/stories/ProviderRow.stories.tsx b/packages/components/src/stories/ProviderRow.stories.tsx
index 91c18901e..e78ae6967 100644
--- a/packages/components/src/stories/ProviderRow.stories.tsx
+++ b/packages/components/src/stories/ProviderRow.stories.tsx
@@ -90,6 +90,12 @@ export const ClaudeWithRateLimit: Story = {
windowDurationSeconds: 604_800,
resetsAtEpochSeconds: resetIn(86_400),
},
+ {
+ label: 'Fable',
+ usedPercent: 67,
+ windowDurationSeconds: 604_800,
+ resetsAtEpochSeconds: resetIn(86_400),
+ },
],
},
},
diff --git a/packages/components/src/stories/SessionUsagePopover.stories.tsx b/packages/components/src/stories/SessionUsagePopover.stories.tsx
index 10da70762..32ad444a7 100644
--- a/packages/components/src/stories/SessionUsagePopover.stories.tsx
+++ b/packages/components/src/stories/SessionUsagePopover.stories.tsx
@@ -116,3 +116,33 @@ export const Unavailable: Story = {
rateLimits: {},
},
};
+
+export const ClaudeFableWeekly: Story = {
+ args: {
+ agentType: 'claude',
+ modelId: 'claude-fable-5',
+ modelLabel: 'Fable',
+ showRateLimitWithoutContext: true,
+ rateLimits: {
+ [getRateLimitEntryKey('claude', 'claude')]: {
+ limitId: 'claude',
+ scope: { providerId: 'claude' },
+ planName: 'Max',
+ windows: [
+ { usedPercent: 12, windowDurationSeconds: 18_000, resetsAtEpochSeconds: resetIn(3600) },
+ {
+ usedPercent: 30,
+ windowDurationSeconds: 604_800,
+ resetsAtEpochSeconds: resetIn(172800),
+ },
+ {
+ label: 'Fable',
+ usedPercent: 67,
+ windowDurationSeconds: 604_800,
+ resetsAtEpochSeconds: resetIn(172800),
+ },
+ ],
+ },
+ },
+ },
+};
diff --git a/packages/components/tests/session-usage-popover.test.tsx b/packages/components/tests/session-usage-popover.test.tsx
index 38263035e..ef41e08f4 100644
--- a/packages/components/tests/session-usage-popover.test.tsx
+++ b/packages/components/tests/session-usage-popover.test.tsx
@@ -99,6 +99,39 @@ describe('SessionUsagePopover', () => {
expect(container.querySelector('button')).toBeNull();
});
+ it('shows Fable weekly as a distinct meter beside shared quotas', async () => {
+ await renderUsage({
+ agentType: 'claude',
+ modelId: 'claude-fable-5',
+ showRateLimitWithoutContext: true,
+ rateLimits: {
+ [getRateLimitEntryKey('claude', 'claude')]: {
+ limitId: 'claude',
+ scope: { providerId: 'claude' },
+ windows: [
+ { usedPercent: 12, windowDurationSeconds: 18_000, resetsAtEpochSeconds: null },
+ { usedPercent: 30, windowDurationSeconds: 604_800, resetsAtEpochSeconds: null },
+ {
+ label: 'Fable',
+ usedPercent: 67,
+ windowDurationSeconds: 604_800,
+ resetsAtEpochSeconds: null,
+ },
+ ],
+ },
+ },
+ });
+ await act(async () => {
+ container.querySelector('button')?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
+ });
+ const popover = document.body.querySelector('[aria-label="Usage"]');
+ expect(
+ Array.from(popover?.querySelectorAll('[role="progressbar"]') ?? []).map((meter) =>
+ meter.getAttribute('aria-label')
+ )
+ ).toEqual(['Weekly: 30% used', 'Weekly · Fable: 67% used', '5 hours: 12% used']);
+ });
+
it('shows a truthful unavailable state when the provider omits utilization', async () => {
const unavailableLimits: MachineRateLimits = {
[getRateLimitEntryKey('grok', 'grok')]: {
diff --git a/packages/components/tests/session-usage.test.ts b/packages/components/tests/session-usage.test.ts
index bf0b440a5..c245baf5f 100644
--- a/packages/components/tests/session-usage.test.ts
+++ b/packages/components/tests/session-usage.test.ts
@@ -84,6 +84,16 @@ describe('session usage', () => {
]);
});
+ it('preserves scoped windows even when their values match the shared quota', () => {
+ const weekly = { usedPercent: 0, windowDurationSeconds: 604_800, resetsAtEpochSeconds: null };
+ expect(
+ getAgentRateLimitWindows(usage({ windows: [weekly, { ...weekly, label: ' Fable ' }] }))
+ ).toEqual([
+ { ...weekly, remainingPercent: 100 },
+ { ...weekly, remainingPercent: 100, label: 'Fable' },
+ ]);
+ });
+
it('keeps sub-one-percent Grok usage on the percentage scale', () => {
expect(
getAgentRateLimitWindows(
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 80d3382ed..4190b87e8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -611,7 +611,7 @@ importers:
specifier: ^1.4.0
version: 1.4.0(zod@4.3.6)
'@openai/codex':
- specifier: ^0.153.3
+ specifier: ^0.153.4
version: 0.153.4
acp-extension-core:
specifier: workspace:*