Skip to content

dev → master - #52

Open
im4codes wants to merge 914 commits into
masterfrom
dev
Open

im4codes wants to merge 914 commits into
masterfrom
dev

Conversation

@im4codes

Copy link
Copy Markdown
Owner

Summary

Routine dev → master promotion (following the same pattern as PRs #28#50).

🤖 Generated with Claude Code

IM.codes and others added 30 commits September 16, 2026 10:48
…-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>
IM.codes and others added 30 commits September 20, 2026 22:59
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant