Repository navigation
Conversation
im4codes
pushed a commit
that referenced
this pull request
Sep 16, 2026
…-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>
im4codes
pushed a commit
that referenced
this pull request
Sep 16, 2026
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>
im4codes
pushed a commit
that referenced
this pull request
Sep 16, 2026
…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>
im4codes
pushed a commit
that referenced
this pull request
Sep 16, 2026
…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>
…s or inert project; a periodic pass names itself when it stalls the daemon 215: 26 legacy tasks of a pairs project sat in ready_for_integration (3 in rework); every 60 s tick retried them and each retry applied an integration bundle = hundreds of synchronous git forks of the 1 GB daemon (perf: 21% of main-thread samples in spawnSync/fork, 830-1100 forks/min), a 2.4 s main-thread freeze that held every keystroke echo. dispatchReadyAudit had the pairs/inert guard; dispatchReadyRework/dispatchReadyIntegration and the tick loops did not. runPeriodicPass(name, fn) blames a stall that ends while a pass is in flight on pass:<name> and logs one line naming a pass that exceeded the 300 ms budget (the 215 freeze stayed anonymous for days). Applied to the supervision watchdog, the convergence-dispatch tick, the pair heartbeat, the both-idle check and the integration-drift pass. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ot by forking the daemon; a guard keeps it that way The daemon pays a fork of its whole address space (~27 ms on a 1 GB daemon, perf-measured on 215) for every child_process spawn on the main thread. The task-pair git probes (workspace / hygiene / material / git-init / non-git), the codex watcher's sqlite3 and version probes and the repo git status/numstat now go through execFileOffMain / the new callback-form execFileOffMainCallback. The legacy integration-bundle entries run in a named watchdog phase (they need stdin and binary stdout the helper cannot carry; the periodic passes can no longer reach them). test/daemon/no-main-thread-child-process.test.ts forbids spawnSync/execFileSync/execSync and direct execFile/exec imports under src/daemon, src/agent, src/context and src/store; every exception is listed with its reason, and a stale entry fails too. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…the exec helper Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ed window is no longer silent - an installable app with its own narrow scope (/remote-desktop/app/): web manifest emitted from shared/remote-desktop-app.ts, icons rendered from the official logo by scripts/aidesk-icon.mjs (pwa-icons) and committed under web/public, no service worker; the manifest link is added at run time only where the remote desktop UI is, so the main pages advertise nothing - 'Install as app' entry (control panel header, wall header): one click with Chromium's install prompt, otherwise a short instruction for the browser (Edge, Chrome, Safari Mac/iOS, Firefox: why its windows keep an address bar); hidden inside the installed app - every opener of a remote desktop window now reports a block (null from window.open) to one notice host, mounted in the app and both standalone windows; inside the installed app another window is an app window, not a pop-up - server: manifest MIME, and a missing app asset is a 404 instead of the SPA's index.html Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… - derived from the identity store at launch, removed from old rows, old rotated copies deleted The rendered identity prompt (250-550 KB per session) is derived data (user / project / session profiles), not session state. Storing it grew the session database 8.4 -> 84 MB, went stale on every edit, and spread the text over rows, snapshots and exports. - SessionRecord has no identityPrompt; it keeps appliedIdentityHash (digest of what the session runs with) and the existing provisionedIdentityHash. externalize/hydrate drop any prompt field, so no code path can store one. - One derivation, src/daemon/session-identity-resolver.ts: resolveEffectiveIdentities (batch, one rendering per distinct profile set, THROWS on an unreadable store so the sync never applies "no identity") and resolveEffectiveIdentityPrompt (for launches; never throws: a launch proceeds without an identity, logs names only, counts identity.resolve_failed). Used by the periodic sync, the transport launch/restart/restore, the process-agent relaunch and init injection, and sub-sessions: same path as main sessions. A caller-supplied identity (a session being created) is used as given. Nothing else about launch/refresh behaviour changed. - Migration (scrubStoredIdentityRows): on every start, before rows are read, rows still holding a prompt (inline or by blob reference) are rewritten in ONE transaction (digests stamped; provisionedIdentityHash recovered from the session section when absent), the blobs only they used are collected, then the file is VACUUMed and the log truncated so no byte of the text stays on disk. A failed compaction leaves a 'vacuum_pending' marker and the next start retries; a rolled-back transaction changes nothing. - sessions.json compatibility export: no identityPrompts table (older readers take the legacy path and lose only the prompt). - Old copies the daemon's own rotation left (sessions.sqlite.bak.1-3, .bak.tmp, sessions.json.1-5, export temp files; names from shared/session-store-compat.ts) are deleted AFTER the migration committed and only when their bytes still hold a prompt; clean ones stay, and a failed delete is reported and retried next start. Anything else that looks like a session file (.backup-*, .pre-final-clean, .migrated-to-sqlite, .empty-at-*) is never touched, only listed with its size. - The generic large-field blob table stays as a bound for any other field; identity is excluded from it. Verified against the real previous build (113a526): new build on a database it wrote -> 1.6 MB db + wal + snapshot with the text -> 24 KB, text in no file; the previous build then reads the migrated database (6 sessions, no crash); the new build starts on it again. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ation (it read the pane twice and failed on the final newline), in-band barriers instead of fixed sleeps; ordering test of the tmux exec helper waits for the last echoed line Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e it is; a server that cannot be reached no longer makes the watchdog kill the node; the worker set is gated at build time
* The POSIX upgrade script looked for the staged Linux worker at the sidecar root, one directory above the linux-x64/ it is
in, so every Linux self-upgrade by a build with the transaction script ended in preflight_failed ("staged remote desktop
worker artifact set is incomplete"). The directory names are shared constants now (installed path, downloader, server
route, script); the preflight checks the worker and its manifest; a test runs the real downloader, the real script
builder and the generated script end to end.
* The systemd watchdog, the macOS health watchdog and the Windows watchdog were fed only by an authenticated server ack, so
an unreachable server (blocked route) restarted the node with SIGABRT every WatchdogSec. A separate liveness lease and
the systemd pulse are now renewed while the connection machinery shows activity (ack, attempt, failure, retry); the
authenticated health lease stays the self-upgrade's proof of installation, and the Linux lease-less survival shortcut is
off for a target that publishes the liveness lease.
* scripts/remote-desktop-worker-artifacts.mjs verifies the Linux worker set (both files, executable bit, strict manifest,
size, sha256, version) on the build runner, the downloaded server artifact and the final image.
* A log warning after 5 minutes without a server acknowledgement.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… module (the chat perf guard imports it); a launch derives the identity with the session's context namespace and the env-injection tests wait for the identity call instead of 1.6 s Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…(2 hours ago was yesterday between midnight and 2 a.m., and failed CI) Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… addresses, says why it cannot connect, and reports a failed preflight * The server advertises its public https origins (IMCODES_PUBLIC_URLS, empty by default) in the authenticated heartbeat ack of a controlled node. The node keeps them, the ones the machine's root pinned (`imcodes-node set-server-url`, root only) and its enrolled address in a non-secret server-endpoints.json (atomic, 0644, validated as untrusted input; a damaged file means no alternates). After three connection failures in a row it moves to the next address with a per-address doubling cooldown, starts on the address that last authenticated, stays on a working one, drops for a day an alternate that rejects its credential or answers as another server ID, and never sends its credential to an address that did not come from the server's authenticated channel or the machine's root. One origin-validation function is shared by enrollment, the node, the CLI and the server. * The reason (tcp_timeout, refused, dns, tls, reset, rejected) and host[:port] go to the log after 5 minutes without an acknowledgement and to the node's local panel (new strings in all seven languages). * A node that was sent an upgrade it cannot install (preflight_failed) now reports it as a failed install of that exact target, so a server restart does not re-offer it at once. * Watchdog backstop: sockets that keep opening for 45 minutes with no authenticated ack stop the liveness renewal once (the platform watchdog restarts the node); each restart without an ack in between doubles the period up to 12 hours. * The repeated "failed to publish health signal" stderr line is limited to once an hour. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…entity store (SESSION scope) so restart/restore/sync keep it; the e2e asserts the new model
tsk_ee4f47f8e4 stopped storing the identity prompt in the session record. The e2e "carries a selected-file identity from
session.start into the first SDK system prompt" still asserted the OLD model (record.identityPrompt), which is why it went red;
the first SDK prompt itself did carry the identity (verified: the same e2e passes with that stale assertion removed).
The real gap behind it: an identity that arrives with session.start was only handed to the launch. Everything that derives the
identity afterwards (restart, restore, session.identity.refresh, the 60 s sync) reads the identity store, so a client that does not
save it there afterwards (any non-web client, or a web save that failed) lost it at the first restart or sync.
- handleStart saves a validated explicit identity into the SESSION scope (key `<serverId>:<session>`, the key the resolver reads)
BEFORE launching (persistExplicitSessionIdentity, in the resolver module; never throws; a failure is logged by name and counted
as identity.persist_failed, and the run still starts with the identity it was created with). The record keeps only the digest.
Process and transport agents share this one place.
- e2e (test/e2e/sdk-transport-flow.test.ts): the stale assertion is replaced by the new-model checks, plus the whole path through
session.start -> real command handler/session manager/Claude SDK provider -> the system prompt: first prompt, session.restart
(identity survives; an edit made while stopped applies), session.identity.refresh, a session with only a user-scope identity, a
session with none, a sub-session start + subsession.restart, and a store that cannot be written. The e2e execFile mock now
resolves {stdout,stderr} when promisified like the real one (without it a second launch in one process failed in the
process-start probe: stdout.split on undefined).
- command-handler-transport-queue test: process (codex) and transport starts persist the identity, none persists nothing,
an invalid identity persists nothing.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ps the connection; docs say a node sends its credential to every listed address Audit follow-up of the address fallback: the serverId check only chose the next address while the connection stayed up and was treated as authenticated (lease, "service healthy", adoption). The tracker now answers whether the ack is accepted; the runtime stops processing it and reconnects, and the address is dropped for a day. Also: the IMCODES_PUBLIC_URLS notes now state that a node sends its credential to each listed origin; a doc comment is back next to the constant it describes. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… menu-bar process when no remote session is active The node compares the installed bundle version with each running menu-bar process (not the launch agent or workers), and when one is older and no remote desktop connection is active it ends that process and starts the app again as the console user. Attempts are recorded before acting, spaced and capped per version; unknown versions and active sessions are left alone. The running-app check no longer mistakes the helpers for the menu-bar app. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…r are really in use remote_desktop.data.encoder (worker -> viewer): codec (vp9|vp8|h264|pending), implementation (software|hardware), the encoder's own name, threads, and the node's raw-codec decision (allowed|hardware_h264|capture_cannot_scale|disabled_by_setting). It is a message TYPE of its own, not extra keys on the quality message: every web validates the quality message with an exact key set and drops the whole message on an unknown key, which would blank the status bar of every older web; an older web drops an unknown data-message type silently. So the quality message is unchanged (pinned by a test that also shows the extra-key failure mode), a newer web simply never receives the new message from an older node, and no capability bit is needed. Exact-key validator with bounded name and threads; native constants pinned to the shared ones by the cross-layer test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…viewer and to a log the node can read - EncoderDescription: built from what the worker already holds (negotiated codec, the facts libvpx reports when it initialises -- its own name and the capped core count -- the VideoToolbox class for H.264, and the raw-codec decision). Nothing is claimed that both the policy and the negotiation do not say; unknown is software, never hardware. - The worker sends it as the encoder data message when the control channel opens and once the encoder is up (the first rate update), again only when it changes, never before a codec exists, and marks it sent only when the channel took it. - The worker's stderr is /dev/null, so the decision and each change are also appended to worker-video.log in the node's per-user runtime directory (0700, the session's user; the node reads it as root): UTC timestamp and key=value pairs, no secrets, nothing from the screen. Capped at 64 KiB with one rotated file, O_NOFOLLOW, mode 0600, control bytes and newlines replaced, lines cut at 512 bytes. - The quality message is untouched. Tested under ASan/UBSan: every codec/implementation/reason combination, the log's sanitising, rotation, symlink refusal, mode and concurrent appends, with mutation checks on each (one survivor: the log's mutex, which only matters for double rotation and is not observable through O_APPEND-atomic writes). Source pins for the wiring. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…, libvpx)" The client keeps the worker's encoder message in the snapshot and the footer shows "Encoder: VP9 (software, libvpx)", "Encoder: H.264 (hardware)" or "Encoder: H.264 (software)", with a note when H.264 is there because the node setting forced it or because its capture cannot scale. A node that does not send the message (older macOS, Windows, Linux) keeps the old class-only text. Malformed messages and messages for another session are ignored, as is a data-message type the client does not know (which is what an older web does with this one). Strings in all seven locales; the RTT tooltip is already there. Tested: the label for every codec, implementation and reason in all seven locales with all placeholders filled and identical to English; the client path (message shown next to an unchanged quality report, replaced by a later one, malformed/other-session ignored, works against a node that never sends it, drops an unknown type without touching the session), with mutation checks on the branch, the label rules and the validator. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ough its single environment lookup A test pins std::getenv to exactly one use in the worker (the lookup helper); the video log's runtime-directory read went around it. Same behaviour, through the helper. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…again; teardown forgets it The encoder message was deduplicated against the last one sent, but topology and quality go out on every control-channel open: a viewer that opened its channel later (a new peer after renegotiation or an ICE restart, another viewer on the same route) found an identical description and was told nothing, keeping the class-only fallback or a stale value. The rule now lives in EncoderAnnouncement (nothing before a codec exists, nothing a viewer already has, a message the channel did not take stays owed) and a (re)opened control channel resets it before the send. Tested directly, with mutants on the reset, the pending rule, the dedupe and the sent-marking. The web client also clears the encoder in the teardown patch next to the quality it belongs to, so a reused client never shows a stale one (tested, with a mutant). The README gives the exact log paths and the rotation rule for a future node-side reader. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s its server token POST/GET/PUT/PATCH/DELETE /api/server/<serverId>/cron with no Authorization and no Cookie used to run as the server owner, daemon-attested (commit e2a9d5b removed the daemon's credential). The serverId is not secret (share recipients and group members receive it, it is in URLs and logs), so anyone could create, read, change, trigger and delete the owner's cron jobs, i.e. inject prompts or commands into the owner's agent sessions. requireCronAuth is now requireAuth: cookie, user bearer/API key, or the daemon server token + X-Server-Id (revoked tokens and controlled nodes refused as everywhere else). A daemon from before this change gets 401 {error: daemon_credential_required_upgrade_the_daemon}. cron-mcp-client sends the credential through one shared builder (shared/daemon-server-auth.ts) that the other daemon->server callers now use too; X-Server-Id and Authorization are shared constants. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… was rejected; a 403 scope denial keeps its own message Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nodes shared with them A participant's turn runs on the owner's agent and was admitted on the owner's node access alone. Device admission now also requires the participant's own Owner/Participant access to the exact target (machine share or group), read live; list_machines for such a turn is narrowed the same way; staged file transfers re-read both. Owner turns are unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…currency bound, the fs-delegate sweep never deletes a live request, native compile specs get the long timeout, queue admission tests wait for the service to be idle Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rd is bounded and retried Run 37664607739 sat in 'playwright install --with-deps chromium' (apt) for hours and held the push concurrency group, so the next dev run never started a job. That step normally takes ~25 s and stalled to 9-11 min twice before. Every job in every workflow now has a deliberate timeout-minutes; apt's network waits are bounded (ci-apt-hardening.sh), the install is retried with a per-attempt limit (ci-retry.sh); a workflow lint test fails if a job has no limit. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…its oldest event leaves the list A run of consecutive assistant messages is ONE view item, keyed by the first event of the run that is in the derived list. That event leaves the list whenever the retained-event cap drops the oldest event or the tail derivation window slides, so the key - and with it the DOM row, including the streaming one - changed about once per message in a long run and Preact re-created it. The key a run was rendered under is now remembered per member event (chat-run-keys.ts): a run that still holds an event it had keeps its key; a split keeps it for the part with the original first event. The row's data-event-id stays a real, current event id. The probe tracks every message row's DOM node on the real app path. Co-Authored-By: Claude Sonnet 5.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