Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions docs/provider-cost-and-degradation.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,20 +105,32 @@ exhausted quota rotation also qualifies, including when another interview
exhausted it. The offer expires after five minutes and the agent leaving ends
it. A candidate who leaves has 30 seconds to rejoin under the same identity,
which is how a full LiveKit reconnect looks, and a regeneration under way keeps
running through it and is published once they are back; a candidate already
gone when the interview ends gets the failure at once with no window. The
Live session is closed after the report is published, or right after the
provisional report when a window opens. While the offer is open the agent
running through it and is published once they are back. One who rejoins
before acknowledging the provisional report gets it again, since the drop may
have lost it; a candidate already gone when the interview ends gets the failure
at once with no window. The
Live session is closed while the report, or the provisional report when a
window opens, is being delivered. While the offer is open the agent
answers each request on the control topic, `report_retry` with status
`accepted` or `early` (with the seconds still to wait, which does not spend the
regeneration), and announces `closed` at expiry or when no key can ever answer;
a duplicate request during
regeneration gets no answer. The browser waits at most 140 seconds from its
regeneration gets no answer. The browser waits at most 145 seconds from its
request or the acceptance, whichever came last, so neither a reconnect nor a
lost answer cuts off a regeneration still in progress. A transient
burst followed by a schema failure offers no regeneration: the terminal failure
determines eligibility. Reloads and process restarts cannot recover the inputs.

Every report packet, provisional, regenerated or final, is delivered the same
way. The agent publishes those same bytes at most three times, each with a
five-second publication and receipt window, without another Gemini call. The
Live session closes beside the first delivery, so retries spend no Live time.
Each delivery keeps the room open for at most fifteen extra seconds and stops
waiting when the candidate leaves or the room disconnects. A candidate receipt
names the SHA-256 digest of the packet bytes. Missing receipts mean delivery is
unconfirmed, not that the candidate received no report; only publication
attempts that all fail or time out produce `report_delivery_failed`.

Quiet-pause interim reviews use that same report model and quota. A review is
eligible after 8 seconds of candidate quiet and 150 seconds from interview start,
no more often than every 150 seconds, and only after six new candidate turns;
Expand Down Expand Up @@ -190,10 +202,12 @@ Watch `codetrial dispatch_refused ... reason=at_capacity` and
`reason=finalizing`, `livekit quota:` transitions, token HTTP 429 with
`Retry-After`, `gemini report transport_failed call=... retry=...`, `gemini
report retry_unavailable` (a key rotation lost during backoff, without another
HTTP call), `codetrial report_recovery_notice_failed`, `codetrial live_usage
... outcome=billing`, and the bounded incomplete-report categories. Each
recovery window also keeps the agent and the candidate connected to LiveKit for
up to about seven minutes after the interview, which counts against
HTTP call), `codetrial report_recovery_notice_failed`, `codetrial
report_publish_failed`, `codetrial report_receipt_missing`, `codetrial
report_delivery ... outcome=acknowledged|unconfirmed|failed`, `codetrial
live_usage ... outcome=billing`, and the bounded incomplete-report categories.
Each recovery window also keeps the agent and the candidate connected to
LiveKit for up to about seven minutes after the interview, which counts against
connection-minute quota like interview time.

Raise concurrency only after checking provider minutes, Gemini limits, CPU and
Expand Down
13 changes: 13 additions & 0 deletions scripts/gen-wire-fixtures.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,19 @@ const files = {
cases: codeUpdateCases(languages),
},
"control.json": { topic: lib.topics.control, cases: controlCases() },
"report-receipt.json": {
topic: lib.topics.control,
cases: [
{
name: "received report",
// The page's own digest, so the agent test that rehashes these
// bytes checks the browser's hashing and not a copy of it.
payload: await lib.reportReceipt(
new TextEncoder().encode('{"codingScore":80}'),
),
},
],
},
"test-results.json": { topic: lib.topics.tests, cases: testResultsCases() },
"integrity-chain.json": await integrityChain(),
};
Expand Down
2 changes: 1 addition & 1 deletion src/gemini.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const WRITE_TIMEOUT: Duration = Duration::from_secs(5);
/// Bounds the close handshake the same way. The socket being closed is most
/// often the one that stopped answering, and a close that waits on it held up
/// the reconnect and the agent's exit behind a peer that was already gone.
const CLOSE_TIMEOUT: Duration = Duration::from_secs(2);
pub(crate) const CLOSE_TIMEOUT: Duration = Duration::from_secs(2);
/// How often the room loop pings. A socket nobody is writing to cannot fail a
/// write, and a paused interview writes nothing, so without a ping a peer that
/// went away there would not be noticed until something was finally said.
Expand Down
4 changes: 2 additions & 2 deletions src/livekit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3014,8 +3014,8 @@ async fn handle_data_packet(
)
.await?;

// Give the report packet a moment to leave before the agent goes.
tokio::time::sleep(Duration::from_millis(250)).await;
// Every report went out through a receipt wait, so leaving now drops
// nothing the candidate has not acknowledged or stopped listening for.
leave_room(room).await;
Ok(ControlFlow::Break(()))
}
Expand Down
Loading