Per-session token usage + cursor startup fix + E2E suite expansion#49
Merged
Conversation
…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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
SessionUsageextracted 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_usageRPC, 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).
spawnStaggerMson the harness registry (cursor: 300ms) serializes launches at the choke point. Startup crashes now persisterrorReason(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)
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), tokensubnon-spoofable, device revoke/list scoped bysub.push: mainadded (gates main on merge); npm + Playwright caching. No nightly (hermetic stack, PR-per-change).Honestly skipped rather than faked: retry (needs
errorstatus 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-channelsgreen is a genuine security milestone given the whole tenant wall rests on one Centrifugo flag.🤖 Generated with Claude Code