* fix(control-ui): stop config form save from corrupting 64-bit id strings
Saving the schema-driven config form coerced every numeric-looking string
to a JS number before submission. For union-typed fields such as
tools.elevated.allowFrom.* (anyOf: string | number), string entries
holding 64-bit ids (Discord/Telegram snowflakes) were rewritten through
Number(), which rounds past 2^53:
"1048113311314608148" -> 1048113311314608100. The corruption also hit
untouched fields, because serialization coerces the whole form, so merely
saving an unrelated setting silently broke elevated-approval allowlists
(fail-closed: the real user id no longer matched).
Two guards fix this:
- coerceFormValues keeps a string that already satisfies a string variant
of an anyOf/oneOf union instead of parsing it into another variant's
number.
- coerceConfigFormNumberString refuses lossy integer parses: plain
integer text beyond Number.MAX_SAFE_INTEGER that does not round-trip
through BigInt stays a string, so pure number/integer fields fail
validation loudly instead of storing a corrupted id.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(control-ui): harden 64-bit config id preservation
* fix(control-ui): validate mixed-union scalar branches
* test(control-ui): prove real gateway id preservation
* test(control-ui): use communications route for config proof
* test(control-ui): grant config proof admin scope
* test(control-ui): reopen raw config for proof
* fix(control-ui): preserve explicit union input types
* test(control-ui): exercise union collection draft
* ci: retry flaky control ui e2e
* fix(control-ui): preserve mixed scalar branch types
* ci: retry service worker e2e
* fix(control-ui): preserve typeless string union branches
* fix(control-ui): reject lossy decimal coercion
* fix(control-ui): reject lossy pure numeric input
* fix(control-ui): preserve exact numeric branch semantics
* ci: retry checkout rate limit
* ci(control-ui): capture real gateway proof
* test(control-ui): frame config proof values
* ci: retry checkout download
* test(control-ui): prove Gateway-served production bundle
* fix(control-ui): preserve exact incremental union edits
* refactor(control-ui): isolate scalar edit session state
* fix(control-ui): keep scalar edit branch type internal
* fix(control-ui): avoid detached focus selector
* fix(control-ui): round-trip exact numeric branches
* refactor(control-ui): share exact scalar formatting
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(tasks): rank terminal tasks by completion and keep Recent terminal-only
- updateTaskStateByRunId backfills lastEventAt from endedAt for terminal
finalizers (mirrors markTaskTerminalById), keeping activity monotonic
- both taskUpdatedAt projections rank terminal tasks by the maximum
available activity timestamp, healing stale rows while preserving later
delivery/terminal-outcome events recorded after completion
- Tasks page Recent fetch filters to terminal statuses so queued/running
rows cannot starve the Recent section
Related to #100911
* refactor(tasks): normalize completion at registry owner
Absorb terminal timestamp ordering into the canonical registry lifecycle boundary, remove duplicated projection and writer policy, and prove Recent remains visible behind 200 active tasks in Chromium.
Co-authored-by: SunnyShu0925 <shu.zongyu@xydigit.com>
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
With message preview off, rows collapse to one line and the second-row icon
logic (spinner, unread dot, badges, facepile, trail) rides beside the title.
Two defects broke that:
- Long titles crushed the icon endcap: the single-line title used flex-basis
auto, so overflow shrink was distributed onto the endcap (24px -> ~7px),
clipping the status icons. Basis 0 now mirrors the two-line subtitle
contract: text absorbs all shrink before icons do.
- Native catalog session rows always rendered the two-line skeleton with an
empty details line (46px vs 30px) and never collapsed, in both preview
modes. Catalog rows never carry preview text, so they are now always
single-line.
Regression coverage: long-title endcap geometry in
chat-flow.sidebar-presentation.e2e.test.ts (fails pre-fix), and
codex-sessions.e2e.test.ts density assertions updated to the new
all-single-line contract.
* fix(ui): give embedded settings sections the shared section rhythm
The agents tab panel hosted settings sections in a bare div, so sections
stacked with zero separation; the bespoke .agents-main margin rule in
agents.css missed the nested tabpanel entirely. Descriptions also pulled
up to 4px under control-height header actions (squeezed Verify/Save rows).
Add a .settings-stack primitive to settings.css for embedded surfaces,
use it on the agent tab panel, delete the page-local margin fork, and let
section descriptions clear action-bearing headers.
* test(agents): export getRuntimeConfigSourceSnapshot from runtime-snapshot mock
Main's checks-node-compact-large shard is red: #126531 routed
provider-model-routes through projectConfigOntoRuntimeSourceSnapshot,
which reads getRuntimeConfigSourceSnapshot, and this suite's explicit
vi.mock factory did not export it (24 failures). Return null so the
projection no-ops and resolvers keep reading the provided config.
* refactor(ui): unify sidebar alerts with custodian
* refactor(ui): move custodian alert state
* fix(ui): use the defined mono token in the custodian alert card
* fix(ui): break the update-watcher import cycle
Type the watcher against a structural leaf contract instead of
Pick<ApplicationContext,...>; context.ts reaches this module through
overlays-types.ts, so naming the context type closed a madge cycle.
* fix(ui): re-arm the alert explanation when an incident recurs
Failed-automation and model-auth alerts keep one incident id across
recover-then-fail-again, so an id-keyed dedupe showed the renewed alert
and never explained it. Scope ask-once to the presentation instead.
* refactor(ui): share Control UI resource route contract
Gateway and Control UI now share browser-safe resource route grammar, while authorization and domain validation remain handler-owned.
* perf(ui): keep resource routes out of startup
Narrow browser-safe contract modules and the lazy authenticated avatar loader keep route/cache code out of the startup chunk while the stable Gateway barrel remains compatible.
* fix(ui): render one assistant reply when history and the terminal event race
A finished run's reply reached the Control UI twice: the durable transcript
row and the pane's own terminal projection shared no identity, so the
projection reducer kept both and the transcript collapsed them behind the
"x2" duplicate badge. The gateway's chat final event carries no message id,
and a durable assistant row carries no run id, so the reducer's same-run
promotion could never match them. A WeakMap side channel hid the duplicate
only when history happened to be applied before the terminal event arrived,
which made the badge look intermittent.
The pane now admits the durable assistant row of the run it is finishing
through the reducer, attributed to that run, so the durable row adopts the
terminal projection in place; the reducer refuses to downgrade a row that
already carries a transcript id back to an id-less projection.
Splits the run-terminal bookkeeping tests into a sibling file to stay under
the max-lines ratchet.
* fix(ui): bind a late reply row to its run only when it carries that reply
The terminal tombstone outlives its run by design, so attributing every
unowned assistant row to it could stamp a delayed older row with a newer
run's id; the reducer then matched that row to the newer run's terminal
projection and replaced the answer the user should see. The tombstone now
claims only the row carrying the reply already projected for that run.