Skip to content

Per-session token usage + cursor startup fix + E2E suite expansion#49

Merged
fkesheh merged 8 commits into
mainfrom
feat/usage-cursor-e2e
Jul 17, 2026
Merged

Per-session token usage + cursor startup fix + E2E suite expansion#49
fkesheh merged 8 commits into
mainfrom
feat/usage-cursor-e2e

Conversation

@fkesheh

@fkesheh fkesheh commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #48 (these landed on the branch after its squash-merge). Six commits; 497 bridge tests, 110 UI tests green; all e2e specs + helpers typecheck.

Per-session token usage

SessionUsage extracted from harness-native transcripts — facts only (model + four token classes, no hand-maintained pricing). claude gets per-model attribution (streamed, deduped by message id); codex reports rollout totals with cache decomposed out of input. Surfaces: ftown-sessions usage <id...> (table + per-model sub-rows), GET /api/sessions/:id/usage, get_session_usage RPC, Terminal header + session-row display. cursor/grok have no structured source and report none.

Cursor concurrent-startup fix

Investigation proved a macOS Keychain write race during token refresh (threshold 2; both incident signatures reproduced deterministically; 250ms stagger → 0/27 failures at N=9). spawnStaggerMs on the harness registry (cursor: 300ms) serializes launches at the choke point. Startup crashes now persist errorReason (exit message + de-ANSI'd terminal tail) on the session record.

E2E suite: 4 tests → ~25 across 7 specs (all shell-harness, zero LLM cost)

  • Security / IDOR: cross-tenant Centrifugo wall — user B rejected (code 103) from A's events/terminal/terminal-input/presence and from PUBLISH to A's commands:rpc (the load-bearing assertion — the bridge trusts the channel with no payload check); positive control proves the wall vs a broken client. Bridge loopback guards (421/403/401/200), token sub non-spoofable, device revoke/list scoped by sub.
  • Lifecycle: CRUD round-trip + resurrection across a real bridge restart (same sid resumes running, fresh marker round-trips the revived pty).
  • Usage: seeded transcript → per-model extraction, dedup, codex cache decomposition, null-for-shell.
  • Loops + mail: run accumulation / run-now / disable / delete / preflight-skip; inbox deliver + long-poll waiter path.
  • CI: push: main added (gates main on merge); npm + Playwright caching. No nightly (hermetic stack, PR-per-change).

Honestly skipped rather than faked: retry (needs error status a shell can't reach cost-free), mail nudge (PTY line-wrap flake), factory-db seeding (cross-repo Python coupling).

Note: e2e specs typecheck but haven't run against the live stack yet — this PR's CI run is the first real execution; the idor-channels green is a genuine security milestone given the whole tenant wall rests on one Centrifugo flag.

🤖 Generated with Claude Code

fkesheh added 6 commits July 16, 2026 13:11
…orReason

Investigation proved concurrent cursor-agent startups race on a macOS
Keychain WRITE during token refresh (threshold 2; both incident
signatures reproduced deterministically; 250ms stagger → 0/27 failures
at N=9). Harness registry gains spawnStaggerMs (cursor: 300); create and
relaunch serialize per-harness via lock-free slot reservation. Startup
crashes now persist errorReason (exit message + de-ANSI'd terminal tail,
capped) instead of status:'error' with no explanation.
usage-collector extracts SessionUsage (4 token classes, models, cost)
from claude transcripts (streamed, deduped by message id — multi-line
assistant messages double-count naively) and codex rollouts (last
total_token_usage; cached decomposed out of input). Hand-maintained
model-pricing table; unknown models report tokens, omit cost. Collected
automatically on session complete/error (terminal-pump) and on demand
(controller). Surfaces: ftown-sessions usage <id...>, GET
/api/sessions/:id/usage, get_session_usage RPC, Terminal header +
session-row display with detail tooltip. cursor/grok have no structured
source and return null; opencode TODO.
SessionUsage v2: costUsd and the model-pricing table are gone; perModel
gains a per-model breakdown of all four token classes (claude attributes
per message; codex keeps honest totals + model list, no fabricated
split). CLI table drops $ and prints indented per-model sub-rows;
UI shows tokens + model with full per-model detail in the tooltip.
Dollars stay derivable downstream from exactly this data.
…gating

New helpers: second-user login + per-user Centrifugo tokens; a raw
Centrifugo v5 client that attempts cross-tenant subscribe/publish and
reports the rejection code (103) distinctly from a connect refusal;
loopback bridge-API probe (Host/Origin/bearer); claude/codex transcript
fixture seeders (verified slug rule); bridge restart for resurrection.
e2e.yml gains push:main + nightly triggers and npm/playwright caching.
Five specs against the frozen helpers:
- idor-channels: cross-tenant Centrifugo wall — B rejected (code 103)
  from A's events/terminal/terminal-input/presence and PUBLISH to A's
  commands:rpc; positive control proves the wall, not a broken client.
- idor-http: bridge loopback guards (421/403/401/200), token sub
  non-spoofable, device revoke/list scoped by sub (verified WHERE sub).
- session-lifecycle: CRUD + resurrection across a real bridge restart
  (same sid resumes running, fresh marker round-trips the revived pty).
- usage: seeded transcript → per-model extraction, dedup, codex cache
  decomposition, null-for-shell; keyed off persisted native session id.
- loops-and-mail: run accumulation/run-now/disable/delete + preflight
  skip; inbox deliver + long-poll waiter path.
The stack is hermetic (dockerized Postgres, pinned Centrifugo, Neon
shim; no external calls, no auto-updated deps) and every change lands
via PR, so main is gated on merge. Nightly added only runner-drift
coverage at the cost of daily noise.
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ftown Ready Ready Preview, Comment Jul 16, 2026 6:04pm

Request Review

…close)

The e2e IDOR suite proved allow_user_limited_channels gates subscribe
only, not publish: any authenticated user could publish create_session/
kill/bridge_exec to another user's commands:rpc#{email} — which the
bridge executed with no caller check — and inject keystrokes via
terminal-input. OSS-native fix: publications carry the publisher's
authenticated sub (ctx.info.user); the bridge's command + terminal-input
handlers and the UI's response handler now drop any publication whose
publisher != channel owner, fail-closed on missing info. 7 unit tests;
the e2e publish test now asserts A's bridge ignores B's create_session
while A's own (matching info.user) still spawns — validating info.user
is populated so the fail-closed guard never breaks owner commands.

Follow-up (lower severity, separate): terminal/sessions/loops/events
still grant needless client-publish (output/state spoofing, not exec).
restartBridge now gates on the NEW bridge's pointer (fresh pid) before
returning — the old bridge unlinks bridge.json on SIGTERM and its
Centrifugo presence lingers, so the helper was resolving on stale
presence before the new pointer existed. Loop-disable test asserts
run-count stability across a full 30s scheduler tick instead of run-record
status (records stay 'running' until a 30s finalize grace). Both run
green twice against the local stack.
@fkesheh
fkesheh merged commit 354b414 into main Jul 17, 2026
5 of 6 checks passed
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