Skip to content

Bound interview token startup and no-show cleanup - #440

Merged
jserv merged 2 commits into
sysprog21:mainfrom
xubo9367:fix/interview-token-timeout
Oct 23, 2026
Merged

jserv merged 2 commits into
sysprog21:mainfrom
xubo9367:fix/interview-token-timeout

Conversation

@xubo9367

@xubo9367 xubo9367 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Add a server-side deadline while reading the /api/token request body.
  • Release recorded-interview consent when a dispatched room reaches the candidate no-show deadline.
  • Add a browser-side deadline for /api/token and use the existing offline-practice fallback when it expires.
  • Keep successful startup and existing non-timeout failure behavior unchanged.

The no-show cleanup is guarded by the room name, matching the existing refused-dispatch cleanup. A lost response may still leave an agent waiting until CANDIDATE_JOIN_LIMIT, but the agent then exits and the consent is no longer left bound to an unused room.

Tests

  • cargo test token_body_read_has_a_deadline -- --nocapture
  • cargo test --test web token_stalls_before_and_after_dispatch_have_different_side_effects -- --nocapture
  • node --test tests/browser/dom-contract.test.js tests/browser/recording-consent.test.js

The integration coverage verifies:

  • a stall before dispatch leaves no dispatched room or consent claim;
  • a stall after dispatch has a dispatched room and claimed consent;
  • the consent is released after the dispatched room reports a candidate no-show.

The full local test script reached 1,455 browser tests. One unrelated lobby layout test failed because the VM maps system-ui to Noto Sans and measured 814 px against an 800 px viewport. The branch does not modify the lobby test or its layout files.

Closes #380


Summary by cubic

Bounds interview startup so a stalled /api/token request can't hold it open forever, and releases recorded-interview consent when a dispatched room never gets a candidate or fails before one joins. Closes #380.

  • Adds a 10-second server deadline while reading the token request body; a timeout returns 408 before any room is reserved or consent claimed. A matching 20-second browser deadline falls back to offline practice.
  • Claims consent and dispatches in one spawned task, so a dropped request can't cancel the claim after it commits but before the dispatch sees it.
  • Releases a claimed consent when a dispatched room sees no candidate or fails before a candidate joins; the release is guarded by the room name, refuses once recording has started, and is skipped while a newer pending interview holds the same consent version's pending slot.
  • Keeps successful startup and failures after a candidate join unchanged, including the different side effects of a stall before versus after dispatch. Regression tests cover both paths.

Written for commit b706fc4. Summary will update on new commits.

View guided diff

cubic-dev-ai[bot]

This comment was marked as resolved.

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run make indent before submitting PRs.

Comment thread src/dispatch.rs Outdated
Comment thread src/web/mod.rs
///
/// Implementations must be idempotent per room and must not block: this is
/// called on the request path.
#[derive(Clone)]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NoShowConsent is inserted between the RoomDispatcher doc comment and the trait. As a result, that whole comment, including "Implementations must be idempotent per room and must not block", now documents NoShowConsent, and the trait has no documentation. Move the struct and its impl above the doc comment, and give the struct a line of its own saying what it carries.

Comment thread src/dispatch.rs
}
}
Ok(crate::livekit::RoomRunOutcome::Completed) => {}
Err(error) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An Err here also covers rooms the candidate never reached: join_room or publish_opening_attributes failing inside open_session, or candidate_metadata ending with "room closed before a candidate joined". Those paths only log, so the claimed consent stays bound to a room nobody joined, the stuck state this change sets out to clear. Have open_session report failures before a candidate is accepted as their own outcome and release no_show for them as well, while keeping errors after the interview started as spent.

Comment thread src/web/mod.rs
provider: &crate::config::Provider,
no_show: Option<NoShowConsent>,
) -> Result<(), DispatchRefusal> {
let _ = no_show;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test reaches this default body, because RecordingDispatcher overrides it: the mutation job reports replacing it with Ok(()) as missed, and that mutant would stop any dispatcher relying on the default from ever staffing a recorded room. The same run also misses LocalDispatcher::ensure_agent_with_no_show_release. Add a test dispatcher that implements only ensure_agent and assert that a recorded /api/token request still staffs the room.

cubic-dev-ai[bot]

This comment was marked as resolved.

cubic-dev-ai[bot]

This comment was marked as resolved.

@xubo9367

Copy link
Copy Markdown
Contributor Author

I addressed all requested changes in aa3a24c. Locally, make indent and the targeted regression tests pass. Mutation testing also completes with 19 mutants: 16 caught, 3 unviable, and none missed. The latest GitHub Actions run is awaiting maintainer approval.

Comment thread src/livekit.rs
let (room, mut events) = match join_room(config, room_name, &agent_identity, now_seconds).await
{
Ok(joined) => joined,
Err(error) => return Ok(OpenSessionOutcome::FailedBeforeCandidate(error)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FailedBeforeCandidate means the agent failed before it saw a candidate, not that no candidate joined. The browser joins the room and calls startRecording() as soon as /api/token answers, without waiting for the agent. So if join_room or publish_opening_attributes fails, the candidate can already be in the room with a recording starting under this interview. The dispatcher then calls release_interview_room, which puts the interview back to pending while its recording row still names the old room. A reload makes create_interview return that same interview, it claims a second room, and "one consent is one interview" no longer holds. Release only on NoCandidate, or check room.remote_participants() / the interview's recording row before treating a failure as a no-show.

WHERE id = ?1
AND account_id = ?2
AND room_name = ?3
AND NOT EXISTS (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this NOT EXISTS guard matches, the UPDATE changes nothing, but release_interview_room still returns Ok(()). The dispatcher drops the NoShowConsent and nothing tries again. That is exactly the flow this PR targets: the browser times out after 10 s, the candidate reloads, create_interview inserts a new pending row, and five minutes later the no-show release for the first room is skipped without any trace. That room stays bound for good, even though the description says it no longer is. At minimum, return whether a row changed and log the skip in release_no_show. If the claim really has to come back, retire it explicitly instead (for example, mark it spent) rather than relying on the pending-row index to allow the update.

Comment thread web/interview.js Outdated
Comment thread src/dispatch.rs Outdated
@jserv

jserv commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

#285 adds a Strict Interview mode that must never fall into offline practice: when /api/token is refused, its branch of this same catch disables the editor and shows error.message with a Retry. This PR turns a stalled token request into "The interview connection timed out. Offline practice is ready.", which in Strict mode would tell the candidate offline practice is ready while it is refused. Whichever of the two lands second needs the timeout to reach the Strict branch with wording of its own; please plan for that when rebasing.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 8 files (changes from recent commits).

You’re at about 97% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="web/interview.js">

<violation number="1" location="web/interview.js:223">
P2: This 20s browser deadline spans the server's claim and dispatch work, not just the body read. If dispatch runs past 20s, the browser shows "The interview connection timed out. Offline practice is ready." while the server still starts the interview and the agent. Consider making the browser deadline cover only the upload and response headers, and documenting that a timeout can fire during server-side dispatch.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment thread web/interview.js
// The agent reads the editor once per 2s watch tick, so publishing every
// keystroke sends ~10x more full-buffer packets than anyone consumes.
const CODE_PUBLISH_DEBOUNCE_MS = 300;
const TOKEN_REQUEST_TIMEOUT_MS = 20_000;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This 20s browser deadline spans the server's claim and dispatch work, not just the body read. If dispatch runs past 20s, the browser shows "The interview connection timed out. Offline practice is ready." while the server still starts the interview and the agent. Consider making the browser deadline cover only the upload and response headers, and documenting that a timeout can fire during server-side dispatch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At web/interview.js, line 223:

<comment>This 20s browser deadline spans the server's claim and dispatch work, not just the body read. If dispatch runs past 20s, the browser shows "The interview connection timed out. Offline practice is ready." while the server still starts the interview and the agent. Consider making the browser deadline cover only the upload and response headers, and documenting that a timeout can fire during server-side dispatch.</comment>

<file context>
@@ -220,7 +220,7 @@ let editorPaintFrame = null;
 // keystroke sends ~10x more full-buffer packets than anyone consumes.
 const CODE_PUBLISH_DEBOUNCE_MS = 300;
-const TOKEN_REQUEST_TIMEOUT_MS = 10_000;
+const TOKEN_REQUEST_TIMEOUT_MS = 20_000;
 /// Long enough to read twice, short enough that it is gone before the answer
 /// it is about. Measured against the hint text, not chosen round.
</file context>

Comment thread src/dispatch.rs Outdated
Comment thread src/dispatch.rs Outdated

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase latest main branch and resolve conflicts.

@xubo9367
xubo9367 force-pushed the fix/interview-token-timeout branch from 95fb059 to 7e92120 Compare October 23, 2026 11:08

@jserv jserv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase the current branch onto the upstream default branch and rework the series into functionally minimal commits, folding similar ones and enforcing the project's commit message rules.

Bound server and browser waits around token startup.
Release consent when a dispatched room never admits a candidate, and
preserve successful startup behavior across dispatch boundaries.
Keep claim and dispatch together after a client disconnects. Guard
release against pending or recording state, and document when browser
fallback can precede completed dispatch.
@xubo9367
xubo9367 force-pushed the fix/interview-token-timeout branch from 7e92120 to b706fc4 Compare October 23, 2026 11:38
@jserv
jserv merged commit 302a4c7 into sysprog21:main Oct 23, 2026
10 checks passed
@jserv

jserv commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

Thank @xubo9367 for contributing!

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.

Interview startup never falls back when the token request stalls

2 participants