Repository navigation
Show subscription quotas in existing status bars - #60
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
alleriasun
left a comment
There was a problem hiding this comment.
Guided walkthrough of passive subscription status
| import { applyPatch } from "diff"; | ||
|
|
||
| // Temporary backport of codex-acp PR #334. Fail closed if the bundled source changes. | ||
| const version = "1.11.0"; |
There was a problem hiding this comment.
Codex ACP is pinned to 1.11.0. Installation backports only the metadata-emission portion of upstream PR #334 onto its bundled entry point, preserving the version’s existing sparse-window merge. Both original and patched SHA-256 values are checked; already patched installs are accepted. The patch and this installer are included in the npm artifact, and registry.ts resolves that local adapter instead of an unpatched npx latest.
| if (live) { | ||
| const windows = codexPlanUsage(notification.update._meta); | ||
| if (windows.length) queue.push({ type: "plan_usage", windows }); | ||
| } |
There was a problem hiding this comment.
Standard ACP used/size populate context stats. Only live _meta["_codex/rateLimits"] snapshots become normalized plan_usage events; replayed session metadata is ignored. Claude normalizes its SDK rate-limit events into the same shared event before the orchestrator sees them. Missing percentages remain unknown. Real Codex verification reported a 7-day primary window, so labels follow provider durations, not primary/secondary positions.
| } | ||
|
|
||
| /** Latest observation per window, shared by workspaces using the same engine/account. */ | ||
| export function recordPlanUsage( |
There was a problem hiding this comment.
Account observations are indexed by engine in the orchestrator and by quota window here. Each incoming window replaces its previous reading; absent windows remain as last reported. There are no observation timestamps or freshness comparisons. Claude unifiedWindows keeps five-hour, overall weekly, and Fable weekly percentages separate, taking precedence over the selected top-level reading. Provider values above 100% are preserved. The shared map is read when a workspace's normal status refresh runs.
| } | ||
|
|
||
| private async updateStatusMessage(chatId: string, state: ChatState): Promise<void> { | ||
| private schedulePlanStatus(engine: string): void { |
There was a problem hiding this comment.
A quota event schedules a coalesced refresh for known chats using the same engine. It does not await fanout in the engine stream. Per-chat writes serialize and render the latest state when they run, so late responses cannot restore old quota or engine status. A local minute timer ages readings without contacting either provider. Existing warning messages remain, with Unix-second resets formatted correctly.
| return `${day}${clockTime(seconds * 1000)}`; | ||
| } | ||
|
|
||
| export function formatPlanUsage(engine: string, state?: PlanUsageState, now = Date.now(), maxLength = 4096): string { |
There was a problem hiding this comment.
Plan percentages remain separate from ctx. Latest readings stay visible until another event replaces them, including after a reported reset. Reset dates are informational and do not schedule work. Missing five-hour, overall weekly, and Fable weekly readings stay independently unknown. Whole readings fit the channel budget with an explicit omitted count; Slack's full status is tested against its 250-character limit.
alleriasun
left a comment
There was a problem hiding this comment.
Updated walkthrough: turn-completion status replaces expiry and background refresh.
| const engine = ws?.engine ?? state.engineName ?? this.config.defaultEngine; | ||
| const maxLength = this.channel.statusMaxLength ?? 4096; | ||
| if (text.length > maxLength - 80) text = `${text.slice(0, maxLength - 81)}…`; | ||
| text += ` | ${formatPlanUsage(engine, this.planUsage.get(engine), maxLength - text.length - 3)}`; |
There was a problem hiding this comment.
- Refresh alongside context at turn completion
Quota events only update the in-memory account readings. This existing status formatter adds those readings beside context when the workspace turn completes, or an existing engine/permission action refreshes status. No quota timer, expiry transition, fanout, or additional write queue remains. Other workspaces using the same engine see the shared readings when their own status next refreshes. The recording-channel test verifies a new 50% event leaves the displayed 20% unchanged until completion.
| }); | ||
| } | ||
|
|
||
| export function formatPlanUsage(engine: string, state?: PlanUsageState, maxLength = 4096): string { |
There was a problem hiding this comment.
- Display readings within the status budget
Plan percentages remain separate from ctx. Latest readings stay visible until another event replaces them, including after a reported reset. Reset dates are informational and do not schedule work. Missing five-hour, overall weekly, and Fable weekly readings stay independently unknown. Whole readings fit the channel budget with an explicit omitted count; Slack's full status is tested against its 250-character limit.
| if (engine !== "codex" && engine !== "claude-code" && !state) return "plan unknown"; | ||
| const windows = [...(state?.windows.values() ?? [])]; | ||
| const parts = windows.map((window) => { | ||
| const value = window.usedPercent !== undefined ? `${Math.round(window.usedPercent)}%` | ||
| : window.status === "rejected" ? "limited" : "?"; | ||
| return `${window.label} ${value}${window.resetsAt ? ` ↻${resetTime(window.resetsAt)}` : ""}`; | ||
| }); | ||
| for (const label of engine === "claude-code" ? ["5h", "7d", "Fable 7d"] : ["5h", "7d"]) { | ||
| if (!windows.some((window) => window.label === label | ||
| || (engine !== "claude-code" && window.label.endsWith(` ${label}`)))) parts.push(`${label} ?`); | ||
| } | ||
| if (state?.isUsingOverage) parts.push("extra usage active"); | ||
| const heading = "plan used: "; | ||
| const kept: string[] = []; | ||
| for (const part of parts) { | ||
| const remaining = parts.length - kept.length - 1; | ||
| const candidate = heading + [...kept, part].join(" · ") + (remaining ? ` · +${remaining} more` : ""); | ||
| if (candidate.length > maxLength) break; | ||
| kept.push(part); | ||
| } | ||
| const omitted = parts.length - kept.length; | ||
| return heading + kept.join(" · ") + (omitted ? `${kept.length ? " · " : ""}+${omitted} more` : ""); |
There was a problem hiding this comment.
Can we somehow normalize the engine-specific things, like model name and the different time boundaries, into a common structure in an engine-specific method before we do the formatting?
Let's keep the formatting logic really simple, consolidated, and as one logic for all, right?
There was a problem hiding this comment.
Implemented in e35d4f2. Claude and Codex normalize their provider identifiers, durations, percentages, and limited state in the engine layer before emitting one shared plan_usage event. Claude also supplies modelLabel while retaining the raw model ID. The common formatter no longer branches on engine names or provider window types.
|
|
||
| export interface Channel { | ||
| name: string; | ||
| statusMaxLength?: number; |
There was a problem hiding this comment.
What is this for? Is this for our channel, or just for the engine?
There was a problem hiding this comment.
This is for our chat channel status surface, not the engine. Slack uses a channel topic and limits it to 250 characters; Telegram uses a pinned message. Added that explanation directly on statusMaxLength.
| /** Newer configured Claude executables emit fields absent from the bundled SDK's types. */ | ||
| export interface ClaudeRateLimitInfo extends Omit<SDKRateLimitInfo, "rateLimitType"> { | ||
| rateLimitType?: string; | ||
| unifiedWindows?: Record<string, { utilization: number; resetsAt: number }>; | ||
| } |
There was a problem hiding this comment.
Let's just use one type here and keep extra fields optional.
There was a problem hiding this comment.
Consolidated the two engine event shapes into one plan_usage event with optional extra-usage information. Each normalized window has optional percentage, reset time, and limited fields. The raw Claude SDK extension is now private to the engine normalization layer rather than part of the shared event contract.
| case "plan_usage": | ||
| break; // Captured account-wide in executeTurn. |
There was a problem hiding this comment.
Umm do we still have to do this ? Or just informational
There was a problem hiding this comment.
It was an informational chat warning, not a retry, wait, or scheduling action. Removed that duplicate quota message. Quota events now only update retained usage; actual engine errors still appear through the normal error event, so an exhausted turn remains visible.
| ? formatModelName(state.stats.model) | ||
| : state.engineName ?? "agent"; | ||
| text = `🤖 ${displayName} ${usage} | 🔒 ${modeLabel}`; | ||
| text = `🤖 ${displayName} ctx ${usage} | 🔒 ${modeLabel}`; |
There was a problem hiding this comment.
Can we make it more concise
There was a problem hiding this comment.
Shortened the quota section to used: 5h 11% · 7d 58% · Fable 7d 101%. Removed repeated reset dates from the bar while retaining their values in normalized data. Model display names now arrive normalized from the adapter.
|
|
||
| const ws = this.config.workspaceByChat(chatId); | ||
| const engine = ws?.engine ?? state.engineName ?? this.config.defaultEngine; | ||
| const maxLength = this.channel.statusMaxLength ?? 4096; |
There was a problem hiding this comment.
Is it really needed to check...
There was a problem hiding this comment.
Yes, this is the chat API length budget. Slack rejects topics above 250 characters, so the formatter still fits complete readings into the remaining space and shows +N more for omitted windows. It does not check engine quotas or trigger any refresh.
alleriasun
left a comment
There was a problem hiding this comment.
Updated walkthrough for engine normalization and compact status.
| { id: "seven_day_overage_included", label: "Fable 7d" }, | ||
| ]; | ||
|
|
||
| export function claudePlanUsage(info: ClaudeRateLimitInfo): Extract<EngineEvent, { type: "plan_usage" }> { |
There was a problem hiding this comment.
- Normalize provider data at the engine boundary
Claude and Codex normalizers produce shared window IDs, display labels, percentages, optional reset times, and optional limited flags. Claude also supplies known unknown-window seeds; Codex uses reported durations without assuming primary means five hours. Engine events share one plan_usage shape with optional extra-usage data. The common recorder only replaces supplied IDs, preserving unrelated windows. Raw SDK extensions remain here, outside the formatter.
| const usage = this.planUsage.get(engine) ?? { | ||
| windows: new Map(this.engines.get(engine)?.planUsageWindows?.map((window) => [window.id, window])), | ||
| }; | ||
| text += ` | ${formatPlanUsage(usage, maxLength - text.length - 3)}`; |
There was a problem hiding this comment.
- Refresh alongside context at turn completion
Usage events only update the shared in-memory readings. This existing status update path renders them when the chat turn completes, or an existing engine/permission action refreshes status. It uses the adapter-provided model label. There are no quota timers or cross-workspace updates. statusMaxLength belongs to the chat channel: Slack provides 250 for its topic limit.
| if (event.isUsingOverage !== undefined) state.isUsingOverage = event.isUsingOverage; | ||
| } | ||
|
|
||
| export function formatPlanUsage(state?: PlanUsageState, maxLength = 4096): string { |
There was a problem hiding this comment.
- Render one compact format for every engine
The formatter accepts normalized state, without an engine name or provider-specific branches. It prints label plus percentage (or limited/unknown), using the same logic for every window. Reset values remain data but are omitted from this compact bar. Whole readings fit the remaining channel budget, with +N more if necessary. Model labels are normalized separately in the adapter; the raw model ID remains unchanged.
e35d4f2 to
26771f6
Compare
26771f6 to
07cba94
Compare
| { id: "seven_day_overage_included", label: "Fable 7d" }, | ||
| ]; | ||
|
|
||
| export function claudePlanUsage(info: ClaudeRateLimitInfo): Extract<EngineEvent, { type: "plan_usage" }> { |
There was a problem hiding this comment.
- Normalize usage inside each engine adapter
Claude owns its SDK extension type, known windows, provider labels, and quota normalization here in claude-code.ts. Codex metadata parsing lives beside ACP usage_update handling in acp.ts. Both produce the shared plan_usage event; the common recorder and formatter remain provider-neutral. The intermediate engine/plan-usage.ts module has been removed. This is code movement only, with all 166 tests passing.
Normalize Claude and Codex account quota readings in their adapters and show the latest values beside context usage at turn completion. Preserve separate Fable weekly usage and unknown readings without polling or timers. Pin and patch Codex ACP to emit its existing account quota snapshots.
Normalize Claude and Codex account quota readings in their adapters and show the latest values beside context usage at turn completion. Preserve separate Fable weekly usage and unknown readings without polling or timers. Pin and patch Codex ACP to emit its existing account quota snapshots.
Review walkthrough
Latest revision: removed
src/engine/plan-usage.ts. Claude normalization now lives inclaude-code.ts; Codex metadata parsing lives inacp.ts. Shared formatting is unchanged. Code movement since the last squashed version.This revision addresses the comments on
bf1e829: provider normalization now happens inside the engine layer, both engines emit one usage event, and the status bar is shorter. The channel length budget remains because Slack limits its topic to 250 characters. The duplicate rate-limit chat warning is removed; actual turn errors remain visible. Review changes since those comments.Replies on all six review comments explain what changed or why the channel constraint remains. The threads are left open for review.
Show subscription quota beside context usage in the existing Telegram pinned status and Slack channel topic. Workspaces share the latest in-memory account readings for their engine, and each chat refreshes through the existing turn-completion status path. No new command, polling, history, expiry clock, background fanout, or engine routing.
Codex uses pinned adapter 1.11.0 with only the metadata-emission portion of upstream PR #334, preserving its existing sparse-snapshot merge. A hash-checked postinstall patch makes the adapter reproducible. Claude normalizes optional unified window data from newer configured executables, keeping five-hour, overall weekly, and Fable weekly readings distinct.
Both adapters emit the same
plan_usageevent. The common formatter consumes normalized labels, percentages and optional limited/extra-usage information. It has no provider-specific branches. Claude supplies a display model label separately from the raw model ID. Reset timestamps are retained as data but omitted from the compact bar.Verification
npx tsc --noEmit: passed.npm test: 166 passed. Covers live versus replayed ACP metadata, independent windows and Fable, zero and above-100% provider readings, missing values, retained readings after reset, shared account state, engine isolation, display model labels, turn-completion refresh, actual errors without duplicate quota warnings, and the full Slack status budget.Current recording-channel integration scenario: a completed turn reports 20%; the next turn emits 50% and pauses before completion. Observed status:
Earlier authenticated Codex check through the actual patched adapter used
Reply exactly OK. Do not call any tools.It returned a structured 7-day window at 6% used and independently reported context at 10%. The window wasprimary; its label comes from the actual 10,080-minute duration.Earlier authenticated Claude check through configured Claude Code 2.1.251 with
claude-fable-5returnedYou're out of usage creditsand still emitted this real quota data:{ "status": "rejected", "rateLimitType": "seven_day_overage_included", "unifiedWindows": { "five_hour": { "utilization": 0.11, "resetsAt": 1789092000 }, "seven_day": { "utilization": 0.58, "resetsAt": 1789369200 }, "seven_day_overage_included": { "utilization": 1.01, "resetsAt": 1789369200 } } }Replayed those captured provider responses through the current normalization and formatter without another provider request. Observed output:
Earlier packaging check compiled outside the live worktree, packed into a temporary artifact, and installed production dependencies into an isolated consumer with a pristine hoisted adapter. Postinstall produced the expected patched hash, rerun was idempotent, and the compiled registry resolved that adapter. The packaging code is unchanged in this revision.
Independent local review of normalization and the subsequent move into each adapter found no actionable issues. Typecheck and all 166 tests were rerun after the move. Hosted GitHub Codex review did not run because its account review-usage limit was reached.
Limits
Readings describe the last provider report, not a live account view. They stay visible until replaced, including after a reset or while idle. Each chat refreshes on its own turn completion and existing engine/permission status actions. Current aggregation assumes one account per engine.
Claude seeds its known five-hour, overall weekly, and Fable windows as unknown. Codex displays reported windows only, showing
used: ?before any report; primary/secondary positions do not imply a fixed duration. Older Claude executables may omit percentages while allowed. Anthropic describes Fable's allowance as a limit within the overall weekly budget.The live relay was not rebuilt or restarted. Provider emission was verified in the earlier authenticated checks; current normalization/formatting was verified with their captured responses. Telegram/Slack status delivery was exercised through recording-channel integration tests rather than modifying live chats.