Skip to content

Show subscription quotas in existing status bars - #60

Merged
alleriasun merged 1 commit into
mainfrom
feat/status-center
Sep 11, 2026
Merged

alleriasun merged 1 commit into
mainfrom
feat/status-center

Conversation

@alleriasun

@alleriasun alleriasun commented Sep 11, 2026 •

Copy link
Copy Markdown
Owner

Review walkthrough

Latest revision: removed src/engine/plan-usage.ts. Claude normalization now lives in claude-code.ts; Codex metadata parsing lives in acp.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.

  1. Ship the upstream Codex quota patch.
  2. Keep account allowance separate from context.
  3. Normalize usage inside each engine adapter.
  4. Refresh alongside context at turn completion.
  5. Render one compact format for every engine.

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_usage event. 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:

    During turn: 🤖 codex ctx 13% | 🔒 Default | used: 5h 20%
    After completion: 🤖 codex ctx 13% | 🔒 Default | used: 5h 50%
    
  • 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 was primary; its label comes from the actual 10,080-minute duration.

  • Earlier authenticated Claude check through configured Claude Code 2.1.251 with claude-fable-5 returned You're out of usage credits and 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:

    Claude: used: 5h 11% · 7d 58% · Fable 7d 101%
    Codex: used: 7d 6%
    
  • 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.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@alleriasun alleriasun left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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";

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/engine/acp.ts
if (live) {
const windows = codexPlanUsage(notification.update._meta);
if (windows.length) queue.push({ type: "plan_usage", windows });
}

@alleriasun alleriasun Sep 11, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/plan-usage.ts Outdated
}

/** Latest observation per window, shared by workspaces using the same engine/account. */
export function recordPlanUsage(

@alleriasun alleriasun Sep 11, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/orchestrator.ts Outdated
}

private async updateStatusMessage(chatId: string, state: ChatState): Promise<void> {
private schedulePlanStatus(engine: string): void {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/plan-usage.ts Outdated
return `${day}${clockTime(seconds * 1000)}`;
}

export function formatPlanUsage(engine: string, state?: PlanUsageState, now = Date.now(), maxLength = 4096): string {

@alleriasun alleriasun Sep 11, 2026 •

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 alleriasun left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated walkthrough: turn-completion status replaces expiry and background refresh.

Comment thread src/orchestrator.ts Outdated
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)}`;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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.

Comment thread src/plan-usage.ts Outdated
});
}

export function formatPlanUsage(engine: string, state?: PlanUsageState, maxLength = 4096): string {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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.

Comment thread src/plan-usage.ts Outdated
Comment on lines +92 to +113
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` : "");

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/types.ts

export interface Channel {
name: string;
statusMaxLength?: number;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for? Is this for our channel, or just for the engine?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/types.ts Outdated
Comment on lines +179 to +183
/** 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 }>;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just use one type here and keep extra fields optional.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/orchestrator.ts
Comment on lines +889 to +890
case "plan_usage":
break; // Captured account-wide in executeTurn.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Umm do we still have to do this ? Or just informational

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/orchestrator.ts
? formatModelName(state.stats.model)
: state.engineName ?? "agent";
text = `🤖 ${displayName} ${usage} | 🔒 ${modeLabel}`;
text = `🤖 ${displayName} ctx ${usage} | 🔒 ${modeLabel}`;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make it more concise

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/orchestrator.ts

const ws = this.config.workspaceByChat(chatId);
const engine = ws?.engine ?? state.engineName ?? this.config.defaultEngine;
const maxLength = this.channel.statusMaxLength ?? 4096;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it really needed to check...

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 alleriasun left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated walkthrough for engine normalization and compact status.

Comment thread src/engine/plan-usage.ts Outdated
{ id: "seven_day_overage_included", label: "Fable 7d" },
];

export function claudePlanUsage(info: ClaudeRateLimitInfo): Extract<EngineEvent, { type: "plan_usage" }> {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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.

Comment thread src/orchestrator.ts
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)}`;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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.

Comment thread src/plan-usage.ts
if (event.isUsingOverage !== undefined) state.isUsingOverage = event.isUsingOverage;
}

export function formatPlanUsage(state?: PlanUsageState, maxLength = 4096): string {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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.

Comment thread src/engine/claude-code.ts
{ id: "seven_day_overage_included", label: "Fable 7d" },
];

export function claudePlanUsage(info: ClaudeRateLimitInfo): Extract<EngineEvent, { type: "plan_usage" }> {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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.

@alleriasun
alleriasun merged commit 07d7c6e into main Sep 11, 2026
@alleriasun
alleriasun deleted the feat/status-center branch September 11, 2026 04:04
alleriasun added a commit that referenced this pull request Sep 11, 2026
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.
alleriasun added a commit that referenced this pull request Sep 24, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant