Skip to content

Add reciprocal peer mock interviews - #588

Open
jserv wants to merge 7 commits into
mainfrom
mock-interview
Open

jserv wants to merge 7 commits into
mainfrom
mock-interview

Conversation

@jserv

@jserv jserv commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Adds reciprocal peer mock interviews, so two signed-in members can practice interviewing each other. It is offered like the other features once GitHub sign-in and a Google key for the silent observer are configured, with no separate enable flag. Members are identified by GitHub login plus a nickname, matched through a LiveKit lobby or a private invite, and play two rounds with swapped roles, sharing code, tests and a hint guide. Each round ends with written human feedback, and a silent AI observer adds suggestions after the second.

Transcripts and audio are saved only if a member chooses, kept separately from the AI interview reports, readable by the two members alone, and removed on request or after 24 hours. Closed sessions, code snapshots and feedback are deleted 30 days after creation by an hourly worker.

The Gemini side-call attempt and the code workspace are moved out of the existing interview code so both modes share them; the browser half stays plain web/*.js with no build step. The commits go in that order: shared pieces, service, pages, docs, then the flag removal.

Verified with ./scripts/test.sh. The last full run failed only clippy on an unused import, since fixed; clippy and the format check pass again. Desktop clippy and cargo-deny were not run. Left out: a per-account lobby limit, observer reconnect (documented as a limitation) and problem shuffling.

@jserv
jserv requested a review from ColtenOuO as a code owner November 5, 2026 01:28
@jserv jserv added the explained A maintainer has seen the contributor explain this large change label Nov 5, 2026
jserv added 5 commits November 5, 2026 09:33
The interim review and the phase judge shared one function for a
single attempt on the report keys, kept inside the large gemini module.
Another caller of the same attempt is coming, and it should not have
to reach into that module's private helpers.

Move the function to gemini/side_calls.rs unchanged. Behavior stays
the same; the existing review and judge tests cover it.

Change-Id: I84e397d70ff117debd3dd6c703ec541c5de0b5f5
The editor, line numbers, indent guides, bracket handling, history and
run results were wired directly into interview.js. A second page needs
the same editor, and copying 600 lines of it would let the two drift.

Move it into web/workspace.js behind mountWorkspace, and have the
interview page mount it. The editor behaves as before; the browser
tests that read the old handler and paint code now read the new
functions.

Change-Id: Iff887ada21e5ebedee3be31079f1e7c90f324488
Practice interviews today need an AI interviewer. Add an opt-in mode,
CODETRIAL_PEER_ENABLED, where two signed-in members interview each
other instead. Members are identified by their GitHub login and a
nickname, and are matched through a LiveKit lobby or a private invite.

A session has two rounds with swapped roles. The candidate and the
interviewer share one code buffer behind a writer lease, the server
holds the problem, tests and guide, and each member submits written
feedback that stays private until both have. A silent AI observer
joins the room, never speaks, and produces advice for each member.

Transcript and audio capture are independent choices. Both are kept
apart from the AI interview reports, readable only by the two members,
and deleted on request or after 24 hours. Room capacity counts waiting
rooms and active sessions together, and a confirmed lobby pair keeps
its reservation until it is cancelled.

The server refuses to start in this mode without GitHub sign-in and the
observer's Gemini key, and caps a session at two hours so the
observer's capture stays bounded.

Change-Id: Ie2a568c015088e6fcdff055151a9c50b8286987f
The peer service has no browser half yet. Add a lobby page where a
signed-in member sets a nickname, publishes availability, answers
pairing requests or follows an invite, and an interview page for the
two rounds: the shared workspace, the problem and guide, the writer
lease, the mute toggle, feedback, and the transcript, audio and advice
the observer produced.

Both pages are plain modules with no build step. The home page links
to the lobby only for a verified member of a deployment that turned the
mode on, and the shared results renderer can now leave out the
interview-only hint.

Change-Id: I02db219c4737e5a310c7b6910826c504305eb95f
The new mode changes what a deployment stores and who can read it,
so the contracts that describe that need to say so.

Add docs/peer-interviews.md for configuration, the session and round
lifecycle, the observer, the transcript and audio retention, and a
staging check. Extend the recording contract, the observable-delivery
policy and the provider-cost note with how peer capture differs from an
AI interview, and link the new page from the README.

Change-Id: I31971093011d80ff450520fb40a4bbdc2027b1c0
cubic-dev-ai[bot]

This comment was marked as resolved.

Reciprocal practice was the one feature an operator had to switch on
with CODETRIAL_PEER_ENABLED, and startup refused to run when that flag
was set without GitHub sign-in or the observer's Google keys.

Treat it like the other optional features instead. It is offered
whenever GitHub OAuth credentials and a Google key for the silent
observer are present, and is otherwise left out, which the session
reply already reports as peerEnabled false. The CODETRIAL_PEER_*
limits are validated either way, and an observer cap above two hours
still fails startup.

Change-Id: I39156d0c52e82952e7712f369cad4ab17332d05a
cubic-dev-ai[bot]

This comment was marked as resolved.

Verified each automated review comment against the code and fixed the
ones that held.

Security and limits: reject web-root symlinks that reach the audio
directory, classify the snapshot rate limit on the decoded path, check
the invite version, and size the snapshot ceiling from its schema.
Require both rounds to fit the hard limit and give LiveKit a departure
timeout that follows the configured reconnect grace.

Retention and data: prune finalizing sessions and paired availability
rows once their room is gone, index the audio sweep on its predicate,
and drop the unreliable transcript wipe. Advice evidence needs a real
excerpt and the verbatim check matches whole words.

Pages: order previews with a sequence number, keep polling when an
invite or session load fails, drop a rejected invite token, handle
expired transcripts and a disconnected room, and report every error
the server can send.

Tests: cover the previously untested paths and bound the waits.
Change-Id: Ie76b7e2ca665bb04b3499ef1a67d6c506da58824

@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.

3 issues found across 50 files (changes from recent commits).

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/peer-lib.js">

<violation number="1" location="web/peer-lib.js:58">
P2: The new `lease_lost` text says to reload, but the page's status line says to recover access or download unsaved code. Reloading can drop unsaved edits in this tab. Reword the message to match the recovery path, for example "Edit access moved to another tab or expired. Use Recover access to continue."</violation>
</file>

<file name="src/accounts/peer.rs">

<violation number="1" location="src/accounts/peer.rs:453">
P2: This check caps each round at half the hard maximum, but the lobby input still advertises `max="90"`. Members who enter 46 to 90 minutes will get `invalid_settings` at submit. Update the input's `max` to follow the configured limit, or have the lobby state the effective ceiling.</violation>
</file>

<file name="web/peer-artifacts.js">

<violation number="1" location="web/peer-artifacts.js:101">
P2: A transcript `not_found` also means the partner withdrew the transcript, not only that the entire practice expired. Calling `expire()` then hides still-available advice and audio and reports a false expiration; distinguish transcript withdrawal from retention expiry before clearing unrelated artifacts.</violation>
</file>

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

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread web/peer-lib.js
request_limit: "Too many requests. Wait a moment and try again.",
feedback_required: "Submit your feedback before moving on.",
lease_lost:
"Edit access moved to another tab or expired. Reload to edit again.",

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: The new lease_lost text says to reload, but the page's status line says to recover access or download unsaved code. Reloading can drop unsaved edits in this tab. Reword the message to match the recovery path, for example "Edit access moved to another tab or expired. Use Recover access to continue."

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/peer-lib.js, line 58:

<comment>The new `lease_lost` text says to reload, but the page's status line says to recover access or download unsaved code. Reloading can drop unsaved edits in this tab. Reword the message to match the recovery path, for example "Edit access moved to another tab or expired. Use Recover access to continue."</comment>

<file context>
@@ -52,7 +54,10 @@ const PEER_ERRORS = {
   feedback_required: "Submit your feedback before moving on.",
-  lease_lost: "Your partner took over the editor.",
+  lease_lost:
+    "Edit access moved to another tab or expired. Reload to edit again.",
+  round_sealed: "This round has ended.",
+  round_unavailable: "This round is not available.",
</file context>
Suggested change
"Edit access moved to another tab or expired. Reload to edit again.",
"Edit access moved to another tab or expired. Use Recover access to continue.",

Comment thread src/accounts/peer.rs
|| !["easy", "medium", "hard"].contains(&settings.difficulty.to_ascii_lowercase().as_str())
|| !["python", "javascript", "cpp", "c", "java"].contains(&settings.language.as_str())
|| settings.round_minutes == 0
|| settings.round_minutes.saturating_mul(2) > config.hard_max_minutes

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 check caps each round at half the hard maximum, but the lobby input still advertises max="90". Members who enter 46 to 90 minutes will get invalid_settings at submit. Update the input's max to follow the configured limit, or have the lobby state the effective ceiling.

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 src/accounts/peer.rs, line 453:

<comment>This check caps each round at half the hard maximum, but the lobby input still advertises `max="90"`. Members who enter 46 to 90 minutes will get `invalid_settings` at submit. Update the input's `max` to follow the configured limit, or have the lobby state the effective ceiling.</comment>

<file context>
@@ -450,7 +450,7 @@ pub(super) fn valid_settings(settings: &Settings, config: &crate::config::PeerCo
         || !["python", "javascript", "cpp", "c", "java"].contains(&settings.language.as_str())
         || settings.round_minutes == 0
-        || settings.round_minutes > config.hard_max_minutes
+        || settings.round_minutes.saturating_mul(2) > config.hard_max_minutes
         || settings.topic.len() > 80
         || rounds::eligible_problems(settings).len() < 2)
</file context>

Comment thread web/peer-artifacts.js
pending = ["processing", "observing"].includes(answer.status);
} catch (error) {
if (error.code === "not_found") {
expire();

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: A transcript not_found also means the partner withdrew the transcript, not only that the entire practice expired. Calling expire() then hides still-available advice and audio and reports a false expiration; distinguish transcript withdrawal from retention expiry before clearing unrelated artifacts.

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/peer-artifacts.js, line 101:

<comment>A transcript `not_found` also means the partner withdrew the transcript, not only that the entire practice expired. Calling `expire()` then hides still-available advice and audio and reports a false expiration; distinguish transcript withdrawal from retention expiry before clearing unrelated artifacts.</comment>

<file context>
@@ -85,15 +98,7 @@ export function mountPeerArtifacts({
-        nodes.withdrawTranscript.hidden = true;
-        nodes.audioNote.textContent = "";
-        nodes.transcriptNote.textContent = "";
+        expire();
         refreshing = false;
         return;
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

explained A maintainer has seen the contributor explain this large change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant