Skip to content

Fix permission-prompt screen jumping + OpenAI token over-counting#104

Open
QodeXcli wants to merge 9 commits into
mainfrom
fix/prompt-flicker-and-token-accounting
Open

Fix permission-prompt screen jumping + OpenAI token over-counting#104
QodeXcli wants to merge 9 commits into
mainfrom
fix/prompt-flicker-and-token-accounting

Conversation

@QodeXcli

@QodeXcli QodeXcli commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What changed

1. UI: violent screen jumping while a permission prompt is open (src/cli/ui.tsx)

  • The 250ms elapsed-time ticker kept firing during accept/decline/always prompts (busy stays true), re-painting the whole dynamic Ink frame 4×/second. With a tall diff + confirmation on screen, that re-paint reads as heavy scroll-jumping until the user answers.
  • The ticker now goes quiet while a prompt is pending — guarded via a ref inside the interval callback, so the timer and the task's start time survive and the readout simply freezes and resumes.
  • Tool-activity lines also hide under the prompt: the prompt IS the activity, and every extra dynamic line enlarges the frame Ink re-paints.

2. Tokens: OpenAI-provider models appeared to burn a 200k budget in minutes (src/llm/providers/openai.ts, src/agent/result-aging.ts)

  • OpenAI's prompt_tokens INCLUDES automatically-cached prefix tokens. Usage now splits prompt_tokens_details.cached_tokens out as cacheRead, matching the Anthropic provider's semantics — so the budget counts only fresh input instead of re-billing the entire context at 1× every turn.
  • Result aging tightened from 3 turns / 8KB to 2 turns / 5KB so large unique tool outputs (build logs, grep walls) stop riding the context full-size for most of a task.

Why

Two live reports from daily use: the chat view bounces hard whenever a tool-permission prompt appears, and some models exhaust their 200k token budget within minutes while comparable environments stay economical.

Reviewer notes

  • Verified against the audit: Anthropic budget accounting was already correct (input excludes cacheRead) — untouched.
  • Deliberately NOT reset sessionToolNames per task: its monotonic growth is a documented design choice (src/agent/loop.ts ~917) that keeps the serialized schema prefix byte-stable for prompt caching.
  • npx tsc --noEmit clean; test/result-aging.test.ts and test/openai-user-agent.test.ts green (12/12).

QodeXcli added 9 commits July 5, 2026 10:27
The 250ms nowTick interval kept firing during accept/decline/always prompts,
re-painting the whole dynamic Ink frame 4x/second — with a tall diff+prompt
on screen this read as violent scroll-jumping until the user answered. The
ticker now goes quiet via a ref (timer and task start time stay intact, the
readout just freezes), and tool-activity lines hide under the prompt.
…ults sooner

OpenAI's prompt_tokens INCLUDES automatically-cached prefix tokens; usage now
splits prompt_tokens_details.cached_tokens into cacheRead (matching Anthropic
semantics), so the budget no longer re-bills the whole context at 1x every
turn for OpenAI-provider models. Result aging also tightens (3 turns/8KB ->
2 turns/5KB) so large unique tool outputs stop riding the context full-size.
…text re-reads

An agentic turn makes one API call per tool round and each call re-sends the
whole conversation, so charging every call's full input made budget spend grow
quadratically with tool rounds — a 21k-context task on a local model (no cache
fields at all) burned 216k/200k within two minutes and died. A prompt high-water
mark now counts each context token ONCE: consume = output + max(0, seen - mark).
Dollar cost stays full-usage per call — bills are bills; the token budget is a
runaway guard, not an invoice.
…ust apply blindly

qodex already had 'qodex update' and a dashboard button that ran git pull →
npm install → build. Both fired blind — no way to know an update EXISTED. Adds
checkForUpdate(): a no-merge 'git fetch' + behind-count against the upstream.
- terminal: 'qodex update --check' reports availability without applying
- dashboard: Health panel now shows 'QodeX vX.Y.Z', an 'update available — N
  behind' badge (instant local-only check on render), a 'Check for updates'
  button (networked, app.checkUpdate), and the existing update button
Tests build a real local remote→clone pair (no network) to prove the behind
count and the fetchRemote:false fast path.
…sks mid-work

checkpoint() runs right after a completed model call, so the old absolute
ceiling could ONLY ever kill a task that was actively working (live: 'Time
budget exceeded: 608s/600s' during an active edit loop on a local model) — a
hung task never reaches a checkpoint at all. The ceiling now fires only when
the task is ALSO stalled (no completed call in the last 2 minutes); true
runaways stay bounded by the iteration and token caps. Default ceiling raised
600s -> 3600s: it was calibrated for cloud latency, and a local model
legitimately spends 10 minutes on a handful of long generations.
…he status bar

The bar said 'local' whenever cost was zero (a guess) and rendered the STATIC
context window from the model table even though loop.ts already live-syncs the
real window from LM Studio's native API — the detection existed, unwired. Now
budget_update carries providerName/providerIsLocal and the live-synced window:
- source badge next to the model: 'ollama·local', 'lmstudio·local',
  'anthropic·api' … (LM Studio detected via its /api/v0/models registry, which
  its OpenAI-compat facade hides)
- cloud at zero accrued cost shows 'api · $0.0000', never 'local · free' —
  billing surfaces the moment real cost accrues
- context meter uses the live window, falling back to the static table
…rst write — fabricated sub-session ids (parent/sub-<ts>) had no parent row in sessions, so the messages FK failed instantly and every delegation died with SUBAGENT_FAILED
…re ceiling

A 5-minute shell command is work, not a stall — but only consume() (a model
call) bumped lastProgressAt, so the wall ceiling read long tool runs as
'stalled' and killed the task right after the tool returned (live: 'Time
budget exceeded: 808s/600s (stalled 300s)' immediately after a 300s shell).
noteProgress() now fires when tool executions complete, at all three dispatch
sites (parallel read-only, sequential mutating, batched).
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