Commit Graph
145 Commits
Author SHA1 Message Date
Jinwoo Hong b8f7ece200 feat(relay): keep a memory resume's basis in the cell and confirm it from one read (#30651)
* feat(relay): add the per-cell phonePath switch, effective only in reserve mode

Step 6 of the relay redesign keys every phone-path change on this switch. The cell
reports the effective value in runtime status, and the flag tool refuses phonePath=memory
unless the cell is reserve, and a reserve flip that would turn phones over unnamed.

* feat(relay): resume paired phones from one device read on memory cells

With phonePath=memory in reserve mode, a phone resume is admitted from one device
read and the cell's own seat, and takes no activity leases. A refused device closes
4401, a failed read 4503, and only a missing device row falls through to the invite
path. A phone whose host has no seat gets one assignment read: 4409 when the row
names another cell or the read fails, otherwise 4404.

* feat(relay): keep a memory resume's basis in the cell and confirm it from one read

A phone resumed on the memory path keeps its connection basis in the cell, not in
relay_connection_bases. Its confirm rereads the device row once, renews it only when
less than 29 days remain, and always answers a current-version confirm renewed:true
with the expiry the database holds. Confirms route by where their basis lives, so a
flip between accept and confirm completes either way. Retries answer from a 10-min
in-memory result map.

* fix(relay-ops): store phonePath=db as no key, so old images keep taking writes

An explicit phonePath=db left in the switch file would make the flag tool refuse every
later write after an image rollback, including a watcher's admitMode=db flip back.

* fix(relay): log a failed no-seat read, recheck the switch after the device read

A failed assignment read for a phone with no seat now logs a rate-limited line before
its 4409. A switch that turned off during the device read sends the phone down today's
path. resolveResume reuses the one-read lookup, so the expiry and grace rules have one copy.

* fix(relay): log a failed memory renewal; test flips, replays and basis cleanup

A renewal write that fails still answers the stored expiry, but now logs a rate-limited
line so a persistently failing update is visible before phones age toward re-pairing.
2026-10-19 06:27:13 -04:00
Jinwoo Hong 1a41a23419 feat(relay): resume paired phones from one device read on memory cells (#30649)
* feat(relay): add the per-cell phonePath switch, effective only in reserve mode

Step 6 of the relay redesign keys every phone-path change on this switch. The cell
reports the effective value in runtime status, and the flag tool refuses phonePath=memory
unless the cell is reserve, and a reserve flip that would turn phones over unnamed.

* feat(relay): resume paired phones from one device read on memory cells

With phonePath=memory in reserve mode, a phone resume is admitted from one device
read and the cell's own seat, and takes no activity leases. A refused device closes
4401, a failed read 4503, and only a missing device row falls through to the invite
path. A phone whose host has no seat gets one assignment read: 4409 when the row
names another cell or the read fails, otherwise 4404.

* fix(relay-ops): store phonePath=db as no key, so old images keep taking writes

An explicit phonePath=db left in the switch file would make the flag tool refuse every
later write after an image rollback, including a watcher's admitMode=db flip back.

* fix(relay): log a failed no-seat read, recheck the switch after the device read

A failed assignment read for a phone with no seat now logs a rate-limited line before
its 4409. A switch that turned off during the device read sends the phone down today's
path. resolveResume reuses the one-read lookup, so the expiry and grace rules have one copy.
2026-10-19 06:06:50 -04:00
Jinwoo Hong cb0b152409 test(relay): census every activity-lease reader for phones without leases (#30625)
* test(relay): census every activity-lease reader for phones without leases

Step 6 lets a reserve cell accept phones without writing confirmation or
splice leases. This ratchet classifies every reader of the lease table by why
that is safe, and pins the guards that keep completion and idle rehome off a
reserve cell whose host holds no source lease.

* test(relay): scan subdirectories and USING reads, and ban a bare lease table name

The ratchet now walks every production file under src, counts USING as a read, and
fails on a bare table-name value, which could reach an interpolated FROM it cannot see.
2026-10-19 05:54:32 -04:00
Jinwoo Hong a12c592f75 feat(relay): add the per-cell phonePath switch, effective only in reserve mode (#30629)
* feat(relay): add the per-cell phonePath switch, effective only in reserve mode

Step 6 of the relay redesign keys every phone-path change on this switch. The cell
reports the effective value in runtime status, and the flag tool refuses phonePath=memory
unless the cell is reserve, and a reserve flip that would turn phones over unnamed.

* fix(relay-ops): store phonePath=db as no key, so old images keep taking writes

An explicit phonePath=db left in the switch file would make the flag tool refuse every
later write after an image rollback, including a watcher's admitMode=db flip back.
2026-10-19 05:38:03 -04:00
Jinwoo Hong 7a1b5336a8 fix(relay-ops): judge a reserve cell's connections against its configured hard cap (#30668)
A reserve cell's cell-status read is refused, so it carried no
connection_hard_cap and the evaluator fell back to the 500 default:
run 39225932592 froze on reserve c7 at 731 connections under its
1000 cap. Read connection_hard_cap from the durable Terraform cells
relay-ops already parses and emit it for reserve cells.
2026-10-19 05:07:20 -04:00
Jinwoo Hong c18cb91dce fix(relay-ops): let the incident monitor sample a reserve-mode cell (#30661)
The director's cell-status refuses a step-5 reserve cell with a 404
{error: cell_in_reserve_mode}, so every monitor sample failed while any
cell was reserve. Classify that exact refusal as reserve-not-probed, skip
the refused migration reads, and judge the cell on probes, logs and the
selector instead. Any other 404 still fails the sample.
2026-10-19 04:52:53 -04:00
Jinwoo Hong cb82bc80da fix(relay): never call a reserve cell's hosts stranded when it goes existing-only (#30609)
A reserve cell keeps no activity rows and its ledger stamps last_activity_at, so the
stranded rule read its connected hosts as unserved and re-placed them.
2026-10-19 03:33:15 -04:00
Jinwoo Hong 535e995565 fix(relay): log controls a cell closes after host proof but before wiring (#30559)
* fix(relay): log controls a cell closes after host proof but before wiring

* fix(relay): log a pre-refusal hang-up as the hang-up and share printableCloseReason

* refactor(relay): move printableCloseReason into its own module to break the import cycle

* test(relay): assemble the fake pg URL at runtime so the secret scan passes
2026-10-19 00:36:10 -04:00
Neil 2c12c1edf1 refactor(runtime): move runtime/relay to runtime/mobile-cloud-relay (#30274)
The folder is the cloud relay that reaches phones; under its old name it read as the legacy SSH
relay. git mv plus import and path-literal rewrites only.
2026-10-18 01:35:40 -07:00
Jinwoo Hong 8f117e19b1 fix(relay): destroy a pooled client whose session the server ended (#29573)
* fix(relay): destroy a pooled client whose session the server ended, and mark work that never ran

pg keeps calling a client queryable until its socket's end arrives, so a client released after
57P01 (pg_terminate_backend, shutdown) or a dropped socket went back to the pool and failed the
next caller's statement with 'Connection terminated unexpectedly'. Such clients are now released
with the error, so pg-pool discards them.

Also marks failures where no statement of the operation reached the server (acquire, a failed
BEGIN, a client dead at checkout) so a caller can tell work safe to re-run.

* fix(relay): skip ROLLBACK on an ended session, drop unused nothing-ran marking

Adds 57P04/57P05 to the session-ended codes and covers 08xxx, a dropped
socket, an ordinary 23505, and a real pg_terminate_backend.

Claude-Session: f2fe8cb5
2026-10-16 11:46:22 -07:00
Jinwoo Hong 88accb2e4a feat(relay): log where the sticky lane's slot time goes, per pooled connection (#28908)
* feat(relay): log what the sticky slot spends its hold on, per pooled connection

* fix(relay): time every sticky round trip at the PostgreSQL client, pool wait apart

* docs(relay): say which round trips the sticky slot tally covers
2026-10-14 20:55:18 -04:00
Neil 39815a086f Reduce CI import, frame-capture, teardown, and shard costs (#28879)
* Avoid unused module imports and trim cosmetic test coverage

* Capture hidden popup frames and attach CDP before guest creation

* Overlap relay test projects within the existing worker budget

* Consolidate native preparation and add an opt-in transform cache comparison

* Release inherited debugger sockets during final E2E teardown

* Refresh unit scheduling weights from all five qualified ARM shards

* Avoid retaining unasserted RPC call history in the delivery model

* Use the supported unconditional background wait syntax

* Overlap unconditional output upload with native preparation

* Verify relay integration stays enabled for ordinary unit callers
2026-10-14 17:10:55 -07:00
Jinwoo Hong 818e7829f6 feat(relay): split step-5 shadow compare and dry-run logs per cell (#28693)
* feat(relay): split step-5 shadow compare and dry-run logs per cell

The 24 h per-cell shadow gate needs per-cell sticky-compare counts, per-cell
unexplained counts, and dry-run `full` refusals while the map showed room.
Both summary lines gain additive per-cell fields; existing fields are unchanged.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): count a cell-mismatch on both cells and bound per-cell maps by the fleet

- countByCell (was cells) counts a compare on every cell it involves: the
  database's cell, every seat's cell when there is no row, and both cells
  for a cell-mismatch. Drop the 64-cell cap and cellsOverflow.
- Add ReservePlacer.free and use it for the dry-run free share.
- Rename the room bound to RESERVE_DRY_RUN_ROOM_BOUND.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): count a duplicate-seat compare on every seat cell

An unexplained duplicate seat counted only on the database's cell, so a cell
whose stale feed kept a second seat could pass its own step-5 check. Count it
once on every seat cell, the same as a cell-mismatch.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): count explained cell-mismatch and duplicate-seat on the database cell only

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): key per-cell counts on prototype-free records

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): explain a duplicate seat left by a just-superseded half-open control

When the database names the newest seat exactly, every other seat has a
strictly lower epoch, and the newest seat joined within the cell's silence
close (timeout + one ping interval + the map lag bound), the older seat is
a half-open control awaiting its 75 s pong-silence close, not a fault.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): bound the superseded duplicate-seat rule on the move's age

A same-epoch rejoin resets joinedAt, so a stale older seat could stay
explained forever; the director-clock sighting of the epoch moves only
when the epoch rises.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): leave a superseded duplicate seat unexplained without an epoch sighting

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): explain a superseded duplicate seat only from a prior sighting

compare() recorded the epoch before classifying, so a first-ever sighting
(restart or eviction) read as a fresh move. Classify first and take the
move's age from the record that existed before this compare.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): also bound a superseded duplicate seat on its join age

A long-ago lower-epoch sighting dates the move to this compare, so the
current seat's join age bounds it too.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): count an ageless superseded duplicate seat as its own class

A superseded-shaped duplicate-seat with no prior sighting of the host (the
first compare since a restart) has no move age, so it is now
`superseded-unknown-age` (explained: false) rather than a plain duplicate.
The count line also carries `uptimeMs`, so the gate can discount that
class in an instance's first 100 s.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): limit superseded-unknown-age to an instance's first 100 s

Flushes are lazy, so a window has no upper bound and an uptime field on the
count line cannot date it. The compare now records when it started and
emits superseded-unknown-age only within the superseded close bound of
that; later, a missing sighting means another instance served the host or
the sighting was evicted, so it is a plain unexplained duplicate-seat.
Drops the uptimeMs field.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* docs(relay): say where a missing sighting comes from after the first 100 s

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
2026-10-14 19:15:04 -04:00
Neil 6607c54984 perf(ci): cut redundant tests and reuse expensive setup (#28669)
* perf(ci): reduce test overhead and reuse expensive build work

* test(ci): keep worker startup outside failure-case deadlines

* test: isolate push fixture advisory locks

* test(ci): preserve fixed drain retry allowance

* Align CI contracts with current triggers and guide content

* Stabilize consolidated reveal scenarios and retain behavioral CI contracts

* Fix upstream lint blockers while retaining behavior and assertions

* Classify global Jira preferences in the WSL migration inventory

* Align Jira test fixtures with startup and build safety checks

* Include package-test routing dependency in CI checkouts and fixtures

* Keep native stream classes intact in transcript dependency tests
2026-10-14 16:14:27 -07:00
Jinwoo Hong 2de7381204 fix(relay): supersede a reserve seat from the feed once a later, newer join shows (#28716)
* test(relay): check invariant 4 (one demotion winner) in the step-5 simulation

Every demotion now names its winner (the newer booking, or the row). The model
flags a director that names a different winner for a divergence another already
named, and a host whose two seats are each demoted for the other. A fault where
directors break level ties by their own cell order shows the check fires.
Violations are capped per invariant so one flood cannot hide another class.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* test(relay): bound unseen duplicates by the silence close and assert all invariants on contested ties

A reserve seat the booking director's map had not seen (joined, then half-open within a poll)
is never superseded and has no lease to re-check, so it lasts until the cell's control silence
close (75 s + one 15 s ping). Invariant 3 now uses that bound for such seats, and the
level-divergence config asserts every invariant.

Stale maps now judge a divergence another director already demoted, so the config's
"contested" counter (divergences judged by two or more directors) is the case invariant 4
guards. The silence-close constant now matches production (90 s, not 100 s).

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* test(relay): raise the floor on map-judged demotes and count only level contests

A reconcile whose map has not seen another director's demotion still demotes in
production, which raises its epoch floor; the model now does too. The contested
counter counts only same-epoch pairs, the only ones invariant 4 can split. The
keep-this-seat rule is one helper.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* refactor(relay): name the simulation's fault-kept seat check for the fault

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): supersede a reserve seat from the feed once a later, newer join shows

A half-open seat the booking director's map had not yet seen was never demoted and held its
cell until the 75 s control silence close. Directors now demote it once both joins are in the
map; the simulation holds invariant 3 at the supersede bound for it.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): outrank only past cell clock skew, reuse demoteSeat, keep invariant 3's loose clock running

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* refactor(relay): share the outrank rule with the simulation, pin the skew boundary

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): count only confirmed level demotions and every second judgment, and bound duplicates by placement records alone

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): a drained seat keeps its drain grace under a demote, and the feed reports each seat once

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* refactor(relay): name the outranked-report bound and the reconcile backstop

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
2026-10-14 17:06:33 -04:00
Jinwoo Hong 17959a90f7 feat(relay): measure each host hello's pre-auth time on the cell (#28695)
* feat(relay): measure each host hello's pre-auth time on the cell

Adds preAuthLatencyMs to the per-cell orca_relay_runtime_metrics line: per
admit path (db, reserve) and outcome (admitted, refused), a count plus
p50/p95/p99/max for the window. Each sample runs from the cell accepting the
host control WebSocket to the hello being admitted (proof verified) or
refused. Samples are kept in a bounded reservoir, shared with the control
round-trip metric. No behaviour, wire, or flag change.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): count abandoned hellos and publish mergeable pre-auth buckets

- Record a third outcome, abandoned, for a host that hangs up before a decision or before
  the cell acts on one, so the slowest hellos no longer drop out of the window.
- Add non-cumulative bucket counts per group (bounds 50..5000 ms and +Inf, 450 exact) so
  the share at or under 450 ms sums exactly across windows and cells.
- Split refusals that end before an admission check answers into a beforeCheck path.
- One refuse() closure per phase records the metric, and the path is passed to settle.
- Drop the half-used LatencyReservoir type; the shared reservoir function stays.
- Replace test casts with a typed accessor or SAFETY comments.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* docs(relay): note pre-auth path labels for late throws and the combined role

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
2026-10-14 15:35:27 -04:00
Jinwoo Hong fa5533e8cd test(relay): check invariant 4 (one demotion winner) in the step-5 simulation (#28696)
* test(relay): check invariant 4 (one demotion winner) in the step-5 simulation

Every demotion now names its winner (the newer booking, or the row). The model
flags a director that names a different winner for a divergence another already
named, and a host whose two seats are each demoted for the other. A fault where
directors break level ties by their own cell order shows the check fires.
Violations are capped per invariant so one flood cannot hide another class.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* test(relay): bound unseen duplicates by the silence close and assert all invariants on contested ties

A reserve seat the booking director's map had not seen (joined, then half-open within a poll)
is never superseded and has no lease to re-check, so it lasts until the cell's control silence
close (75 s + one 15 s ping). Invariant 3 now uses that bound for such seats, and the
level-divergence config asserts every invariant.

Stale maps now judge a divergence another director already demoted, so the config's
"contested" counter (divergences judged by two or more directors) is the case invariant 4
guards. The silence-close constant now matches production (90 s, not 100 s).

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* test(relay): raise the floor on map-judged demotes and count only level contests

A reconcile whose map has not seen another director's demotion still demotes in
production, which raises its epoch floor; the model now does too. The contested
counter counts only same-epoch pairs, the only ones invariant 4 can split. The
keep-this-seat rule is one helper.

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* refactor(relay): name the simulation's fault-kept seat check for the fault

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f

* fix(relay): count only confirmed level demotions and every second judgment, and bound duplicates by placement records alone

Claude-Session: f2fe8cb5-4467-4a25-8aab-98598b70791f
2026-10-14 07:26:27 -04:00
Jinwoo Hong 7ce15106f3 fix(relay): keep verifying host tokens from the last served keys while auth is down (#27490)
* fix(relay): keep verifying host tokens from the last served keys while auth is down

jose refetches the relay JWKS once its cache is 10 min old and rejects the token when that
fetch fails, so an auth outage turned every still-valid host token into a 4401. The cell now
falls back to the last set auth served for up to an hour, skips refetching for 30 s after a
failure, and still rejects any key that set never had. A fresh set replaces it as soon as auth
answers, so a rotation takes effect at the first successful fetch.

* fix(relay): bound stale relay keys at 30 min and document the emergency purge

Thirty minutes still covers a full auth outage (no token the stale keys could accept outlives
~16 min of one) and halves how long a rotated-out key stays trusted on a cell kept from auth.
A restarted process holds no set and refuses host tokens until a fetch succeeds; the ops doc
names that as the purge after a signing-key leak, and a test pins the fail-closed start.

* test(relay): pin the 30 min stale relay key bound (29 min accepts, 31 min rejects)
2026-10-11 14:25:29 -04:00
Jinwoo Hong 4af35473eb fix(relay): a resumed host control adopts the token its hello proved (#27487)
* fix(relay): a resumed host control adopts the token its hello proved

A rebind replaced the socket, lease and epoch but kept the predecessor's token claims, so a
host that resumed after its token lapsed (auth down past expiry plus grace) was closed 4401
"relay authorization expired" at the next heartbeat, 15 s later, and every resume repeated it.
The rebind now takes the hello's verified claims when they carry a later expiry, and a hello
that speaks for another profile or organization starts a new generation instead of resuming.

* fix(relay): a refresh never lowers the held expiry and closes only its own socket

An auth refresh now keeps the later of the held and refreshed expiry, the rule a resume uses.
A refused refresh, or one that meets an idle cutover, closes the socket it arrived on: after a
rebind, session.socket is the successor, which proved itself and must not be closed for its
predecessor's late frame. Adds an organization-mismatch resume test beside the profile one.

* test(relay): a superseded socket's refresh during an idle cutover closes only that socket
2026-10-11 14:25:11 -04:00
Jinwoo Hong 735e6d6c5d fix(relay): let a host leave a dead reserve cell without an operator fence (#27458)
* fix(relay): let a host leave a dead reserve cell without an operator fence

A host whose reserve home cell died got 503 relay_home_cell_unavailable until the
cell came back: the database path demands a committed fence for every capped cell.
On its own /v1/assign, a host now moves off a reserve cell (Postgres records it as
reserve) when the cell's heartbeat is stale and this director's seat-feed polls have
gone unanswered for 45 s, unless more than a third of the polled cells look the same.
Sweeps, migration pins and capped db cells keep the fence.

* fix(relay): guard the dead-cell verdict by region and sample size, and floor the move on the map

- No verdict when half or more of the dead cell's own region fails with it (a cross-region
  partition), more than a third of the fleet does, or fewer than 6 cells are polled. A withheld
  region or fleet verdict logs orca_relay_dead_cell_verdict_withheld, which alerts.
- The move needs the step-5 plan's epoch floor, so it mints above every seat the map knows;
  without the plan the fence is kept. A test revives the old cell's seat above the row and checks
  that the ledger never writes the row back.
- The move's log line carries the fleet and region counts.

* fix(relay): count only feed cells and young streaks in the dead-cell guards; gate the alert

- Denominators hold only cells that served this director a feed page and have capacity: legacy
  rows answering 404 no longer fill the 6-cell sample or dilute the fleet and region ratios.
- A peer counts as failing for the guards on any current streak (a failure in the last 5 s), so a
  staggered partition cannot let its first cell cross 45 s while its neighbours read healthy.
- Every withheld verdict is logged, sample included (a cell missing from the feed cells is one).
- Tests: ready=0 and enabled=0 with failing polls keep the fence (drain then death is fenced).
- The withheld metric and alert are gated by relay_dead_cell_verdict_alert (default false, on in
  staging); the alert docs now describe poll failures, not heartbeats.

* fix(relay): always sample the dead-cell candidate, so a restarted director can still judge it

A director that restarted after a reserve cell died never received a feed page from it, so the
cell was never in the sample and its verdict stayed withheld as 'sample'. The candidate now always
counts when it has capacity (the store asks only about a cell Postgres records as reserve, and only
feed images run reserve); peers still count only once they have served a feed. Tests: a restart
during a lone outage decides, a partition begun before the restart withholds by region, and the
capacity filter. The Terraform comments now say to apply the metric first and the policy minutes later.
2026-10-11 14:24:52 -04:00
Jinwoo Hong 12a8fc0166 fix(relay): report event-loop freezes and keep seat polls off the cell-list read (#27405)
* fix(relay): keep seat polls off the cell-list read and report event-loop freezes

The seat poller no longer waits on its database cell-list read before polling
cells once it has a list, so a stalled read cannot starve the polls that feed a
reserve cell's dead-man. Runtime metrics add eventLoopDelayMsMax: a single long
freeze is one sample, so p99 read 68 ms across a 134 s staging freeze.

Adds a pool test against a database that accepts and never answers, and a
real-Postgres stop/start recovery test.

* fix(relay): state the seat-poller gain precisely, justify the AddressInfo cast
2026-10-11 04:30:18 -04:00
Jinwoo Hong 43645e5274 feat(push): carry a notification's workspace server to the phone (#26516)
A desktop relays alerts for agents on its paired servers, but the push named
only the server's worktree, so a tap opened it on the desktop and bounced.
The gateway accepts an optional runtime execution host and forwards it.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
2026-10-11 02:09:36 -04:00
NeilandMatheus Breguêz 41de89a626 fix: spell source NUL separators as Unicode escapes (#26864)
Co-authored-by: Matheus Breguêz <23284276+matbrgz@users.noreply.github.com>
2026-10-10 21:04:49 -07:00
Jinwoo Hong 3c6c24f8fa test(relay): deterministic step-5 simulation with invariants 1-8 (#27040)
Rebased onto main as one commit after its parent squash-merged.
2026-10-10 19:41:33 -04:00
Jinwoo Hong a18883130f fix(relay): database sweeps and ops reads skip reserve-mode cells (step-5 census, inert) (#27046)
Rebased onto main as one commit after its parent squash-merged.
2026-10-10 19:22:11 -04:00
Jinwoo Hong fda01facc2 feat(relay): the director that booked a join writes its row about a second later (off) (#27048)
Rebased onto main as one commit after its parent squash-merged.
2026-10-10 19:11:59 -04:00
Jinwoo Hong 3f7943c411 fix(relay): cells without a seat feed stay out of shadow coverage and switch state (#27058)
Rebased onto main as one commit after its parent squash-merged.
2026-10-10 18:54:30 -04:00
Jinwoo Hong 5493c5e815 feat(relay): directors place on reserve-mode cells from memory (off) (#27047)
Rebased onto main as one commit after its parent squash-merged.
2026-10-10 02:26:12 -04:00
Jinwoo Hong abff7ab86a feat(relay): flip a reserve-mode cell back without disconnecting anyone (E-mix) (#27043)
Rebased onto main as one commit after its parent squash-merged.
2026-10-10 02:18:37 -04:00
Jinwoo Hong d488ce5bc0 feat(relay): cell reserve mode: book, admit and demote without the database (off) (#27041)
Rebased onto main as one commit after its parent squash-merged.
2026-10-10 02:11:01 -04:00
Jinwoo Hong 2f267889e8 feat(relay): step-5 reserve core: the cell's booking book and the director's placer (inert) (#27038)
Rebased onto main as one commit after its parent squash-merged.
2026-10-10 02:03:26 -04:00
Jinwoo Hong 9c41d9e8d8 feat(relay): per-cell switches for the rejection fence and the lost-reply margin (#27057)
* feat(relay): per-cell switches for the rejection fence and the lost-reply margin, and report ignored keys

rejectionFence (default on) turns the unhandled-rejection fence off without a roll.
readTimeoutMarginMs (1-60 s) replaces the boot margin per query through pg's query_timeout.
The applied log line and flagsApplied name keys this image does not know.

* feat(relay): runtime-status names the switches and values this image accepts

The flag tool checks a write against it, so it can refuse ticketCheck=enforce (or any key)
on an image that would void or drop it.
2026-10-10 01:54:00 -04:00
Jinwoo Hong 94b7936b8b fix(relay): a switch-file read cut off mid-body is asked for again (#27049)
The flag channel recorded a generation as seen before reading its body, so a
read that failed mid-body sent ifGenerationNotMatch for it on every later poll
and got 304 forever: that write was stranded until the object changed again.
The generation now counts as seen only once its body has been read and parsed.
2026-10-10 01:43:03 -04:00
Jinwoo Hong 452e44464e feat(relay): read-only /v1/admin/host-whereabouts on directors (step 3) (#26816)
On-call can ask a director where a host is: its relay_assignments row and
open migration (primary-key reads only), the shadow map's seats and recent
leaves with close codes, the compare verdict (pure classification, judged as
/v1/resolve sees the host, recording nothing), and each named cell's poll
status and applied flags. shadow is {state:'off'} when the poller is off.
Deploy, monitor and fence identities may read it; capacity and asia-proof
may not. With no user id, the map names the user; an unknown host answers
empty.
2026-10-09 18:58:10 -04:00
Jinwoo Hong 404e54d7f6 feat(relay): dark shadow seat compare on sticky verify and /v1/resolve (step 3) (#26813)
After the database answers a reconnect verify or a phone resolve, the
director compares that answer with the shadow seat map and counts the
result by class, logging one count line per route and class a minute and a
few rate-limited samples of unexplained disagreements (cell and epoch only,
never raw ids). It runs only when the seat poller is on, inside try/catch,
and never changes a response.

A map-behind mismatch is explained only while this director first saw the
newer epoch within 10 s (recorded only by the compare; classification is
pure). A seat on a cell not polled within 10 s is cell-unpolled. A null
answer for a seat on a cell placement's liveness rule calls dead is
map-only-cell-unlive, or cell-unlive-pending when its heartbeat ran out
after the last cell-list read.
2026-10-09 18:47:32 -04:00
Jinwoo Hong 8848ca95d6 feat(relay): key id on assignment leases; cells shadow-check the lease (off) (#26815)
* feat(relay): key id on assignment leases; cells shadow-check the lease behind ticketCheck=shadow (off)

Directors name the lease signing key with a kid. With the per-cell
ticketCheck switch at shadow, a cell verifies the lease a host echoes in
x-orca-relay-assignment-lease, compares it with the database answer that
still decides, and logs per-class counts and the hello's database read
time once a minute.

* feat(relay): flat per-class totals on the lease shadow line, so log metrics can extract them

* refactor(relay): the lease shadow line carries the flat classes and one database-refused count

The per-class-and-verdict map duplicated the flat classes; disagree
already names the valid-lease refusals, and dbRefused gives the total.
2026-10-09 18:40:34 -04:00
Jinwoo Hong f514cb2c66 feat(relay): dark shadow seat directory poller on directors (step 3) (#26810)
Directors can poll each cell's read-only seat feed (#26803) once a second
and keep an in-memory map of which host is seated on which cell, plus a
bounded 24 h recently-left memory. Off unless ORCA_RELAY_SHADOW_SEAT_FEED_CELLS
names cells. Applies a change at or above the seat's generation, follows
cut pages, resyncs on a gap or new incarnation, and checks the map against
counts.seats. A 404 reads as no feed; timeouts and errors keep the last
seats as unverifiable. The cell list is every cell (an existing-only cell
still seats hosts), from RelayAssignmentStore.seatFeedCells with
placement's liveness rule; completeness waits only on live cells, and a
failed list read backs off. Nothing reads the map yet.
2026-10-09 18:33:32 -04:00
Jinwoo Hong de6d49d52f feat(relay): local /ready behind the per-cell readinessLocal switch (off) (#26812)
* feat(relay): local /ready behind the per-cell readinessLocal switch (off)

With the switch on, a cell's /ready answers 200 while it listens and its
host token verifier holds keys, and reports failing probes as degraded.
The heartbeat keeps the SQL-gated verdict, so directors still stop
placing on a cell that cannot reach the database.

* refactor(relay): the app verifies relay tokens with the server's shared key set

One cached JWKS per process now serves the host-control upgrade, HTTP
token checks and local readiness, instead of a second set in the app.
2026-10-09 18:32:59 -04:00
Jinwoo Hong f0dd050a23 feat(relay): per-cell runtime switches read from Cloud Storage every 5 s (#26805)
* feat(relay): per-cell runtime switches read from Cloud Storage every 5 s

Cells read cells/<cellId>.json from the <project-id>-relay-control bucket,
start on all-off defaults, keep their applied values when the object
cannot be read, and report what they applied in the seat feed. No
consumer reads the switches yet.

* test(relay): exempt the flag channel's poll from the floating-promise census; it catches every failure

* fix(relay): release every switch-file reply the reader does not consume, and bound the read while streaming

404, 5xx, malformed-generation and lower-generation replies, and failed
metadata replies, now cancel their bodies. The object read stops and
cancels at the first byte past 64 KiB instead of buffering it whole.

* feat(relay): runtime status reports the applied cell flags for the flag workflow's read-back

* docs(relay): the switch channel reads per-cell objects only
2026-10-09 18:08:24 -04:00
Jinwoo Hong e72044a936 fix(relay): a lost database reply ends its connection within statement timeout + 10 s (#26829)
* fix(relay): a lost database reply ends its connection within statement timeout + 2 s

The request pool sets query_timeout to the statement timeout plus 2 s. A
read timeout poisons its transaction: later statements fail at once,
COMMIT never runs (even when the operation caught the error), no ROLLBACK
queues behind the dead statement, and the client is released with the
error so pg-pool destroys it. Autocommit statements destroy their client
too. The server bounds an orphaned idle backend with idle_session_timeout
30 s. A read timeout is answered 503 and never retried, and a renewal
batch that loses its reply fails whole instead of fanning out.

* fix(relay): read timeout margin 10 s, configurable, so routine 5-7 s database stalls never trip it

A whole-VM stall delays replies the server already timed. With a 2 s
margin the read timeout fired inside routine stalls, destroying healthy
clients and failing requests that succeed today. The margin is now 10 s
(ORCA_RELAY_POSTGRES_READ_TIMEOUT_MARGIN_MS), and a real-server test holds
a reply 6.5 s and requires it delivered on the same connection. The fix
level rises to 2 here, with the lost-reply fix.

* docs(relay): name the startup retry that can see a read timeout, and why it is safe

Boot open retries only an unreachable database. The director's startup
cell reconcile does retry a lost reply, which is safe: it re-runs upserts
and limit-change deletes that converge on the same rows.

* fix(relay): answer 503, not 500, when Postgres ends the session mid-request

Staging B1: pg_terminate_backend during /v1/assign returned 500 29 times.
57P01/57P02 and 08000/08003 join the transient SQLSTATEs; pg's
'Connection terminated' and ECONNRESET count only when the database layer
threw them. transaction() still never retries them.

* fix(relay): answer 503 when the pool acquire fails on a dead socket

Staging B1 re-test: a Cloud SQL restart failed PostgresDatabase.transaction's
acquire with 'write EPIPE', answered 500. An acquire failure with EPIPE,
ECONNRESET, ECONNREFUSED, ETIMEDOUT or EHOSTUNREACH is now a pool connect
failure (no statement ran), and acquire failures count as database-layer
errors. EPIPE mid-statement is treated like ECONNRESET.
2026-10-09 18:00:10 -04:00
Jinwoo Hong 37d2154765 feat(relay): read-only seat feed on cells for the directors' shadow directory (#26803)
* feat(relay): read-only seat feed on cells for the directors' shadow directory

Cells keep an in-memory log of host joins, leaves and drain notices and
serve it at GET /v1/admin/cell-seats, authenticated with the directors'
existing rehome identity. Nothing reads it yet.

* test(relay): read the seat feed through its published shape so the tests typecheck

* fix(relay): serve the seat feed's full snapshot from the change log's own seats

counts.controls drops when a socket starts closing, but the leave lands
only at its close event, so a full snapshot built from open sockets could
disagree with a reader that applied every change. The log now keeps its
own seats under the readers' generation rule and reports counts.seats.

* fix(relay): report auth-expiry drain-only and its refresh as seat-feed changes

The heartbeat's auth-expiry drain-only and an auth refresh back to active
changed session state without a feed entry, so a reader's seat state
drifted from the cell's. Both now go through the log; the refresh is a
new 'active' kind.

* refactor(relay): cut unread seat-feed fields; verify a feed poll once

fixLevel, counts.splices, counts.enforcedUnits and counts.hardCap had no
reader, so they leave before they become a cross-version contract. The
admin middleware no longer verifies /v1/admin/cell-seats polls, which the
route already gates on the rehome identity.
2026-10-09 16:58:53 -04:00
Jinwoo Hong dc69efa113 fix(relay): a database fault reaching unhandledRejection no longer kills the process (#26817)
* fix(relay): a database fault reaching unhandledRejection no longer kills the process

A rejection is fenced only when the relay's database layer threw or
rethrew it and it is a pool acquire failure, pg's read timeout, a dropped
connection, or a server error in classes 08/23/40/53/55/57/58. Everything
else stays fatal and is named by an orca_relay_process_fatal line first.
Every pooled client also keeps a permanent error listener. Fix level 2.

* fix(relay): name class 23 in the fence comment; move the fix-level bump to the read-timeout fix

R1 alone does not close the lost-reply stall, so the level rises with R2.
The fix-level comment now describes the fixed alert floor.

* fix(relay): survive and answer 503 for 25P03, the idle-in-transaction timeout a VM stall triggers

A whole-VM database stall of 6-7 s outlasts the 5 s idle-in-transaction
limit for a backend caught idle mid-transaction, and the server ends that
session with FATAL 25P03 on resume. The fence now survives that one class-25
code (the rest of class 25 stays fatal), and routes answer it as transient.

* style(relay): one transient SQLSTATE per line

* fix(relay): fence a socket reset or broken pipe the database layer rethrew

pg 8.22 rejects the active query with the raw socket error ('read
ECONNRESET'), which the message-only check let through to a fatal exit:
the 09-28 crash shape. Match ECONNRESET and EPIPE by code, gated by the
database-layer mark.
2026-10-09 16:58:33 -04:00
Jinwoo Hong 80d7e2dded fix(relay): spread drain placement over a load band instead of one empty cell (#26802)
* fix(relay): spread placement over a load band so a drain does not land on one empty cell

Placement took the single least-loaded cell, so a just-rolled cell took a
whole drain and every follow-up serialized on its relay_cells row. Directors
now pick at random among cells within 5% of capacity of the minimum, and skip
a cell that already took over 40% of its pool's placements in the last 10 s
(floor 4). It only reorders: the least-loaded cell is the fallback, the hard
cap and region preference are unchanged.

* fix(relay): size the load band's intake window for the 1,200 s drain pace

At ~0.5 picks/s per director, a 10 s window with a floor of 4 let the floor
decide and the empty cell took ~65% of a c27-sized drain. A 60 s window and
a floor of 2 keep it at 40% at both the 300 s and 1,200 s paces across five
directors. Adds a once-a-minute orca_relay_placement_load_band line (picks,
budget skips, fallbacks, band size, picks per cell).

* chore(relay): log the load-band summary at info, not warning
2026-10-09 16:58:07 -04:00
Jinwoo Hong ad7af63b20 fix(relay): throttle the connection-reservations autovacuum so a run stops evicting the shared database cache (#26370) 2026-10-08 20:35:55 -04:00
Jinwoo Hong 3062b9d8d0 fix(relay): refuse host hellos fast when the database pool is timing them out; renewals jump the queue (#26362)
* fix(relay): refuse host hellos fast when the database pool is saturated; renewals jump the queue

A reconnect herd (2026-10-06 18:34Z, 1,046 asia-east2 hosts dropped by the
load balancer at once) queued hundreds of host hellos behind each cell's
16-connection pool. They timed out after 2 s, the desktops redialled into
the same queue, and control-lease renewals starved behind them.

- Host control upgrades get an immediate 503 (Retry-After: 2) once 32
  callers wait for a pooled connection. Shipped desktops already treat
  that as a connect error and back off with jittered exponential retry.
- General work now queues in PostgresPoolPressure instead of pg-pool, so
  renewal statements (priority lane) take the next released connection.
- A renewal batch that never got a connection no longer fans out into
  one statement per host.
- hostHellosShedDelta counts refusals in the runtime metrics.

* fix(relay): shed host hellos on the pool's oldest wait, not its queue length

asia-east2 queues of 50-196 waiters are routine and keep moving, so a
32-waiter limit would refuse hellos that were going to succeed. Shed only
while the oldest pool waiter has waited 1.5 s of the 2 s acquire timeout,
when a new hello would time out anyway; a refused desktop gets the same
connect error and backoff it gets today, 2 s sooner. Rebinds over a live
control (lease rotation) are never refused.

* fix(relay): shed hellos only when a full pool queue has aged 1 s

Per review: require both an oldest waiter of at least 1 s and a pool's worth
of waiters, so one slow waiter cannot trip it, and state the real baseline
(p99 of 2 waiters on 10-07).
2026-10-07 21:18:26 -04:00
Jinwoo Hong bac9e0c2bf fix(relay): latch rehome off only on a sql failure breach that outlasts a DB stall (#26371)
A single 5-7 s stall of the shared database pushed the fleet sql failure sum
to 396 (bar 250) on 2026-10-06 and a source cell latched regional rehome off
fleet-wide. Source cells now only pause the claim on a sql-only breach; the
director's regular poll latches once the breach has been watched for 150 s.
Other safety reasons still latch at once on the cell.
2026-10-07 20:55:21 -04:00
Jinwoo Hong 48a985678b fix(relay-ops): let the pre-drain auth gate tolerate an isolated auth 5xx (#26353)
A single orca-cloud-auth 500 on the desktop sign-in callback stopped the
c13 and c28 same-cap waves before any drain. Allow up to two auth 5xx per
five-minute window while the error ratio over all auth requests stays at
or below 0.1%; a burst, or errors against near-idle auth, still freezes.
2026-10-07 20:55:17 -04:00
Jinwoo Hong a0f0ae343e fix(relay): skip idle rehome for hosts that reconnected under 3 minutes ago (#26355)
A host that wakes briefly and sleeps again never acts on the move, so it
expires after 5 minutes and holds one of the concurrency slots. The source
cell now answers busy until the control socket is 180 s old.
2026-10-07 20:55:14 -04:00
Jinwoo Hong a07f4b2cbd feat(relay): slower same-cap drain paces (15 and 20 min) for every cell class (#26350)
* feat(relay): slower same-cap drain paces (900000, 1200000) for every cell class

c28's 1,901-host drain at the 300 s pace (~6.3 hosts/s, 2026-10-07) held
database lock time over its bar for about 10 minutes. Add 15- and 20-minute
pace windows to the same-cap closed set, allowed on every cell class.

- The cell's /v1/admin/drain cap rises from 5 to 20 minutes.
- The drain script fails instead of falling back to an unpaced drain when a
  cell rejects a window above 300000, since an older image answers both with
  the same 400.
- The fast-pace US-only rule and the canary PASS rule apply only below the
  default; a canary still authorizes its own pace or slower.
- The restart-safe timeout adds the window's excess over 300000 (quiet starts
  after the last host leaves), unchanged at 300000/60000/30000; the job
  timeout rises from 90 to 120 minutes.

* fix(relay): refuse a slow drain pace before isolating a cell that cannot accept it

The cell advertises its drain pace cap on /health; the same-cap job reads it
before the isolate, so a pace above an old image's 300000 cap stops with the
cell untouched instead of isolated. Also refreshes two stale timeout comments
and pins the drain step under its one-hour ID token and the job's 70-min
remainder.
2026-10-07 19:07:02 -04:00
Jinwoo Hong 54ded3bc18 fix(relay): commit the cell counter in one round trip; cells boot without the database (#25765)
* fix(relay): commit the cell counter in one round trip; cells boot without the DB

Step 2 (option B) cell image:
- One-round-trip counter commit at acquireActivity, releaseActivity and
  activateControl: the final counter UPDATE and COMMIT go as one simple-query
  message. Server errors mean COMMIT never ran (retry as today; 22012 = no row,
  rolled back and disambiguated outside the transaction); a lost connection is
  never retried.
- Cells skip the schema apply and region backfill, so they listen while the
  database is down and turn ready on their first successful query.
- G13: rehome target connection headroom folded into the existing NOWAIT
  UPDATE, excluding the host's own reservation by key.
- fixLevel on every runtime metrics line, plus declared (not applied) cell
  fix-level metrics and alert.
- Per-desktop drain disconnect-gap measurement from existing log lines.
- Census test that fails on floating database promises; fixes two shutdown
  sites. Lock-wait sample keeps the combined role.

* fix(relay): make the outdated-image alert creatable: one PromQL condition, 1 h lookback, fixed floor

A PromQL condition must be the only condition in its policy, and alerts on
log-based metrics may look back at most 25 h. Replace the 6-day/7-day design
with relay_cell_min_fix_level (tfvars, raised by a targeted apply after each
wave) and one query: a serving cell below the floor or reporting no level,
sustained 6 h. Drops the separate without-level metric.

* fix(relay): review fixes: gap-script ordering, wider promise census, fused-path guard, row-busy as scheduled

- Drain gap script: sort closes by time (gcloud exports newest first) and
  refuse an invalid drain start.
- Census: any floating promise in relay src, including callback-discarded
  and never-read ones, with a reviewed never-rejects list.
- Test the fused counter commit through the store the server builds, so a
  wrapper that stops forwarding commitWithFinal fails CI.
- Same-cap shadow gate: a row-busy refusal (the host's own release still
  holds its row) is a scheduled 503, like an own early retry. No client
  change.

* test(relay): judge drain redials by no host refused twice, not a refusal count

The row-busy count tracks how many releases are still in flight at the
dial (80 of 180 every run at 1 s, against a bar of 90). What matters is
that the release has finished by the next dial: assert no host is refused
twice, keep the time-to-placed p95 bound.

* fix(relay): cap row-busy as scheduled at the drain-return admissions; bound the gap script's window

Shadow gate: a row-busy refusal of a drained host follows its drain-return
lane admission, so per minute only that many (plus a rounding margin of 2)
are scheduled; the rest stay non-drain, so row contention the drain does
not explain still fails the budget. Gap script: --drain-ended-at excludes
the new container's closes after the roll; later grants still close a gap.
2026-10-06 00:53:51 -04:00