Skip to content

Add coding-plan usage tracking and fix OpenCode backend issues#157

Open
DavidSegun wants to merge 4 commits into
Quality-Max:mainfrom
DavidSegun:feat/plan-window-tracking
Open

Add coding-plan usage tracking and fix OpenCode backend issues#157
DavidSegun wants to merge 4 commits into
Quality-Max:mainfrom
DavidSegun:feat/plan-window-tracking

Conversation

@DavidSegun

Copy link
Copy Markdown

Adds rolling coding-plan usage tracking with a new /plan command, status bar updates, configurable window duration (plan_window_hours), and automatic handling of plan exhaustion.

Also fixes several OpenCode issues discovered during implementation:

  • Added token reporting for OpenCode and Codex.
  • Surface OpenCode errors instead of silently dropping them, including plan limit (429) responses.
  • Fixed two Windows compatibility issues (--auto handling and -- argument separator).
  • Suppressed benign OpenCode 1.0.105 trailing errors while gracefully handling missing token usage events.

Testing

  • Added unit and parser tests.
  • Verified end-to-end with OpenCode 1.0.105.
  • Build, go test, and go vet all pass.

Orchestration backends (Claude Code, Codex, OpenCode + Z.AI GLM) run on
subscription plans that meter usage over a rolling ~5-hour window, but
qmax-code never tracked it, so it was unclear how much was used or when the
plan would stop.

Add a time-based PlanWindow tracker (internal/api/planwindow.go) that opens on
the first orch turn, accumulates turns/tokens, rolls over after the window
elapses, and can be marked exhausted from a provider limit hit. Surface it in
the input status bar and a new /plan command (also summarized in /status and
/cost). Window length is configurable via plan_window_hours (default 5).

Also close two backend blind spots that fed the same problem:
- OpenCode and Codex now report per-turn token usage (previously CC-only),
  feeding both session cost and the plan window.
- OpenCode `{type:error}` events were being silently dropped; they are now
  surfaced, and a 429 / usage-limit refusal is detected as a plan-limit hit,
  reading the reset time from rate-limit headers when present.

Build + vet clean; new planwindow unit tests pass. Codex/OpenCode token-field
names are parsed defensively pending confirmation against a live successful run.
…er tests

Verified against live `opencode run --format json` output (opencode 1.0.105):
successful turns emit a trailing status-code-less UnknownError (an internal
schema-validation gripe), and this version emits no token-usage event in the
stream. Surfacing every error event would therefore spam a scary line after
every good turn, so only errors carrying an HTTP status code (auth/quota/5xx)
and detected usage-limit hits are shown by default; status-code-less events
appear only in verbose mode.

Adds opencode_stream_test.go driving the parser with the real captured streams:
success (renders answer, no false limit, no invented tokens), a 429 limit hit
(flags the window + reads Retry-After), a 403 subscription error (surfaced but
not a limit), and forward-compat token extraction for both field shapes.
… supports it

opencode 1.x removed the `run --auto` flag and now governs tool approvals
through the config `permission` block. qmax-code passed --auto unconditionally,
so on current opencode every turn failed: opencode printed its usage and exited
1 with no output ("opencode exited with error: exit status 1"), making the
OpenCode backend (incl. Z.AI GLM coding plans) unusable.

Probe `opencode run --help` once and only append --auto when the flag is still
advertised, preserving behavior on older opencode while working on 1.x. The
managed config's permission block already enforces the standard/unattended
policy, so nothing is auto-approved that was not before.

Verified against opencode 1.0.105: without --auto the run produces the answer
and the turn completes.
On Windows, npm installs opencode as a `.cmd` shim. Go's os/exec runs `.cmd`
files through cmd.exe, which swallows the `--` end-of-flags separator and drops
the positional message that follows it — opencode then aborts with "You must
provide a message or a command" and exits 1, so no OpenCode turn (including
Z.AI GLM / OpenRouter coding plans) can run on Windows.

Pass the message without `--` on Windows (sanitizeCCUserPrompt already strips
control bytes, so a lone positional is safely taken as the message); keep `--`
on other platforms where it protects a message starting with "-". Verified via
a Go exec repro: with `--` the message is dropped, without it the turn runs.
@DavidSegun
DavidSegun requested a review from Desperado as a code owner July 24, 2026 19:12
@sigilix

sigilix Bot commented Jul 24, 2026

Copy link
Copy Markdown

Sigilix Overview

Effort: 4/5 (large)

Quality gates

  • ⚠️ PR title follows convention — Title doesn't match repo convention: ^(?:feat|fix|chore|docs|refactor|test|perf|style|build|ci|revert)(?:\([^)]+\))?!?: .+
  • ✅ PR description is complete
  • ℹ️ PR is linked to an issue — No Closes #N / Closes SIG-N keyword found in PR body or commit messages.

Summary — latest push

Adds rolling coding-plan usage window tracking (5-hour default) surfaced via a new /plan command and the status bar, and fixes several OpenCode and Codex backend issues. OpenCode and Codex now report per-turn token usage and surface provider errors (especially 429 plan-limit responses) instead of silently dropping them. It also fixes a Windows compatibility bug where the -- argument separator was swallowed by cmd.exe and a version-probe for the deprecated --auto flag to prevent OpenCode 1.x from exiting with a usage error.

Important files

File Score Notes Next step
internal/agent/opencode_agent.go 5/5 Core changes for OpenCode: adds token usage extraction, error event handling (including 429 plan limits), Windows -- separator fix, and dynamic --auto flag probing. Add integration tests simulating OpenCode NDJSON streams with error, usage, and token payloads to verify the parsing logic handles all ocTokens shapes and ocError status codes.
internal/api/planwindow.go 5/5 Implements the PlanWindow tracker for the rolling 5-hour usage window, including state transitions for starting, recording, exhausting, and resetting. Add boundary tests for PlanWindow.Record and MarkExhausted ensuring concurrent safety isn't required by the contract and that zero-time/zero-duration edge cases behave correctly.
internal/agent/codex_agent.go 4/5 Adds token usage extraction and plan-limit detection to the Codex backend stream, mirroring the OpenCode changes. Add parser tests for inspectCodexEvent covering the codexEvent struct variations (top-level vs nested usage) and plan-limit message matching.
internal/agent/planlimit.go 4/5 Introduces isPlanLimitMessage for broad heuristic matching of rate/quota errors and parseResetTime for extracting reset times from HTTP headers. Add unit tests for parseResetTime covering delta-seconds, HTTP-date, epoch-seconds, and small-delta heuristics to ensure no misparses.
internal/tui/input.go 3/5 Extends the status bar to display plan window state: remaining time, turns used, or exhaustion notice. Add a unit test for renderStatus verifying the plan segment is omitted when PlanActive is false and correctly formatted when PlanExhausted is true.

Sequence diagram

sequenceDiagram
    participant User
    participant REPL
    participant Agent
    participant Provider
    User->>REPL: /plan or turn
    REPL->>Agent: Run(message)
    Agent->>Provider: API request
    Provider-->>Agent: Stream (tokens, error, 429)
    Agent->>Agent: inspectCodexEvent / handleOCError
    Agent-->>REPL: LastTurnStats, LastPlanLimit
    REPL->>REPL: PlanWindow.Record / MarkExhausted
    REPL-->>User: Status bar update
Loading

Confidence: 4/5

The feature is well-structured with clear separation of concerns, but the plan-limit detection relies on broad heuristic string matching and header parsing that lack visible unit tests in the diff, which could lead to false positives or misparsed reset times.

  • Verify that parseResetTime in internal/agent/planlimit.go correctly handles small delta values vs. epoch seconds, as a value like 999999999 could be misclassified.
  • Check the thread-safety contract of PlanWindow — the doc says it's not safe for concurrent use, so confirm it's only ever called from the single turn loop in the REPL.
  • Ensure the openCodeSupportsAutoFlag probe in internal/agent/opencode_agent.go handles the 10-second timeout gracefully if the binary hangs or is misconfigured.
  • Confirm that the Windows -- separator fix in internal/agent/opencode_agent.go doesn't inadvertently break message passing for prompts that start with a dash on non-Windows systems.

Suggested labels: feature bug

Suggested reviewers: @Desperado


Posted · 12ecdea · 0 findings — View review
Dismiss @sigilix dismiss <reason> (not-a-bug | bad-anchor | already-covered | too-minor | wrong-context) · Re-run /sigilix review
Sigilix · 0 of 50 reviews used in past 5h

@sigilix sigilix Bot added the enhancement New feature or request label Jul 24, 2026
@qualitymaxapp

qualitymaxapp Bot commented Jul 24, 2026

Copy link
Copy Markdown

❌ QualityMax Pipeline

Gate Result
🔍 AI diff review ⏭️ Skipped — disabled · skipped · 0 eligible / 0 reviewed · disabled by project configuration
🔍 SAST skipped · 0 eligible / 0 reviewed · disabled by project configuration
🧪 Repo Tests ❌ 529/530 passed (go)
🤖 AI Tests ✅ 47/51 passed

Powered by QualityMax — AI-Powered Test Automation

Copy link
Copy Markdown
Contributor

Review findings — request changes

I found four correctness issues in the new plan-tracking paths:

  1. BLOCKER — limit hits can bypass exhaustion tracking. LastPlanLimit() is read only inside if err == nil (internal/repl/repl.go:1209-1230). Both CLI agents return an error when a limit event produces no assistant output and the subprocess exits non-zero, so the usual 429/refusal path can set lastLimitHit but never call MarkExhausted. Please consume the limit state regardless of the run error, or return and handle a typed plan-limit error.

  2. WARNING — provider reset times do not roll the window over. PlanWindow.Record resets only after WindowLen. If the provider reports an earlier authoritative reset, the first successful turn after that time remains in the old exhausted window and the UI continues to show it as exhausted. Reset based on the effective ResetAt() when recording a new turn.

  3. WARNING — live backend switching mixes independent quotas. A single tracker is allocated once (internal/repl/repl.go:170-174), while /cc, /codex, and /opencode switch providers live. Turns and exhaustion state from separate subscription plans are therefore combined. Please keep trackers keyed by backend/provider, or otherwise start/select the correct window when the backend changes.

  4. WARNING — OpenCode undercounts multi-step/tool turns. The parser overwrites lastTurnIn/lastTurnOut for every usage-bearing event (internal/agent/opencode_agent.go:347-353), leaving only the final step's tokens. OpenCode emits usage on each step-finish, so tool loops can contain multiple token-bearing steps. Accumulate one canonical token payload per distinct step/event and add a two-step parser test.

Verification

  • go test ./... passed locally.
  • go test -count=3 ./... passed locally.
  • go vet ./... passed locally.
  • The GitHub QualityMax pipeline remains red at 529/530 repository tests; that single failure did not reproduce locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants