Repository navigation
Bill mock interviews by the minute and stop sessions at 0 credits - #138
Conversation
Drop per-turn mock pricing: MockBilling and billing=per_turn on the three mock requests, remaining_questions, metered=0 on the mock socket, mockPricing on the ping and the price quote in the setup form. Live and mock now share one start rule (lib/credit-gate.ts): at least one minute of credit. The home cards, the live control panel and the mock setup form all use it and offer Buy credits. The setup form shows an estimate in minutes and credits, how far the balance goes, and that a session ends with its report when credits run out. Refs #137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend closes an ASR socket with 4402 when the balance reaches zero, and refuses one opened at zero. AudioWsStream never reconnects on that code (in the bound handler, before open, and in the retry loop), and start() throws OutOfCreditsError for a socket refused while it was starting. - Live: one out-of-credits event per session (both channels close), which the control panel turns into the normal Stop path, so the transcript can still be saved, plus a Buy credits toast. - Mock: ends through endSession(), keeping the answer in progress and going on to the report. Ignored once scoring has begun. - Sockets carry kind=mock and a per-session client_session_id so the backend ledger can group one interview's sockets. Refs #137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A running live or mock session gets one toast when the balance falls to five minutes and one at one minute, read off the ping balance. A session that starts below a threshold gets only the lowest one it is under. Not shown while a mock is being scored. Refs #137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs #137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review notesI found no blocking issues. These are worth knowing:
Verified
Still to do by hand: the end-to-end runs listed in the PR description. The stop at 0 in particular needs the backend from #67 running locally. |
Out of stealth, running out of credits ends through the Stop path with the save prompt. In stealth a screen share is likely live, so it now stops the way the stop hotkey does: no modal, no jump to the dashboard, and the next Start still asks about the transcript. The live service also reports out-of-credits only once both channels have started. A channel refused while the other is still starting is start() rejecting, and reporting it too ran a stop beside the start path's own teardown. Refs #137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review pass 2Fixed in
Checked and consistent with backend#67 and the merged admin and hero changes
Small leftover: hero's
|
startAssistant writes Running a few seconds after the sockets are up. A 4402 inside that window stopped the session and was then overwritten by the start path writing Running, leaving a console that showed a live session with no sockets behind it. A report during Starting is now held and acted on once Running, or dropped if the start fails. Also: a language switch refused for credits no longer warns that the language only half applied (the session is ending), and a start refused for credits says "not enough credits to start" instead of "the session has ended". Refs #137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review pass 3Fixed in
Rechecked with no new findings
|
The mock socket opens before main is asked to start, so a 4402 could land while the session still read Idle, and the listener ignored it. The session then ran on a socket that never reconnects: questions generated, no transcription. A refusal in that window is now held and acted on once main's start resolves. Also puts the unmount-cleanup comment back above the effect it describes. Refs #137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review pass 4. Fixed |
Main's start resolves over one IPC message and the state saying the session is running arrives over another, so this side can still read Idle for a moment after start() returns. A 4402 in that gap was parked as pending after the pending check had already run, and dropped. Once main has started, a refusal reading Idle is now acted on at once. Refs #137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review pass 5. Fixed |
Review pass 6: termination, network loss, several clients on one accountI traced each scenario through the code in backend#67 and client#138. Nothing in either PR needed fixing. I added one test, backend App closed or killed during an interview
Network lost during an interview
Several clients on one account
Checks
|
Refs #137 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review pass 7: no functional findings. |
End-to-end run against a local backendI ran the backend from this branch against a local MongoDB and real Deepgram. A throwaway test user was used and removed afterwards. Bug found and fixed:
|
| Scenario | Result |
|---|---|
| New client opens at 0 credits | closed with 4402 |
Old client (no client_session_id) at 0 |
handshake refused, HTTP 403 |
| Reconnect with 3 credits | accepted |
| Mock runs out (5 credits) | 4402 after 38s, balance 0, ledger 5, mock_minutes |
| Live killed abruptly after 20s (two sockets) | charged 4, two ASR_STOP rows, ledger 4, nothing charged after |
| Dead network (client stops reading and ponging) | dropped after 28s, 5 credits for about 30s of mock (exactly 10/min) |
| Two devices, one account, 6 credits, four sockets | all closed with 4402, balance 0, ledger total 6 |
Client socket code (AudioWsStream from client#138, transpiled and run against the same backend)
| Scenario | Result |
|---|---|
| Start at 0 | one socket opened, refused, no retry; next connect throws OutOfCreditsError ("Not enough credits to start"); session not notified |
| Running out mid-session (2 credits) | onOutOfCredits fired exactly once, after 14s; no reconnect |
| Ordinary drop with credits left | reconnects (the 4402 guard doesn't block it) |
Not covered here
The Electron UI itself: the save prompt, the stealth behaviour, the warning toasts and the Buy credits links. Those need someone to click through the app with a microphone and screen capture.
One Deepgram connect timeout came up during the run (timed out during opening handshake). The session never started and correctly wrote no row. It's external flakiness, unrelated to this change.
Closes #137
Backend: PowerInterviewAI/backend#67 (deploy that first)
What
Mock interviews are billed by the minute like live ones, and both stop when credits run out.
5d82110):MockBilling/billing=per_turn,remaining_questions,metered=0,mockPricingand the price quote.lib/credit-gate.ts: at least 1 minute of credit. It's used by both home cards, the live control panel and the mock setup form, each with a Buy credits action.7c164b1):AudioWsStreamnever reconnects on close code 4402. It checks for the code in the bound close handler (ahead ofactive), before open, and in the retry loop.start()throwsOutOfCreditsErrorfor a socket refused while starting.endSession(), keeping the answer in progress and going to the report. A 4402 during Scoring is ignored.kind=mockand a per-sessionclient_session_idfor the backend ledger.62a8d15): a toast at 5 minutes and at 1 minute left, live and mock.1c1b6d7,b38cac9).The title bar already shows the remaining time on every page, including the mock session, so no extra display was added there.
Compatibility
kindandclient_session_id. This client sends nometered, so a mock is billed at 10/min on the old ASR meter, and nobillingfield is sent that could also charge per turn. Nothing is charged twice.Tests
pnpm test:main: all checks pass.tsc(both configs) andeslintare clean.test/credit-gate.test.mjsruns the start rule at runtime.test/out-of-credits.test.mjspins:test/mock-billing-contract.test.mjsis rewritten for the new contract, withchannels=1still pinned.Manual checks before release
🤖 Generated with Claude Code