* feat(control-ui): stream live draft previews in the typing indicator
Multi-identity sessions now show what a teammate is typing, not just that
they are typing: the composer's per-keystroke session.typing sends carry a
bounded tail of the draft (optional preview field, 400 code points max),
the gateway throttle re-emits on changed payloads at 250ms (boolean-only
stays at 1s, trailing edge keeps the latest draft), and the transcript
renders a per-actor bubble with the live text plus a blinking caret.
Actors without preview data keep the three-dot bubble.
Previews are ephemeral presence: never persisted, never part of the
session transcript or model context, excluded from aria-live regions, and
gated by the existing >=2-live-viewers, sharing-role, and incognito
checks. No new config surface.
* chore(protocol): regenerate Swift gateway models for typing preview
* fix(gateway): aggregate typing previews across same-actor connections
A boolean-only session.typing update from a second connection of the same
actor (another tab or device) erased their live draft preview, because
typing liveness aggregated per actor while the broadcast preview came only
from the latest request. Preview aggregation now lives with the connection
aggregation owner: updateTypingConnections tracks per-connection previews
and returns the newest non-empty preview among live connections, so the
broadcast keeps the active draft until its connection stops or expires.
Regression fails pre-fix (event lost its preview field).
* fix: surface Codex input prompts across runtimes
Codex structured input now reaches bounded Gateway questions in native and ACP runs, with exact turn ownership, explicit unsupported outcomes, and cancellation fencing. Consume the published ACPX elicitation support.
* chore: align elicitation helpers with current guards
Use protocol-specific helper names required by current main and update the reservation regression to the generalized input owner.
* fix: formalize structured input SDK surface
Expose one documented, frozen agent-harness structured-input contract with runtime and subpath coverage, replacing the accidental function-property API.
* fix: satisfy elicitation architecture gates
Register the real-process ACPX fixture as an executable test root and move shared structured-input types into the boundary leaf to keep Knip and Madge clean.
* fix: remove structured input lint suppression
Preserve the rejected control and invisible-character ranges with an explicit code-point check so the production suppression inventory stays closed.
* 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.
* fix(media): stop canonicalizeBase64 allocating one cons-string node per char
Per-character append built a rope of ~25 bytes per input character, all
live until the final join — a 20 MB base64 attachment transiently needed
~700-900 MB of heap and OOM-crashed the gateway on POST /v1/responses.
Validate in the same single pass but collect contiguous non-whitespace
runs as slices; already-canonical input is returned unchanged with zero
allocations. Measured: 15 MiB attachment 659 MB -> 0 MB transient heap,
4.3 s -> 0.2 s.
* fix(media): bound canonicalizeBase64 cleanup memory by input length
Review found the run-slicing cleanup unbounded for adversarial input:
alternating data characters and whitespace retains one slice object per
run (measured 421 MB of heap for an 8 MiB payload shredded to one run
per character). Replace the run collection with a single output buffer
materialized on the first whitespace and filled in the same validating
pass: canonical input still returns unchanged with zero allocations, and
any whitespace shape now costs at most one buffer bounded by the input
length (measured 0 MB heap delta, 210 ms for the same shredded payload).
Adds the many-short-runs regression test the review asked for, guarding
both heapUsed and arrayBuffers.
* refactor(media): condense canonicalizeBase64 invariant comment
Review asked for the repository's 1-3-line invariant form: keep why the
buffer is lazy and bounded, drop the implementation-history narration.
* chore: retrigger CI (flaky gateway-server shard)
* test(media): update base64 memory comment
Punchcard-Session: frost-cedar-willow-ae
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* feat(ui): unify focused presentation routes
/focus/<target> replaces unshipped standalone query links across dashboard, terminal, desktop, and native apps.
Gateway-served index assets are anchored so nested documents resolve their bundles from the Control UI base path.
* test(gateway): narrow emitted asset URLs
Fixes check:test-types TS18048/TS2322 by dropping unmatched optional captures before comparing emitted asset URLs.
* test(docs): follow centralized cloud secret guidance
Fixes the stale current-main docs test after #126132 centralized GCP and Hetzner setup in docker-vm-runtime.
* test(ui): retry missing locator reads
The 500ms locator text read can time out while the menu label is still rendering, causing expect.poll to reject instead of using its owning 10s retry window. Treat only Playwright TimeoutError as a missing value so the outer poll retries while page-closure and arbitrary failures still surface.
* test(android): capture TLS probe coroutine
The TLS probe test inferred its coroutine from mutable scope children, racing unrelated child startup and teardown in CI. Capture the exact Job from inside the probe coroutine and join that owner before asserting the stale-attempt guard.
* fix(gateway): preserve plugin focus routes
Keep approval handling ahead of plugin dispatch, but treat focus documents as an unclaimed Control UI fallback after plugin authentication and routing. Exact and prefix plugin routes therefore retain ownership, while unclaimed reads serve the focus document and other methods return 404.
* fix(ui): migrate released terminal links
Preserve stable v2026.7.1 terminal query compatibility by rewriting the root/base ?view=terminal URL once to the canonical /focus/terminal path with history.replace. Keep URL parsing path-only, and leave the removed desktop and dashboard query forms as a hard cut.
* test(codex): assign run-attempt tools shard
Cached filtered configs caused duplicate ownership, and the test lacked a canonical full-suite owner.
* test(ui): keep cloud recovery proof state-owned
The recovery test should assert owner state and reload identity, while dedicated tests own transient alert visibility.
* test(qa): wait for outbound bus state
* fix(qa): reserve gateway ports through staging
* refactor(qa): keep socket creation in gateway owner
* fix(update): validate target config before handoff
Run the existing dev candidate preflight from both managed update entry points while the serving Gateway remains alive. Reject target-incompatible config before starting a handoff or scheduling restart. Closes#126269
* fix(protocol): restore gateway barrel line budget
Route SessionsPatchResult through the existing type-only schema barrel so the public export stays intact without exceeding the core max-lines gate.
* fix(ci): restore required PR gates
Keep schema-types as the registry-free wrapper, drop a redundant project schema re-export to restore the line budget, claim run-attempt-tools in one canonical shard, and align campaign tests with the new preflight boundary.
* fix: capture GitHub identity from authenticated sign-in
Automatically persist verified GitHub identities from Cloudflare Access and Tailscale Serve while keeping public Git co-author credit as a separate opt-in.
* test: stabilize cleanup and activity capture
* fix(security): bind GitHub profiles by account id
* test: scope activity capture to route
* fix(security): gate profile requests on identity sync
* fix(security): close pending profile authorization gaps
* test(ui): stabilize terminal continuation menu
* test: stabilize startup recovery timing
* test: keep one Codex attempt tools owner
* fix(plugins): allow profile-independent gateway reads
* fix(workboard): recover interrupted worker launches
Persist prepared, accepted, and failed launch phases so Gateway restart reconciliation cannot leave cards permanently running between launch preparation and worker acceptance.
* fix(workboard): require durable terminal evidence
Do not synthesize terminal-session acceptance timing during restart reconciliation; stale same-key terminal rows without updatedAt now fail the prepared launch instead of being adopted.
The cloud machine picker described each class in prose ("Cheap smoke checks
and small repos"), which was the widest thing in the row, got ellipsized, and
did not say what the operator actually picks on. Machine options now carry the
class shape and the picker renders "32 vCPU · 64 GB" instead.
Crabbox reports per-provider class shapes from `providers --json`; the plugin
reads that catalog once per lifecycle and attaches cpu/memoryGb to each option.
A missing binary, failed command, unparseable output, absent classes, or an
unmatched provider all degrade to label-only rows, so an older Crabbox keeps
working.
`description` was never released, so it is removed rather than deprecated, and
listMachineOptions becomes async for the catalog read. Deletes the duplicate
machine-option projector in the environments server method.
The Gateway owns start-or-steer at admission (6515f6a255) and no
client produces expectedRunId anymore (d84a910fc8). The field shipped
only in v2026.8.1-beta.2 - never a stable tag - so it is removed rather
than deprecated. Steer sends resolve the selected session's current
operation; the exact-match branch, the operation|run target identity
discriminator, run_mismatch rejection, and the suggestion producers'
active-run-id selection (with its ambiguity failure) are deleted.
Provider-native turn fencing (Codex expectedTurnId) is unchanged:
the backend-captured runId on the injection target remains.
* feat(gateway): proxy channel conversation avatars
* feat(discord): capture conversation avatars
* feat(slack): capture DM sender avatars
* test(discord): bind guild avatar mock
* feat(ui): render channel conversation avatars
* fix(ui): align sidebar owner fixtures
* fix(gateway): version channel-avatar routes by media revision
A stable per-session URL let AuthenticatedAvatarRouteLoader's blob and
sticky-404 caches pin a mounted row to a stale or blank avatar after the
backing media changed. Append an opaque digest of the media reference so
replacement and 404-recovery change the route identity.
* test(ui): align sidebar owner facet
* fix(ui): keep owner chip until channel avatar loads
A session with a channelAvatarUrl suppressed its owner chip even while the
blob was loading, auth was not ready, or the route 404ed, leaving an empty
lead slot. The chip now rides as fallback content inside the avatar element
and yields only to a usable image. Covers 404 and auth-pending states;
avatar rows keep renderedOwnerId unset so an owner-viewer stays visible in
the facepile.
* perf(ui): keep channel avatar fallback within budget
* perf(ui): lazy-load the channel avatar element
The avatar element and its authenticated blob loader rode the startup
bundle through session-leading-indicator, pushing startup JS 51 B over the
CI gzip budget. Channel avatars are not startup-critical: register the
element on the first avatar row; the owner-chip fallback covers the
one-time upgrade window. Startup JS returns ~1 KiB under the ceiling.
* build(ui): raise startup baseline for channel avatars
CI-measured startup JS is 344379 B against a 343289 B baseline (+1090 B).
The avatar element and blob loader are code-split out of startup (previous
commit); the residual is the sidebar lead-slot render branch and row
plumbing, which cannot be deferred. Baseline updated via
check-control-ui-performance --update-baseline with CI bytes per the
script's contract; well inside the 4096 B ratchet step and 358400 B
ceiling.