Skip to content

Fix what the Mac tester found: a deleted home is never rewritten, macOS test reds, provenance on private apply - #2086

Merged
AbirAbbas merged 18 commits into
devfrom
fix/hello-findings-1
Oct 31, 2026
Merged

AbirAbbas merged 18 commits into
devfrom
fix/hello-findings-1

Conversation

@AbirAbbas

@AbirAbbas AbirAbbas commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes what the two-network relay test (lane L3, re-check on 72a2792e6) found on the client side. One PR, split by concern: D1, P, the continue fixes (D6, D3, D5, D7), a fix round, and the change entry.

What each defect was, and what changed

D6: the chat a person picked was not the chat that opened. A bare codeaf chat continues the newest chat another computer let go of. While that automatic move ran, every explicit Enter or confirm on Home answered a chat is already continuing here; wait for it to open, and the older chat opened. Now a confirmed different chat waits with the holder the person saw (continuing <Y> next; <X> is still on its way here). The automatic move finishes, is let go without opening (through the path a closing window uses, so a shared session is never closed), and then the picked chat is taken and opened. Confirming the arriving chat joins its move. Only a confirm abandons the automatic move; opening a card and answering "leave it there" does not. An abandoned move is bounded (2 min, then cancelled the way Escape used to). With no input, a bare start behaves as before.

D3: a false "another device" refusal, and no retry card. ErrLeaseHeld went to a footer-only path that skipped #2078's card. The likely race: after a move, the receiving computer's writer re-acquires, which raises the ownership number, and a slow relay's stale listing gives the other side a card with the old number. Now every refusal keeps the 1 retry / 2 leave it there card and the draft. A fresh reading separates the same holder reopening (the card asks again, with what it now shows) from a real different device (the real refusal, on the card). Consent still binds holder and ownership number. Reproduced with the two-home rig in TestAReturnAfterTheWriterReopensIsNotALostRace.

D5: Home gave up on a slow relay, and the first message after a move stayed unsent. Home's listing had 5 s. The send check ran its own 5 s directory read while the arriving writer was still starting, and it printed Home's timeout sentence into the chat. Now Home's listing waits up to 25 s, shows waiting for <host>; still checking at 5 s, and keeps its rows. A real outage (refused, DNS, TLS, 5xx) still shows at once and retries on the old schedule. A send right after this window's own move waits up to 30 s for its writer, off the UI loop, then sends once. Displacement or another window's hold still refuses. A send that cannot leave says <host> has not answered; your message is still in the box, press enter to send it.

D1: a directly exposed codeaf relay trusted any X-Forwarded-For. Registration and dial limits counted a caller-supplied header. Now relay.Server counts the socket peer unless it is given a policy, and --trust-proxy (default off) applies one policy to registrations, dials, the pairing mailbox and link pairing. A relay behind a proxy must now pass --trust-proxy, or all its callers share one allowance. Pairing already behaved that way without the flag. Flag help, docs/relay.md and the relay manual page say so.

P (from the Cloudflare audit): a chat this computer never held came over serial object batches. SyncEngine.Holds read the store-wide publication ledger, and new chats share one store, so every new chat looked warm and skipped its frame plan. Holds now asks about this chat (its adoption record or a recorded turn), and a return into a fresh staging folder asks about the chat's existing folder. In the fixture, a cold 2 MiB chat in a used store went from 7 object POSTs to 1 frame GET, and a warm one-file return still moves 5.9 KB. Correctness rests on the unchanged completeness loop. Known perf-only edge cases (follow-up): after a failed take whose staging was removed, the next take re-downloads frames, and chat metadata restored into an empty store reads warm (today's behaviour).

D7: slow empty-folder moves. The cause is not established. Each continue now writes one chat.log line with per-phase durations. First data, from the hand walk on a local relay with the laptop under build load: time spread across directory (0.3–7.3 s), restore (0.3–11 s) and staging (1–7 s). No fixed timer showed up; it looks disk- and relay-bound under load.

Proof

  • Hand walk, D6, local relay, real binary in tmux (B bare start auto-continues X while the person picks Y on Home and confirms):
    • BEFORE (72a2792e6): Y opened 1 of 3. X opened 2 of 3 with the busy line, and X was left running on B.
    • AFTER (round 1 head): Y opened 5 of 5 with no busy line. The waiting line showed, and X was let go each time (released, not running).
    • AFTER (final head): FINAL_WALK
  • New tests (each maps to a contract item in the lane's spec): FINAL_TESTS
  • make test-laws: LAWS
  • rd (Cloudflare dev) re-measure for D3/D5: RD

Not in this PR (follow-ups)

  • Worker slowness (D2/D9): every store request on the hosted relay runs two Raft authorize passes, each a device_seen commit plus a barrier, about 2.7 s per request on the dev relay. Ranked causes and the Worker follow-ups are for the relay owners: measure one request in wrangler dev --local, drop the post-I/O device_seen commit, coalesce device_seen, and name the unavailable() branch in 503 bodies. No Worker change in this PR.
  • The automatic continue declines whenever B holds an older copy: continuation needs a choice for changed path: .git/COMMIT_EDITMSG (seen in L3's runs and in this lane's first walk). .git internals probably should not count as local changes.
  • The duplicate directory reads in a take (≈2 requests per move) and the P edge cases above.

Stability

Nothing changes for a person who never races a continue, never hits a slow relay, and does not run a self-hosted relay behind a proxy. No change to internal/clusterstate, wire or stored formats, relay/hosted/, or workflows. HELLO

🤖 Generated with Claude Code

AbirAbbas and others added 18 commits October 30, 2026 22:39
The release-surface step ran go test "${test_args[@]}" under set -u. With no
known-red ledger the array is empty, and /bin/bash 3.2 on macOS treats that
as an unbound variable, so the prologue test failed on a Mac. The conditional
expansion behaves the same on bash 5 and is safe on 3.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two fixtures built Sync{} with an empty Home. A refusal retires the send
hold, which joined the empty Home and wrote v3/sync/holds/<hash>.lock into the
package's source directory on every run. They now use t.TempDir().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ranches

These tests need a furrow engine. GitHub's full tests stage none, so they skip
there, and they failed on every machine that has one (a Mac after make build,
or CODEAF_FURROW on Linux):

- the scripted drive chat wrote files but never a reply, and a chat with no
  conversation is deliberately not published, so its turns never became
  durable; each scripted turn now writes a reply to the transcript
- the large-home startup test drained a warm capture that has no turn, so
  nothing was sealed; it now completes one recorded call first
- a refused drive branches asynchronously, so the counts and two-home tests
  wait for the branch row instead of reading it once
- the superseded-drive test keeps A offline until its next publish, as the
  scenario states, and waits for the kept-work notice too

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A codeaf engine that was still saving after it was asked to stop could find
its home deleted and treat that as a first run: identity.EnsureSolo made the
folder again and wrote a new identity.json, device.json and identity-solo,
and the vault capture then wrote the sync ledgers beside them. The Mac tester
saw exactly that 8-22 s after removing a test home.

Each codeaf process now records the home it starts in (home.Witness). The
identity writers, the vault open, the stats recorder and the send-hold writer
refuse with "codeaf home was removed or replaced" once that folder is gone
or is a different folder, and they never re-create a recorded root. A first
run, where no home existed at start, creates it exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An engine asked to stop closes every conversation before it exits, and for a
synced chat that can take tens of seconds while the relay is unreachable. If
the home was removed meanwhile, everything it still wrote went into a folder
that no longer existed. While shutting down, the host now watches its home;
if it is removed or replaced, the engine exits within about a tenth of a
second instead of saving into it. With the home intact the stop does what it
did before, in the same order and with the same limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2078 named the process holding a busy engine store. On macOS it asked lsof
about every lock file it found, 500 ms each with no cap, so a failing seal
could add seconds. One shared budget now covers the whole lookup, and it
stops at the first busy holder; when the budget runs out the message is the
plain busy sentence. Requests that are not busy never inspect anything.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
runChatV3Once returned before the interactive road registered its process
cleanup, so a paired --once never closed its sync drive. Publishing uploads
a chat's frames before it registers the chat with the relay, and a --once
that exited in between left the uploads with no record pointing at them: the
other computer never listed the chat, and doctor said nothing (Hello saw it
in 1 of 48 runs, about 1 s after pairing).

--once now always registers drive cleanup; with no drive it returns at once,
and a paired chat gets its final send and release inside the same limit the
interactive road uses, with one plain stderr sentence if that fails. A paired
drive records a small local publication marker, so codeaf doctor can report a
chat this computer started that the relay does not list, and doctor --fix
registers it through the ordinary road, create-only, never over another
computer's record and never for a chat put away meanwhile. A never-paired
computer writes no marker and makes no relay request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at did not arrive, a deleted home is refused

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
macOS names the Option key opt, so Home's continue offer shows opt+c and the
Memory switch shows opt+u. Two tests hard-coded alt+. The Memory test now
builds its expected line through the app's own chord spelling, and the
takeback test waits for the offer's question and checks a continue chord is
drawn beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The quiet case removed a paired machine's folder and created a solo identity
at the same path in the same process. codeaf now refuses to create an identity
in a home it saw and that was then deleted, so the case uses a fresh folder,
which is what a never-paired home is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
42 internal/session subtests failed on macOS and passed on Linux:

- On macOS a new file takes its folder's group, so the Apply fixtures' files
  often carried a group other than the process's and the portable-metadata
  check refused them during setup, before the test's own behaviour ran. The
  success fixtures now give their files the process's uid and gid, resolve
  temp-path aliases first, and clear extended attributes. The product refusal
  stays: private Apply stages replacements with the process's group, so a
  foreign group cannot be carried. A new setgid-directory test pins that
  refusal on Linux.
- Bare mktemp on macOS ignores TMPDIR, so five scratch probes now use
  mktemp "$TMPDIR/codeaf-runtime.XXXXXX".
- Two path checks compared a canonical path with a raw temp path; both sides
  are canonicalized now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… chat that did not arrive, a deleted home is refused"

This reverts commit f28b0f4.
…ts home is removed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
macOS 15 stamps com.apple.provenance on every file a process tree started
from some apps creates, and the owner cannot remove it. Private apply refused
any extended attribute, so on such a Mac it refused every changed file. That
one attribute is now tolerated on macOS: the replacements apply writes come
from the same process tree and are stamped the same way. Every other
attribute, quarantine included, is still refused, and Linux is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AbirAbbas AbirAbbas changed the title Fix what the Mac tester found: a deleted home is never rewritten, --once chats always arrive, macOS test reds Fix what the Mac tester found: a deleted home is never rewritten, macOS test reds, provenance on private apply Oct 31, 2026
@AbirAbbas
AbirAbbas marked this pull request as ready for review October 31, 2026 05:23
@AbirAbbas
AbirAbbas merged commit 9638112 into dev Oct 31, 2026
23 checks passed
@AbirAbbas
AbirAbbas deleted the fix/hello-findings-1 branch October 31, 2026 05:23
AbirAbbas added a commit that referenced this pull request Oct 31, 2026
…2092)

* test: remote-task session fixtures pass on macOS temporary folders

Two #2085 fixtures failed on macOS only. The r8b home was made under /tmp,
which macOS links into /private/tmp, and owned conversation storage
refuses a recorded binding whose spelling crosses a link, so newAgent
stopped with "workspace binding traverses a symbolic link"; the fixture
now names its home by its resolved path. The private-task Review and
Apply test wrote its returned file inside a copy under the temporary home,
where macOS hands a new file its folder's group (wheel under /private/tmp),
and Apply refused that group; the fixture now gives that file the
process's own group, as #2086's Apply fixtures do. Both failures reproduce
on Linux with a home reached through a link and a setgid TMPDIR owned by a
secondary group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs: change entry for the macOS remote-task fixture fix

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.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