Skip to content

dev → master - #52

Merged
im4codes merged 2221 commits into
masterfrom
dev
Oct 20, 2026
Merged

im4codes merged 2221 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

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>
T and others added 26 commits October 7, 2026 23:15
…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>
T and others added 29 commits October 15, 2026 16:07
@im4codes
im4codes merged commit 4ba7b24 into master Oct 20, 2026
62 checks passed
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