Conversation
…-ingress cleanup PR #52 CI (Unit Tests Node 22/24, run 35048731082) failed with: Error: ENOTEMPTY: directory not empty, rmdir '.../.imcodes' from this suite's afterAll -> rm(testHome, { recursive: true, force: true }). All 11212 tests in the run passed; only the temp-home teardown raced. This suite spins up real sqlite-backed stores (supervision task registry, delegation replies, transport queue) under a temp HOME, and a background write (e.g. a WAL checkpoint or debounced persist) can still be settling when Node's recursive rm lists the directory, recreating an entry between the listing and the final rmdir. test/store/session-store.test.ts already hit and fixed this exact race (see its comment) by passing maxRetries/retryDelay to let Node retry the removal instead of failing on the first ENOTEMPTY. Apply the same fix here rather than rerunning the job blind. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…open it When a well-known-directory quick-access request (Downloads/Desktop/ Documents) fails because the controlled node lacks Full Disk Access, the daemon now reports FILE_TRANSFER_DIRECTORY_LIST_ERROR. MACOS_FULL_DISK_ACCESS_REQUIRED instead of silently falling back to the home directory mislabeled as the requested folder. - well-known-directories.ts: distinguish "candidate exists but access denied" (EPERM/EACCES) from "does not exist" via a new resolveWellKnownDirectoryDetailed(), without changing the existing resolveWellKnownDirectory() contract or any of its 41 tests. - file-transfer-handler.ts: surface the FDA-required code from either the resolution step or a later readdir/lstat EPERM; add handleMacosOpenFullDiskAccess to reveal System Settings' Full Disk Access pane in the signed-in user's own session via resolveMacosUserSession/launchMacosUserSessionCommand. - shared/transport/file-transfer.ts: new MACOS_OPEN_FULL_DISK_ACCESS request/done/error message trio, wired through the existing validateControlledFileTransferRequest/Response dispatchers. - server: new POST /:id/macos-open-full-disk-access route (mirrors machine-file-list's gate/timeout/error handling); guard-list entry in bridge.ts so the new request type can't traverse the generic sendToDaemon path. - web: FileBrowser shows a persistent banner with a "grant it" button for this specific error instead of only the generic ⚠ indicator; machine-directory-ws-adapter now surfaces ApiError.code instead of the wrapped message so the specific error reaches the UI; new api/machines.ts helper to trigger the daemon action. Strings added to all 7 locales. Verified: daemon/server/web tsc clean; 253 targeted tests pass (well-known-directories 41, file-transfer-handler/shared/platform- smoke 29, server file-transfer routes/bridge 57, web i18n-coverage + FileBrowser 126).
…ot just BWE
Only network-side bandwidth estimation fed quality decisions: BWE
authorizes a bitrate/resolution target and the encoder just tries to
produce it, with no feedback path for "I personally cannot keep up."
On a fast, completely uncongested link, a CPU/GPU-bound encode falls
behind capture and BWE has nothing to react to, so the stream keeps
degrading (queued frames going stale) the more the user interacts,
never actually recovering to a sustainable resolution -- the
long-standing "gets worse the longer you use it" report.
- quality_ladder.{h,cc}: new ApplyEncodeBacklogPressure(bitrate,
backlog_pressure) discounts a target bitrate in proportion to a
caller-tracked local-lateness counter, floored at kMinVideoBitrateBps
and never exceeding the input. Feeding the result back into the
existing SelectQuality lands on a lower ladder rung.
- video_toolbox_h264_encoder.{h,mm}: DeliveryState now tracks a
backlog_pressure counter that climbs fast on a backpressure-dropped
submission and decays slowly on one that keeps up (so a few isolated
blips don't read as "still behind"), exposed via
VideoToolboxEncoderStatistics for observability. Reconfigure() now
discounts the network-authorized selection's bitrate by this local
pressure and re-runs SelectQuality with the selection's own
dimensions as the ceiling before applying it -- the encoder can pull
itself down a rung independently of what BWE currently authorizes,
never up beyond it.
- quality_ladder_unittest.cc: coverage for the new function (no-op at
zero pressure, monotonic non-increasing, floored, and demonstrates
landing on a lower preset id under sustained pressure).
Verified: the shared pure-C++ logic (ApplyEncodeBacklogPressure +
SelectQuality interaction) compiled and run standalone with
clang++ -std=c++17, matching the assertions added to
quality_ladder_unittest.cc. The VideoToolbox-side integration
(DeliveryState/Reconfigure changes in the .mm file) was reviewed
line-by-line against the surrounding locking/threading model but NOT
rebuilt against the pinned libwebrtc SDK or exercised on real
hardware in this pass -- that requires the full
build-worker-from-sdk.sh round-trip, which was not re-run here.
…d show each share
The headline of the per-session usage panel crammed input/cache/output
token counts into one inline text line ("· In 30 · Cache 50 · Out 20"),
with no visual separation and no indication of how much of the total each
category actually was — reported as hard to read, particularly for judging
cache effectiveness.
Render each category as its own tile (count + share-of-total percentage)
instead, via a new formatUsageSharePercent() helper shared with any future
consumer that needs a part/total breakdown of these additive token
categories (input + cache + output = total, per computeTotalTokens).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PR #52 CI's Lint job failed: src/daemon/well-known-directories.ts:440:11 error An interface declaring no members is equivalent to its supertype @typescript-eslint/no-empty-object-type WellKnownDirectoryResolutionInternal added no members over WellKnownDirectoryResolution; a plain type alias is structurally identical for every existing usage (Map value type, return types, catch handler) and satisfies the rule without a suppression comment. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…solation PR #52's Server DB Integration Tests job failed non-deterministically across two retry attempts with two entirely different tests failing each time (team-membership.integration.test.ts, then unrelated controlled-node-install-command.integration.test.ts on retry) -- the signature of cross-file pollution in the shared, un-truncated real Postgres container these integration tests all run against serially (server/test/setup/integration-global.ts starts it once for the whole job; vitest.integration.config.ts pins them to a single fork so files run one after another, not concurrently, but nothing truncates between files). The failing assertion showed team-membership's "adds by username" test resolving to a stray UUID-shaped user id instead of the user id it had just created for username 'Alice'. resolveUserByIdentifier's case-insensitive username lookup has no deterministic tiebreak when more than one row matches, and other integration files running earlier in file order create real users through the actual registration flow (password-register.integration.test.ts registers 'alice' / 'AlIcE') into this same database without leaving it clean afterward. admin.integration.test.ts and password-register.integration.test.ts already defend against exactly this by truncating `users` at the start of their own beforeEach rather than trusting whichever file ran before them left a clean table. team-membership.integration.test.ts was missing that same guard despite relying on a fixed, collision-prone username literal. Added it. Verified against a real Postgres testcontainer: the full server integration suite (43 files, 685 tests) passes with this change; ran password-register + team-membership back to back specifically to confirm no regression from the added truncation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…need it 650beef added VideoToolboxH264Encoder::Impl::Reconfigure() calls into imcodes::rd::SelectQuality() and the new ApplyEncodeBacklogPressure(), both defined in native/remote-desktop-common/quality_ladder.cc. Every one of these three test files independently hand-lists the .cc/.mm sources it feeds to a real clang++ invocation to link a sanitizer test harness against video_toolbox_h264_encoder.mm, and none of them listed quality_ladder.cc -- so every harness that links that encoder now fails with "Undefined symbols for architecture arm64" for both functions instead of compiling. Verified by actually running each affected suite locally with Xcode's toolchain (not just reading the source lists): all three link and pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…2P run PR #52's Unit Tests (macOS) job failed: test/daemon/p2p-orchestrator.test.ts > P2P orchestrator — parallel rounds > preserves completed evidence and still summarizes on partial hop failure Error: Run ... ended in timed_out, expected completed; error=post_summary_execution_confirmation_timeout This test passes hopTimeoutMs=120 to startP2pRun, mainly to make deck_proj_w2's deliberately-never-completing hop fail fast. But run.timeoutMs feeds runPostSummaryExecutionConfirmationGate's own deadline (timeoutMs * 3 = 360ms), which every successful hop -- including deck_proj_w1's -- must also clear. 360ms is well inside the scheduling/ polling overhead already measured on a loaded CI runner (~0.5s, see the sibling "does not double the configured timeout" test's July fix for the same root cause), so deck_proj_w1's own successful hop occasionally lost that race and failed the whole run before it ever reached 'completed'. This test only asserts final outcome, never wall-clock, so there's no reason to keep the timeout tight. Match the sibling fix's value (hopTimeoutMs=2000) and give waitForStatus's cap the matching headroom (15000ms, under the file's 20s default test timeout). Verified: the full p2p-orchestrator.test.ts (68 tests) still passes locally, and this test alone completes in ~2.1s. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every push to a shared branch (dev especially, taking commits from several concurrent sessions within minutes of each other) spawned its own full ~25-30 min matrix that ran to completion even after a newer commit had already superseded it. With commits landing every few minutes, this queued dozens of now-pointless runs behind the one that actually matters (the latest commit's), which is what "CI is slow" was actually measuring -- not any single job getting slower. Add the same concurrency-group-with-cancellation pattern install-smoke.yml already uses, scoped by ref so push and pull_request events land in separate groups automatically and never cancel each other. Release tag pushes are excluded from cancellation on purpose.
… machines A remote desktop popped out into its own browser window (openRemoteDesktopWindow) rendered one bare, unremovable RemoteDesktopPanel for the single machine it was opened for -- no "+", no way to bring a second machine into the same window the way the inline app and the wall window (openRemoteDesktopWallWindow) already let you. The only options were closing that window and going back to the main app, or opening yet another separate window per machine. RemoteDesktopStandalone now hosts the same tabbed RemoteDesktopWorkspace the inline app and the wall window already use, seeded with the window's own machine as the first tab. Its "+" (workspace_add) adds a second remote desktop right there. The window still closes exactly when it always did -- once every host is gone, whether closed one tab at a time or all at once via the workspace's own close-all -- so it never overstays as a live PeerConnection is torn down under it, and never closes itself while it still has something open (verified by a dedicated multi-host test, not just inference from the single-host case). Rewrote remote-desktop-standalone.test.tsx to mock RemoteDesktopWorkspace directly, mirroring the mocking pattern remote-desktop-wall-standalone.test.tsx already uses for the same component, and added coverage for adding a second host and for both ways of losing the last one. Verified: web tsc clean; remote-desktop-standalone (8 tests), remote-desktop-workspace (19), remote-desktop-workspace-state (3), remote-desktop-wall-standalone (3), remote-desktop-workspace-app-integration (3) all pass. No web lint step exists (the repo's `npm run lint` only covers src/), so no lint risk from this web-only change.
…ndow The standalone window now renders through RemoteDesktopWorkspace (so it can add more machines via the same "+"), so its panel is `embedded` like the other two entry points, not `standalone`. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…g back out 650beef wired a local encode-backlog discount into VideoToolboxH264Encoder::Impl::Reconfigure(), but its own commit message already disclosed the .mm/.h integration was reviewed line-by-line and NOT rebuilt against the pinned libwebrtc SDK or exercised on real hardware. The build published from that commit is now confirmed live on two real Macs (mini-2 and m3): imcodes-remote-desktop-worker starts, runs for anywhere from tens of seconds to a few minutes while a viewer is attached, then exits and gets respawned by the root daemon with a new generation -- a tight crash loop that gets faster the longer it runs, matching "远控 capability_unavailable" reports on both machines. Both machines have been manually pinned back to their prior last-known-good worker release as an immediate mitigation. This reverts just the runtime wiring (DeliveryState::backlog_pressure tracking, kMaxTrackedBacklogPressure, and the Reconfigure() discount path) so the next worker build cut from dev does not reintroduce the same regression. quality_ladder.{h,cc}'s ApplyEncodeBacklogPressure and its unit tests are left in place -- they're pure, portable, unit-tested C++ with no caller now, and are the right foundation to re-wire once this can go through a real build-worker-from-sdk.sh + on-device pass instead of a review-only one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… draggable ring Touch mode's remote cursor had no visible on-screen representation and no relative-drag path -- a single-finger drag panned the local zoomed view, tapping did an absolute-position click, and long-pressing anywhere fired a one-shot right-click at the touch-down point. There was nothing to grab onto, mirroring the desired "trackpad-style ring you drag around" feel the mouse-mode widget already has, just packaged as a set of buttons docked off to the side instead of a cursor overlay. RemoteDesktopPanel.tsx: - Touch mode now renders a persistent ring at the last known cursor position (reusing the existing virtualMouse state the mouse-mode handle already drives). Dragging the ring moves the remote cursor relatively, via the same beginVirtualMouseMove/onVirtualMouseMove/endVirtualMouseDrag path the mouse-mode handle uses. A plain tap on the ring left-clicks where it sits; a long-press without moving right-clicks there and flips the ring to a brief hollow "is-right" flash as confirmation. - Tapping elsewhere on the screen keeps today's absolute tap-to-click, and now additionally snaps the ring to wherever was tapped (including the existing tap-anywhere long-press-to-right-click path), so it always reflects where the cursor currently is regardless of which gesture moved it. The small fixed touch-right-click button stays as a press-and-hold fallback for a right-button drag. - Default view scale changed from "fit" (scaled to the whole screen) to "actual" (native remote pixels) -- fit is still one toolbar tap away. styles.css: new .remote-desktop-touch-ring (filled, fingertip-sized) and its .is-right variant (hollow), gated behind @media (pointer: coarse) like the existing touch-only controls. 7 locale files: new touch_ring aria-label and an updated touch_hint describing the ring instead of the old tap/long-press-only flow. remote-desktop-panel-mobile.test.tsx: coverage for the actual-size default, ring drag-to-move without an extra click, tap-to-click, long-press-to-right-click with the visual flash, long-press cancellation on drag, and the ring following a tap elsewhere on the screen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…downs Removes 'qwen' from the agent-type options offered for a project's default coder/auditor role in AddProject and ProjectSettings — part of hiding Qwen as a selectable provider on the frontend. Also extracts the two pages' identical, hand-duplicated agentTypes array into a single PROJECT_ROLE_AGENT_TYPES constant in session-agent-options.ts so they can no longer drift out of sync with each other. This does NOT touch the Qwen entry in SESSION_AGENT_CHOICES (the New Session / sub-session picker) — that surface has ~20 existing tests across NewSessionDialog.test.tsx and StartSubSessionDialog.test.tsx that click a rendered Qwen button to exercise real, substantive behavior (presets, model discovery, thinking level). Hiding it there requires deciding whether that coverage should be preserved via some access path or removed along with the button, which is a product decision pending user input. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…set below it The touch-mode ring landed exactly on top of the logical cursor position, so a dragging finger sat directly on whatever it was about to click -- defeating the point of a visible cursor for aiming. Bind the ring to the same existing cursor marker mouse mode already draws (.remote-desktop-virtual-pointer, at the true click position) and draw the ring TOUCH_RING_OFFSET_Y_PX (72px) below it instead of on top of it. The interaction math needed no changes: beginTouchRing/onTouchRingMove already compute a pure finger-movement delta against virtualMouseRef's true position, and sendVirtualMouseClick already clicks off that same true position -- both are correct regardless of where the ring is visually drawn. Only the ring's rendered top offset, and the addition of the marker itself in touch mode, changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Marks the qwen SessionAgentChoice as hidden and renders it with
display: none in both NewSessionDialog and StartSubSessionDialog, instead
of removing the entry from SESSION_AGENT_CHOICES. A user can no longer see
or click a Qwen card to start a new session/sub-session, but the choice
stays a real DOM node and the underlying data/behavior is fully intact:
existing qwen sessions, presets, and settings UI are unaffected, and the
supervision-pool worker-type picker (StartSubSessionDialog reused via
app.tsx's allowedAgentTypes={getSupportedSupervisionBackendOptions()}) still
legitimately offers qwen as a backend -- a hidden choice explicitly
allow-listed there is a deliberate exception and renders normally.
Kept all ~20 existing qwen-selection tests passing with their scenarios and
assertions completely unchanged. NewSessionDialog.test.tsx needed zero
changes (it already selects agents via a raw data-agent-type query, which
display:none doesn't affect). StartSubSessionDialog.test.tsx's 5 tests that
used getByRole('button', { name: /qwen/i }) needed their query mechanism
swapped to the same raw attribute query: `hidden: true` alone isn't enough
here, because ARIA accessible-name computation excludes text from
display:none descendants regardless of that option -- it only bypasses the
"is this role excluded" gate, not name computation. No test scenario,
assertion, or coverage was removed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Actual-size (1:1 native display pixels) made pinch-zoom/the +/- zoom buttons feel broken: REMOTE_DESKTOP_MIN_ZOOM is 1, and "actual" size IS already scale 1 in the geometry that clampRemoteDesktopViewport works from (video.offsetWidth is the display's literal pixel width before any viewport transform). That leaves no room to zoom out from the default -- you can only pinch/tap further IN, up to 4x, never back out to see the whole screen, since there's nowhere below 1x to go. "Fit" doesn't have this problem: its scale-1 baseline already shows the entire remote screen (the CSS scales the video down to the stage before the viewport transform runs), so pinch/zoom-in is a genuinely useful escape hatch from there, and zoom-out is correctly unnecessary at the default. "Actual size" stays one toolbar tap away for whoever wants native pixels and is fine with cropping/panning to see the rest of the screen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… while pressed The ring had a permanent semi-transparent blue fill, so it always obscured whatever remote content sat behind it. Idle it's now fully transparent (just the white outline stays visible); while actually touched/dragged it fills a translucent light green (still see-through, not solid) as press feedback. The long-press-armed right-click flash (.is-right, hollow white) is unchanged and still takes priority while it's showing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Every reconnect reset the view to the default (fit, 100%), forcing a
manual re-adjustment every time even though the right scale for a given
remote machine rarely changes session to session.
remote-desktop-zoom-preference.ts: new small localStorage-backed module
(keyed per machine serverId, versioned, fail-soft) following the same
shape as the existing subsession-desktop-layout-preference.ts, storing
{ viewScale, scale }.
RemoteDesktopPanel.tsx:
- viewScale and viewport now seed from this machine's saved preference
(if any) via lazy useState initializers instead of the bare defaults;
viewportRef seeds from viewport's own resolved value so the two never
start out of sync.
- The existing mode/display viewport-reset effect unconditionally reset
to the default on every run, including its first (mount) run -- which
would have discarded the just-restored scale immediately. It now
treats the run that first sees a real selected display as still part
of initialization (a fresh connection's own snapshot lands
asynchronously, after the effect's literal first run), preserving
whatever viewportRef was seeded with there; only a later, genuine
display/mode change resets to the default as before.
- A new debounced effect (400ms, skipping its own first run) saves the
current { viewScale, scale } once it settles, so a live pinch gesture
doesn't write on every frame.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ith a reconfigure cooldown Re-lands 650beef (reverted in 7622ef2 after it crash-looped in production): local encode backlog now discounts the network-authorized bitrate again, via DeliveryState::backlog_pressure and Reconfigure(). This is what fixes "画面变化大的时候就会出延迟" (lag when the picture changes a lot) -- a big scene change makes each frame more expensive to encode even though VideoToolbox's DataRateLimits already caps output bytes/sec regardless of content, so a CPU/GPU-bound encode falls behind capture with nothing (BWE has no visibility into local encode time) pulling the resolution/bitrate down to let it catch up. DeliveryState's 2-frame max_pending_frames then just drops the overflow outright, which reads as stutter/lag, not gradual degradation. What's different from the reverted version, addressing the most likely mechanism behind the crash loop: Reconfigure() previously re-ran SelectQuality on every call once backlog_pressure was nonzero, and a pressure value oscillating near one of quality_ladder's fixed bitrate thresholds (rises +2 per drop, decays -1 per accept -- easy to hover under real, mixed accept/drop traffic) could flip the chosen resolution/fps back and forth on nearly every Reconfigure() call. Each flip is a full VTCompressionSession rebuild (a resolution/fps change, unlike a bitrate-only one, does not take the cheap same-session path a few lines below). Repeatedly tearing down and recreating the hardware-backed compression session under sustained load is exactly the kind of thing that would only surface after several minutes of an active viewer -- matching what was actually observed on real hardware. video_toolbox_h264_encoder.mm now rate-limits backlog-driven resolution changes specifically (kBacklogResolutionChangeCooldown, 1.5s) while leaving bitrate-only discounts uncooled (cheap, same session). This was not conclusively proven to be the crash's root cause -- the original revert was defensive, not a confirmed diagnosis -- but it is a real, independently-justified hardening against reconfiguration thrashing regardless, and directly targets the most plausible mechanism found on this re-review. Verified beyond the original commit's own disclosed gap (reviewed only, never built): downloaded the pinned macOS arm64 libwebrtc SDK release (libwebrtc-sdk-macos-arm64-f99aaf4a8c5baa9e-..., matching this repo's committed lock file), ran the same install-libwebrtc-sdk.mjs -> verify-sdk-lock -> build-worker-from-sdk.sh sequence CI uses, and ran each produced component the same way CI's own smoke step does (all four exit 64/EX_USAGE on an unrecognized flag, not a crash). This was NOT exercised under a real, sustained capture/ encode/viewer session -- that would need the full P2P session stack live, which this pass did not attempt -- so the specific "minutes of active use" crash window is still not directly reproduced either way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…se sessions Touch mode is the default input mode on every platform, including a desktop app session driven by a real mouse -- the ring itself is already correctly hidden outside @media (pointer: coarse), but the cursor marker paired with it (.remote-desktop-virtual-pointer, reused from mouse mode's own always-shown marker) had no such gate. On a desktop session it rendered unconditionally, frozen at the stage center since nothing there ever drags the (invisible) ring to move it -- exactly the "多了个大鼠标在中间...也不动" (an extra big cursor in the middle that doesn't move) report. Gives the touch-mode instance of the marker its own modifier class (is-touch-ring-marker) and hides that specific combination outside @media (pointer: coarse), same as the ring. Mouse mode's own use of the bare .remote-desktop-virtual-pointer class is untouched -- that marker is an explicit user choice and stays unconditionally visible on any device. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…on SDK
Adds the Linux counterpart to native/{macos,windows}-remote-desktop's
libwebrtc-sdk.gni/sdk.BUILD.gn/sdk_anchor.cc/build-libwebrtc-sdk.sh: a
pinned WebRTC checkout built once against a curated dependency list and
archived, the same "immutable SDK producer" pattern the other two
platforms already use.
Curated deps (libwebrtc-sdk.gni), not `//:webrtc`: Linux has no
Apple-style monolithic root target, so gn gen --root-target= needs no
transient patch to WebRTC's own root BUILD.gn visibility list the way
the macOS producer requires. The list also carries
builtin_video_encoder_factory, which the Windows list does not: macOS
and Windows inject an OS-native HARDWARE H.264 bitstream and need no
encoder factory of their own, but the initial Linux worker has no
bespoke hardware encoder yet, so it will register libwebrtc's own
builtin factory (VP8 always available; H.264 only if OpenH264 is
enabled) and let it encode frames captured off X11 directly -- deferred
work, not yet wired to anything.
Verified beyond "the ninja build exits 0", end to end on a real Ubuntu
24.04 x86_64 host: ran the full checkout -> gn gen -> autoninja ->
package pipeline this script implements, then took ONLY the produced
artifact directory plus its own recorded sdk-compile-flags.json (no
paths back into the build directory) and used them to compile and link
a translation unit that calls webrtc::CreateModularPeerConnectionFactory
against libimcodes_linux_libwebrtc_sdk.a -- it ran and returned a
non-null factory. Caught and fixed one real bug this way: the
--sysroot= flag needed rewriting from the anchor's cflags_cc (glibc
headers for the pinned Debian sysroot), not cflags where the macOS/
Windows-derived first draft only checked -- without it every consumer
compile failed deep inside <iosfwd> with "reference to unresolved using
declaration" for mbstate_t, a failure mode that pointed nowhere near
the actual cause.
Deliberately not yet done, scoped as follow-up: a native/linux-remote-desktop/
libwebrtc-sdk.lock.json and the native/linux-remote-desktop/BUILD.gn worker
product target, and wiring a "linux-x64" entry into
scripts/libwebrtc-sdk-targets.mjs (shared with the macOS/Windows
publish/verify/promote pipeline -- not touched here to avoid guessing at
its requiredFiles/noticesFormat contract for a platform that has never
been through it).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ion end to end
Verified, for the first time, that Linux remote desktop's media pipeline
actually connects: real X11 screen capture -> libwebrtc's own builtin
video encoder (no bespoke hardware encoder for Linux yet, per
libwebrtc-sdk.gni) -> a genuine PeerConnection (offer/answer, ICE
gathering, DTLS-SRTP) -> decode -> a correctly-sized VideoFrame received
on the far end. New qualification binary
test/spec/linux-remote-desktop-webrtc-loopback-qualification.cc proves
this on-host against a real (or Xvfb) X server, the same way the
existing X11/adapters qualification binaries do, using the Linux SDK
native/linux-remote-desktop/build-libwebrtc-sdk.sh now produces.
linux_native_video_source.{h,cc}: bridges the existing X11CaptureAdapter
(a CapturedFrame push callback) into libwebrtc's OWN video pipeline via
common::NativeCaptureAdapter/NativeVideoSourceLease -- the delivery
model platform_interfaces.h already documents Windows using (a pooled
VideoTrackSource + a platform VideoEncoderFactory), not macOS's
H264-access-unit-injection one. Converts captured BGRA to I420 with
libyuv and pushes it through webrtc::AdaptedVideoTrackSource; from there
it is an entirely ordinary WebRTC video source.
linux_x11_backend.{h,cc}: fixes a real gap the loopback qualification
surfaced immediately -- X11CaptureAdapter::Start() delivered exactly one
frame, ever, and never captured again. Fine for the existing
qualification harnesses (which only ever check that capture works at
all), completely inadequate for an actual session, which needs a live
video feed for as long as it runs. Start() now keeps the existing
synchronous first frame (so a caller learns immediately whether capture
works) and adds a 30fps background poll thread, joined in Stop()/the
destructor. Also calls XInitThreads() once before the first
XOpenDisplay -- required once capture polls on its own thread against
the same shared Display* the main thread drives input/clipboard calls
through.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ession Adds LinuxRemoteDesktopSession, implementing common::TransportSessionAdapter -- the SAME shared route-authority/ICE-queueing/data-channel-readiness/ quality-target/watchdog/diagnostics state machine (common::TransportSessionCore) macOS's MacosRemoteDesktopSession and Windows' PeerSession already drive, now doing the same job for a real libwebrtc PeerConnection over the X11 adapters. StartTransport creates the PeerConnection, acquires and starts a video capture lease via the LinuxNativeCaptureAdapter/AdaptedVideoTrackSource bridge from the previous commit, and adds the resulting track. ApplyOffer/AddRemoteIce are the real, externally-driven signaling entry points a future worker would call with whatever the daemon's own signaling channel delivers -- not the earlier loopback qualification's in-process peer. New qualification binary test/spec/linux-remote-desktop-session-qualification.cc proves this: it drives the session through ONLY its public API from a separate "client" PeerConnection standing in for real signaling, and verifies a real decoded video frame arrives -- stronger than the previous loopback qualification's proof that the codec pipeline works in isolation, this proves TransportSessionCore itself correctly drives a real connection through the Linux adapters. Caught and fixed one real bug this surfaced immediately: TransportSessionCore queues both local and remote ICE candidates until SetLocalIceEmissionReady()/ SetRemoteDescriptionReady() are called explicitly (so a candidate is never handed to the PeerConnection or the far end before there is an SDP to resolve it against) -- neither call site is obvious from the adapter interface alone, since neither is one of TransportSessionAdapter's own virtual methods. Without them the qualification connected nothing: candidates queued forever, ICE never started. Now called from ApplyOffer, at the point each is actually safe: SetRemoteDescriptionReady once the offer's SetRemoteDescription completes, SetLocalIceEmissionReady once the answer's SetLocalDescription does. DELIBERATELY NOT YET DONE, unchanged from the previous commit's scoping: the data-channel wire protocol (pointer/keyboard/clipboard JSON messages) and the daemon-side worker process/challenge/generation protocol. Channels open and are tracked by TransportSessionCore for readiness; incoming messages are not yet parsed or dispatched to the input/clipboard adapters -- this is why the qualification's client adds no data channels and required_channels_ready correctly reads 0 in its printed diagnostics. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ne readiness gap
Adds common::SessionCore (the same input-ledger-backed ApplyPointerMove/
ApplyKey/ApplyButton/ClickButton/ApplyWheel/ApplyText/ReleaseController
dispatch macOS's MacosRemoteDesktopSession wraps) to LinuxRemoteDesktopSession,
backed by the already-qualified X11InputAdapter -- a real, independently
correct surface, even though nothing calls it yet (the data-channel wire
protocol that would is still deliberately deferred, per the previous commit).
common::PlatformAdapters requires an EncoderAdapter reference; Linux has
none (frames leave through NativeCaptureAdapter's pooled VideoTrackSource,
never CaptureAdapter/EncoderAdapter's push-a-frame/emit-an-access-unit
model). New LinuxNoopEncoderAdapter satisfies the type requirement --
SessionCore only ever calls Stop() on it, so a no-op is exactly correct,
not a stand-in for missing behavior.
That no-op's ProbeReadiness() is honestly kUnavailable, though, and that
surfaced a real, previously invisible gap immediately: CapabilityReadiness::
ViewReady() (value_types.cc) requires BOTH encoder and disclosure to read
kReady, not just capture/input/display. Linux's disclosure adapter also has
no real surface yet ("no Linux surface in this slice" in
linux_platform_adapters.h). So SessionCore::Start() now fails on every real
Linux host, not just this qualification's Xvfb one -- left failing loudly,
logged, non-fatal to the transport/video that already work, rather than
made to falsely report encoder/disclosure readiness to paper over it.
That gate needs an actual answer (a real Linux disclosure adapter, or a
documented case for why Linux's control-readiness shouldn't require one)
before SessionCore can honestly report kViewing here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ViewReady/topology gaps
SessionCore::Start() failed on every real Linux host because two
independent gates were never honestly satisfied, not because loosening
either gate was actually warranted:
- CapabilityReadiness::ViewReady() requires disclosure == kReady, and the
old LinuxDisclosureAdapter was a permanent-kUnavailable stub. Replaced it
with X11DisclosureAdapter: a real override-redirect top-right X11 window
("this session is being watched/controlled") shown for as long as a
viewer/controller is attached, using XCheckWindowEvent (not
XNextEvent/XPending) so its redraw loop never steals events belonging to
the capture/input/clipboard adapters sharing the same Xlib connection.
- LinuxPlatformAdapters::MeasureReadiness() never populated
readiness.graphical_session at all, leaving it at kUnknown forever
regardless of how ready everything else was. Now sourced from
SessionFacts::graphical_session_present.
- Independently, DesktopTopology::IsValid()/DisplayTopology::IsValid() both
require a nonzero `generation`, which X11DisplayAdapter::EnumerateTopology
never set (only `revision` was ever incremented). Added a WorkerGeneration
member matching Windows' ToCommonDesktopTopology pattern.
- LinuxNoopEncoderAdapter::ProbeReadiness() now returns kReady with a
documented reason (this delivery model has no separate encoder object to
report on) -- though the comment now also notes MeasureReadiness() never
actually calls it, mirroring readiness.encoder from readiness.capture
instead, so this is a self-consistency fix, not the mechanism that
actually satisfies the gate.
Verified via test/spec/linux-remote-desktop-session-qualification.cc on a
real Xvfb host: SessionCore::Start() now succeeds (no more "SessionCore::
Start failed"), input dispatch (ApplyPointerMove/ApplyKey/etc.) is live,
and repeated runs plus the separate webrtc-loopback qualification both
exit clean with no regressions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…lder The AI Desk auto-unlock password input always showed "Windows sign-in password" as its placeholder/aria-label, even when configuring auto-unlock for a macOS machine. A person configuring it on a Mac node had no in-UI indication this field wanted their Mac account password specifically, which risks the wrong value (or a leftover placeholder-looking string) getting saved as the stored secret -- indistinguishable, from the outside, from the unlock mechanism itself being broken: it just types the wrong password every time and macOS silently rejects it, with no error surfaced back to the operator by design (this feature deliberately never reveals whether a stored credential was right or wrong). Adds a mac-specific placeholder string (all 7 locales) and picks it based on machine.os, alongside the existing Windows-worded one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…red producer registry The Linux libwebrtc SDK producer (build-libwebrtc-sdk.sh) built a real, working SDK, but wasn't reusable through the same install/publish/promote pipeline macOS and Windows already share (scripts/libwebrtc-sdk-targets.mjs and friends) -- it emitted an sdk-build.json in a shape the shared verifier never recognized, and had no third-party notices file at all. - Added a linux-x64 entry to scripts/libwebrtc-sdk-targets.mjs: source fingerprint inputs, required staged files, the exact GN build args string, and a 'linux-sections' notices format (reuses the same generic THIRD_PARTY_NOTICES.webrtc.md section validator Windows already uses -- nothing macOS-specific needed). - Added generate-libwebrtc-sdk-notices.py: a real, working notices generator, structurally the same approach as the Windows generator (reuse upstream WebRTC's own license mapping/renderer, but discover linked third-party trees from the archive's own Ninja edge instead of asking GN for every field of every transitive target). Kept as its own file rather than an import of the Windows generator: that file is a fingerprint input for an SDK release already published, and editing it for Linux's sake would rotate that immutable release's identity for nothing. - Rewrote sdk-build.json emission to the registry's actual manifestVersion 1 contract (os/arch/libwebrtcRevision/depotToolsRevision/buildArgs/ toolchain), generated via json.dump instead of a shell heredoc -- the heredoc interpolated GN_ARGS's own embedded double quotes unescaped, producing invalid JSON. - Staged toolchain/bin/ld.lld (a real copy, not a symlink) so a consumer's -fuse-ld=lld invocation finds the name clang's Linux driver actually looks for, instead of every consumer needing its own workaround symlink. - Dereferenced the staged Debian sysroot (cp -rL, not -a) and pruned packaging-only trees (debian/, .stamp, var/lib/dpkg, var/cache/apt, **/systemd/**) plus any zero-byte file: the raw sysroot tarball is Chromium's own sysroot-creator.py output (real .deb packages installed into a rootfs), not a hand-picked compile surface, and it shipped several files the shared manifest validator's general corruption checks correctly reject -- a systemd unit using Debian's own \x2d escaping convention produces a literal backslash in its filename, and Python's own empty __init__.py/py.typed markers are zero-byte. None of this is needed to compile against these libraries; pruned rather than either check loosened, since both are legitimate general-purpose rules other platforms rely on. Verified end to end on a real Linux host (not just the JS validators): built the SDK from the existing pinned checkout, ran create-manifest/ verify/create-lock against the real staged output (all pass), and recompiled + relinked + reran the actual TransportSessionCore-driven session qualification test against the newly-staged SDK -- identical result to the SDK built before this change (video flows, no regressions). Also ran the existing libwebrtc-sdk-artifacts/cli-entry tests plus the full macOS SDK notices/producer/consumer suite: all still pass unchanged. Publishing an actual GitHub Release for this target (and pointing CI/ install-libwebrtc-sdk.mjs at it) is a separate, heavier step intentionally left for explicit follow-up -- this change makes the SDK a first-class, verifiable registry target, not yet a shipped one. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…less desktop installer
Two related pieces, tested end to end on a real headless Ubuntu box
(no desktop, no sound card) with no prior GUI setup:
- scripts/install-linux-desktop-environment.sh: apt-based bootstrap for a
box with no desktop at all -- Xvfb virtual display, openbox window
manager, plank dock, lxterminal, Firefox (a real Mozilla .deb, not
Ubuntu's transitional snap wrapper), all wired up as persistent systemd
services. --with-vnc also installs and starts x11vnc against the same
display. Also installs pulseaudio + loads snd-dummy: a fully headless box
has no /proc/asound/cards entry at all, which hard-fails WebRTC's
AudioDeviceModule::Init() at real session start -- discovered while
qualifying the rest of this change on exactly such a box.
- linux_vnc_backend.{h,cc}: a real RFB (VNC) protocol client
(VncCaptureAdapter : common::CaptureAdapter) for hosts where this
process has no X11/XTest access of its own but a VNC server is already
reachable -- reusing an existing setup rather than requiring one.
RFB 3.3-3.8 version handshake, security types None and VNC Authentication
(the classic DES challenge-response), Raw encoding, full-frame polling.
Includes a standalone DES implementation (the only place DES is needed
anywhere in this codebase) validated against the published FIPS 46-3 test
vector, and DecryptVncPasswordFile() for the classic ~/.vnc/passwd
format -- decrypt, not decode: the RFB spec's own storage format uses a
fixed, publicly-known DES key, so anyone who can read the file can
already recover the plaintext password this same way.
Wired into LinuxPlatformAdapters::Create() as a strictly-last-resort
fallback: Portal/PipeWire, then direct X11 capture, then VNC only when
neither of those is ready. VNC is deliberately never preferred over a
working direct-capture path -- it adds a whole second RFB encode/decode
round trip before this process's own encoder ever sees a frame, which is
real added latency and CPU a host capable of Portal or X11 has no reason
to pay. CaptureBackend gained kVnc; SelectCaptureBackend's pre-session
advertisement policy does NOT yet know about it (documented as a known
gap in its own header comment) since detecting a real VNC server needs a
live TCP probe, not the side-effect-free fact lookup that function is
built around.
Verified for real, not just compiled: DES self-check against the FIPS
vector; connected to a real x11vnc instance with security type None;
created a real x11vnc -storepasswd password file, decrypted it back to the
exact original 8-character password, and authenticated against a second
x11vnc instance requiring VNC Authentication with that recovered password;
confirmed direct X11 capture is still chosen over VNC even when both are
simultaneously available and ready on the same host (the actual priority
question this whole change exists to answer); ran the full, real
TransportSessionCore-driven session qualification test on the freshly
bootstrapped box end to end (video flows, no regressions on the existing
webrtc-loopback qualification either).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…xplicit settings surface The settings dialog reaches applied only once the daemon reports the requested model (bounded 15s confirmation timeout, disconnected error); sub-sessions update requestedModel/activeModel/modelDisplay locally like main sessions; worker-role main sessions with process codex use the main-session model command (subsession.set_model only accepts deck_sub_*); surface is a required prop, no combined default. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…amble headroom; MCP/orphan/timeline test gaps audit-reply decodes structure only and lets the daemon gate decide; preamble headroom target raised to 400 B with shorter brief clauses; wiring tests for the mcp-backend prefix and orphan-sweep provider; bootstrap tests for in-flight backend exit, late reply drop and short tool timeouts; injectable pid handle check; drain-eviction guard test; bounded test-only OpenSpec reset drain. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…de catalog
Daemon emits {code, params} with the English text kept as fallback; the web renders chat.daemon_notice.<code>
in 7 locales and keeps the actionable detail (bounded, redacted) for notices that carry diagnostics. 52 codes,
guard test against bare display strings.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…needs-input resume, single-card audit result, indexed pool count Bounded 15s console subscribe timeout to stale/error with Retry; task heartbeat_at advances with its assignments; a plain user reply lifts the needs-input pause projection at once; the timeline eventId of an exact audit record is a stable identity hash so a late verdict replaces earlier prose on one card; per-pool active lease count is a single indexed query; idle glyph class, lifecycle listener and watchdog tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…results Round is derived from the registry as the ordinal of the distinct final audit attempt per task (idempotent for replayed receipts), carried on peer_audit.result, the Brain notification and the delegation.reply payload, and shown as an Rn chip beside the outcome (7 locales). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…rdict wins same-eventId prose One shared PeerAuditRoundChip used by peer_audit.result and delegation.reply cards; the timeline merge lets a daemon-authored high-confidence PASS/REWORK delegation.reply replace a same-eventId non-verdict event so the late exact verdict keeps its round; registry test for multiple final receipts of one attempt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…bounded retention for scratch and bundles The worktree GC no longer requires task-level finalization evidence per assignment worktree, treats recovered as terminal, and no longer lets an active sibling or an idle persistent session name shield finished worktrees. Cancelled/recovered assignments keep a 24h handoff grace while the task is still running and their completion evidence is unresolved. Scratch dirs and finalized immutable bundles get bounded, realpath-confined retention with runtime overrides, surfaced in housekeeping results and logs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…hort retention for merged bundles and scratch Worktrees whose task is finalized and pushed are deleted immediately without a backup patch, even when dirty; unmerged or uncertain owners still back up fail-safe. Legacy backup patches are purged by age; bundles of finalized tasks are reclaimed after a short window and their scratch after a day (all runtime-overridable). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…resh-history button The 5h and 7d quotas share one compact wrapping line; the audit heartbeat superscript gets semantic toolbar layers and is no longer covered by sibling controls; the execution-clone launcher next to stats is hidden behind a flag and a refresh-chat-history button (existing timeline forceRefresh, pending/error feedback) takes its place. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…he quiescence tests The 20ms marker poll could consume the marker before the read gate existed, leaving gate.started producerless and the test burning the full 120s cap on macOS CI. Arm the gate first, bound the gate-start wait to 10s with live diagnostics, and release deferreds in finally in both quiescence tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… sidebar footer The footer DaemonRemoteDesktopControl mount is removed; the toolbar, mobile server-actions, per-machine menu, controlled-nodes panel and wall entries remain. The footer status row reflows with dedicated classes and the watch-sync button stays at the far right. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ased A delayed macOS FSEvent about the guard own seed write no longer counts as a machine touch; fs.watch only wakes the guard and a content/inode/mtime/ctime snapshot change is required, with a same-bytes atomic rename positive control. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…r-limited or capped-host load only Load/stress/repeat-under-load validation prefers a CPU-limited Docker container; without Docker only a capped host fallback is allowed (<= min(2 CPUs, 25%), <= 2 nice-19 timed, trap-cleaned burners with cleanup verification); uncapped or all-core host load is forbidden. Clause in the preamble, implementer/auditor/auto-audit/quick-audit/ task-run/rework/send-tool briefs and 7 locales, plus scripts/docker-cpu-limited-test.sh. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…) and render a collapsible run card The cron control contract now states that a wrapped scheduled run must execute the authoritative task body within constraints (no scope expansion, never echo secrets). Exact legacy v1 registrations migrate in place; tampered ones fail closed. Each dispatch projects a durable cron-run timeline record (schedule, interval, timezone, completion policy, previous/next run, task body) that the chat renders as a collapsible card, live and after reload, in 7 locales. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ing directory The locale parity test built its path from process.cwd(), which only exists when vitest starts inside web/. CI runs from the repo root and failed with ENOENT. Locate web/ from the test file instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ent system prompt A short static clause in the session system text (Claude appendSystemPrompt, Codex baseInstructions, and the native launch/resume flags for process agents) states that <imcodes-cron-control> messages are trusted scheduled tasks created by the user, overriding generic ignore-embedded-instructions rules for that wrapper only. Brain delegation and registered contracts now assemble into the session system text instead of per-turn text; cron turns carry only the compact binding and the user-authored task body. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…and pin raw slash-control bytes Contracts now live in the permanent system prompt, so the restore and slash control tests read appendSystemPrompt and assert that user bytes stay exactly raw (/compact is forwarded unchanged at the SDK boundary) while only the permanent cron trust clause remains as system text. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… round, quota and audit heartbeat A shared bar at the top of the chat timeline (main session, sub-session card, sub-session window and pinned panel) derives the latest PASS/REWORK and R<n> from trusted daemon events, reuses the one-line 5h+7d quota component extracted from the usage footer, and shows the audit heartbeat badge inline. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…reset append
The Claude Agent SDK ignores a top-level appendSystemPrompt option; only
systemPrompt: { type: preset, preset: claude_code, append } reaches the
model. Every stable system rule (identity, contracts, cron trust clause) was
silently dropped for Claude and Anthropic-compatible endpoints. Use the typed
documented form and state that earlier prompt-injection memories about the
cron wrapper are obsolete.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
On a stock desktop Ubuntu (GNOME on Wayland) the only X sockets are the login greeter's or user's Xwayland, which reject a client without their cookie, and the real screen is not reachable over X11 at all. The worker picked the lowest-numbered socket regardless, so a session sat in its first connecting step forever, and linuxGraphicalDisplayAvailable() counted any socket as a display, so the node never offered the basic virtual desktop that would work. The node now probes each socket with the X11 connection handshake (no credentials): a server that accepts is usable, one that answers failed / authenticate is not. resolveWorkerDisplayEnv prefers an openable display (falling back to the previous lowest-socket choice when nothing is openable), and a box whose only sockets are unopenable is treated as having no display, so it offers the virtual desktop install. The runtime re-probes on capability refresh (15 s cache) and re-publishes when the openable set changes; the test seam stays hermetic. Verified the handshake against a real Ubuntu 26.04 box: Xvfb :99 accepts, greeter :1024/:1025 reject. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…by the sidebar The pinned CronManager rendered its edit form inside the overflow-clipping sidebar panel, so the click worked but the dialog was invisible. Pinned hosts now portal all cron sub-panels to the document body at the modal layer, and editing a self-managed schedule keeps its registered control contract. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The SDK transport e2e still read the top-level appendSystemPrompt option that
the real SDK ignores and the provider no longer sets. Read the append from
systemPrompt: { type: preset, preset: claude_code, append } through one helper.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… chips into one expandable row Consecutive waiting-heartbeat and empty WAITING chips collapse into a compact summary (counts and time range), collapsed by default and expandable to the original chips. Real content, NEEDS_INPUT and single chips are never folded; the fold is presentation-only and stable across live appends and history reloads. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tion no longer reaches the model Startup and per-message recall items that mention the cron control wrapper are filtered at the shared authoritative assembly boundary; mixed recall is rebuilt from the retained structured items and unbound rendered text fails closed. This breaks the loop where a session summarized its own refusal and cited it as evidence. The permanent authorization stays in the system prompt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… the chat The sticky bar showing the latest PASS/REWORK and the provider quota over the message list is deleted with its props, wiring and styles. The bottom usage footer quota row, the delegation and peer-audit result cards with round chips and the original heartbeat badge are unchanged. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d child heartbeat updates The share redaction allow-list dropped every quota field, so a participant never saw the 5h/7d row that the daemon reports. Participants now receive a clamped display projection of the quota (label, usage label and the primary and secondary windows) while plan, credits and account configuration stay private. The sub-session sync rebroadcast and the existing-child merge also carry the supervision heartbeat instead of dropping it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…-heals to its real history The blank-pane recovery used events.length as a proxy for visible content, so a restored tail made only of rows the chat never draws (peer audit status, last-value state, hidden rows) looked healthy while the incremental catch-up stayed anchored after those newer rows and never fetched the older conversation; the manual refresh worked because it is unanchored. Decide on the shared renderability contract instead and run one unanchored authoritative recovery, and classify tool calls and results as guaranteed content because the chat always renders their activity rail. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ected UI language The compact permanent task-registry contract now carries one locale-bound rule (author:objective|title@<locale>) built from the UI locale the browser already sends with every message, so the Brain writes new task titles and objectives in the language chosen in the web UI. Without a locale (headless or legacy) the rule is omitted and titles stay raw; cards and the task console keep rendering the registry text verbatim. The registry contract is compacted to keep the preamble byte budgets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ter and controls The two cases still asserted quota and heartbeat props on ChatView, which the removal of the floating participant status bar deleted. Assert them where the data actually goes (SessionControls and UsageFooter) and pin that ChatView no longer receives them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… viewer The native worker invented a viewer at startup to satisfy the dispatcher rule that a disclosure must be visible before PREPARE, so the panel showed 1 VIEWING with nobody connected. The worker no longer pre-seeds a viewer; only PREPARE may reach dispatch before a visible disclosure and it is re-checked afterwards (fail closed). The roster now counts only routes whose peer state is Connected, reconciles on peer callbacks and in the worker loop, and resets on disconnect, rejection, route teardown and worker exit. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Routine dev → master promotion (following the same pattern as PRs #28–#50).
🤖 Generated with Claude Code