Skip to content

fix(acp_client): end an agent with its host on macOS and sweep what a dead host left - #953

Merged
LivXue merged 2 commits into
mainfrom
fix/acp_orphans_on_macos
Oct 15, 2026
Merged

LivXue merged 2 commits into
mainfrom
fix/acp_orphans_on_macos

Conversation

@LivXue

@LivXue LivXue commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

Summary

On macOS an ACP agent outlives a Raven host that dies without closing it (killed, crashed, OOM-killed). The agent runs in a session of its own, so it gets none of the host's signals, and only AcpClient.close ends it. #942 closed this on Linux with a parent-death wrapper and a launch-record sweep. On macOS both were no-ops. This PR brings both layers to macOS.

1. The agent ends with its host (raven/acp_client/leftovers.py, _DARWIN_BINDING). macOS has no PR_SET_PDEATHSIG. Instead, the exec wrapper double-forks a small watcher before it execs the agent, then reaps the middle child.

  • The watcher is adopted by launchd, so the agent has no child it did not start.
  • It stays in the agent's process group, closes every descriptor it inherited (so it holds none of the agent's pipes), and changes to /.
  • It waits on the host's exit with kqueue (EVFILT_PROC / NOTE_EXIT), then ends the whole group. An npx launcher's grandchild goes with it, which the Linux signal does not reach.
  • If the host is already gone when the watch is set, registration fails with ESRCH, and that counts as an exit.
  • It ends the group only while the agent leads its own group, which every launch does. Any other failure leaves the agent as it would have been.
  • close() ends the watcher along with the rest of the group.
  • Cost: about 4 MiB RSS per launched agent.

2. The sweep at the next launch runs on macOS. This is the backstop for when the watcher itself is gone. The decision logic is unchanged; each /proc read has a macOS equivalent:

  • process start time: sysctl KERN_PROC_PID (kinfo_proc.p_starttime, in microseconds). It reads any process, including launchd and zombies; proc_pidinfo refuses launchd with EPERM.
  • group members: proc_listpids(PROC_PGRP_ONLY)
  • launch mark: sysctl KERN_PROCARGS2. EINVAL (gone, zombie, or another user's process) counts as no mark.
  • boot identity: kern.bootsessionuuid and kern.boottime. macOS has no PID namespaces, so the namespace is a fixed value.

macOS hides the environment of its own platform binaries (/bin/sh, /bin/sleep). Only a group member that is not one can show the mark, such as the agent's node or python process, or the watcher. What cannot be read still leaves a group alone.

Linux is unchanged. The Linux wrapper string is byte-identical to main's _WRAPPER. _start_ticks is renamed to _start_time, which now dispatches by platform. CONTEXT.md (Leftover agent, Launch mark) is updated to match.

Windows lifetime handling is provided by #951. This PR adds the macOS wrapper and sweep; these helpers remain a no-op on Windows.

Type

  • Fix
  • Feature
  • Docs
  • CI / tooling
  • Refactor
  • Other

Verification

Before rebasing onto the merged Windows lifetime change, on macOS 26.6.2, arm64, Python 3.12 (project venv):

  • uv run pytest tests/test_acp_leftovers.py tests/test_subagent_host_env.py tests/test_acp_client_teardown.py tests/test_acp_client_launch.py -q -p no:cacheprovider -m "slow or not slow" -> 117 passed, 9 skipped (Linux-only mechanisms).
  • Every other tests/test_*acp*.py and tests/test_*subagent*.py file, run one file at a time -> all pass except tests/test_subagent_dag_runner.py. On this Mac that file hangs at the same two points with this branch and with main's leftovers.py:
    • after TestBackgroundRun::test_a_run_that_ignores_cancellation_fails_named_instead_of_hanging_loop_close fails;
    • with that test deselected, after TestBackgroundRun::test_the_failure_path_reaps_a_subagent_child_left_running (21 passed in both runs).
      It is unrelated to this change; Linux CI covers that file.
  • Red/green with a stand-in host (AcpClient.launch, then kill -9 of the host). The stand-in agent ignores SIGTERM, never reads stdin, and starts a child of its own.
    • main: 3 s later the agent is alive with ppid 1, and its child is alive.
    • this branch: 3 s later both are gone.
  • Sweep backstop: kill the watcher, then kill -9 the host. The agent stays (ppid 1). The next host's launch ends the agent and its child.
  • uv run ruff format --check and uv run ruff check on the changed files -> clean. make check-source-language and make check-large-files -> clean.

New macOS tests cover:

  • a killed host ending the agent and what it started;
  • the watcher holding none of the agent's pipes and being the only member left after the agent exits;
  • the watcher ending nothing when the agent does not lead its group;
  • the exec keeping the agent's pid and argv;
  • SIGPIPE and SIGXFSZ reaching the agent at their defaults;
  • the start-time, member, environment and boot readers (including an empty argv entry and a platform binary).

Most existing Linux tests now run on macOS too.

After rebasing onto the merged Windows lifetime change, on Linux / Python 3.12:

  • The only manual conflict resolution is in CONTEXT.md: retain both platform descriptions, scope the record sweep to Linux and macOS, and remove the stale sentence that macOS only has explicit teardown.
  • The macOS implementation and its tests are byte-identical to the approved revision. The Windows implementation and its tests are byte-identical to main; the shared client and environment-test logic also match main, with only the macOS comments/docstring added.
  • uv run --no-sync pytest tests/test_acp_leftovers.py tests/test_acp_windows.py tests/test_acp_client_launch.py tests/test_acp_client_teardown.py tests/test_subagent_host_env.py tests/test_subagent_acp.py tests/test_living_docs.py -q -x -> 468 passed, 10 macOS-only tests skipped.
  • uv run --no-sync ty check raven/acp_client and uv run --no-sync lint-imports -> passed; all 10 import contracts kept.
  • uv run --no-sync pre-commit run --from-ref origin/main --to-ref HEAD -> passed for every applicable hook.
  • make check-large-files check-source-language check-commits with the existing environment and UV_NO_SYNC=1 -> passed.

Native macOS and Windows tests were not repeated after this documentation-only conflict resolution. The current-head Windows re-review before the rebase reported 65 passed and 79 platform-specific skips.

  • Relevant tests pass locally
  • Relevant lint / type checks pass locally
  • User-facing docs or screenshots are updated when needed

Risk

  • User-visible: on macOS an ACP agent is now SIGKILLed with its whole process group when its host exits without closing it. On Linux the parent-death signal ends the direct child; the launch-record sweep handles surviving marked descendants. An agent that relied on outliving its host no longer does.

  • Each launched agent on macOS carries one extra idle process (about 4 MiB) in its group until the group is closed or the host exits.

  • The sweep only acts on groups recorded by a dead host in the same boot, whose leader start time matches and which still hold a member carrying that host's mark. A recycled pid or group number is left alone.

  • Rollback: revert this commit. Record files a macOS host left under acp-launches/ are then inert, because the sweep is a no-op on macOS again.

  • Security impact considered

  • Backward compatibility considered

  • Rollback path is clear for risky changes

Related Issues

#928 (macOS); #951 provides the Windows lifetime handling.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

I reviewed the full diff and surrounding AcpClient launch/close call chain, the launch-record history, backward compatibility of version-1 records, test changes for weakening, and the repository rules in AGENTS.md, CLAUDE.md, CONTEXT-MAP.md, and the ACP terminology in CONTEXT.md. I also checked the macOS process APIs used by the wrapper and sweeper and found no concrete failure.

Verification: uv sync --locked --extra dev --dev succeeded, then uv run pytest tests/test_acp_leftovers.py tests/test_subagent_host_env.py -x completed with 110 passed and 10 macOS-only tests skipped on Linux. The first run in the default environment stopped during fixture setup because the dev-extra raven_everos package was absent; no selected test had run before the environment was synchronized.

@LivXue

LivXue commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility review at f062f4ce11c2c7968739a0f72a5b575282be7587, base 6b938a4267cb1f3bf1453a39ccc7d580e0eea9fd.

No Windows compatibility regression found in the inspected five-file change. Read the complete diff, ACP launch/close callers, renamed helper references, platform guards and tests, plus existing discussion. git diff --check passed. The new macOS native-library calls remain lazy; on win32, _platform() returns None, so wrapping, launch marks, record creation and the first-launch sweep return before the POSIX paths.

Native Windows / Python 3.13.12: 48 passed, 79 skipped. The focused suite passed 47 tests and skipped 79 Linux/macOS-specific cases. An additional external probe passed through a real AcpClient.launch, request round trip and close from a space/non-ASCII path, with fail-fast sentinels on the macOS loader and POSIX lifetime hooks. It confirmed the child was launched directly and stopped on close.

Verification used a detached exact-head checkout outside the active repository, verified source imports, isolated home/temp/token paths and a fresh uv environment. Setup: uv sync --frozen --python 3.13 --all-packages --extra dev. Tests used PYTHONUTF8=1, UV_NO_SYNC=1, and UV_PROJECT_ENVIRONMENT=$R/envs/953.

$R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/0a65ac5061d248b5a51ea46664349e4b'
uv run pytest tests/test_acp_leftovers.py tests/test_subagent_host_env.py tests/test_acp_client_teardown.py tests/test_acp_client_launch.py -n 0 -x --tb=short --basetemp "$R/checks/953-focused/pytest" --junitxml "$R/checks/953-focused/junit.xml"
# 47 passed, 79 skipped
uv run pytest "$R/test_windows_leftovers_guard.py" -s -n 0 -x --tb=short --basetemp "$R/checks/953-native-guard/pytest" --junitxml "$R/checks/953-native-guard/junit.xml"
# 1 passed

Limits: this verifies the Windows side of this PR. Native macOS watcher/sysctl/libproc behavior, native Linux behavior, the full suite, real vendor adapters and integration with separately proposed changes were not exercised.

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

I reviewed the new test-and-coverage commit and re-read the full change in context. The coverage exclusions are limited to native macOS declaration/call shims, while the parsing, dispatch, growth, and error behavior is now exercised with fakes on Linux; the real macOS integration tests remain intact. I also rechecked the callers, history, backward compatibility, repository rules and domain terms, architecture boundaries, and that tests were extended rather than weakened.

Verification: git diff --check github/main...HEAD passed, and uv run pytest tests/test_acp_leftovers.py tests/test_subagent_host_env.py -x completed with 127 passed and 10 macOS-only tests skipped on Linux. The completed CI checks are green; the sharded unit and coverage-dependent jobs were still running when reviewed.

@LivXue

LivXue commented Oct 14, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review at 8ea3a16507858f34db17cd081a703e3f510a4d1e, base 6b938a4267cb1f3bf1453a39ccc7d580e0eea9fd.

No Windows compatibility issue found in the inspected scope. Reconciled the full five-file PR diff with the previous review: three files are unchanged; the two-file revision adds reader tests and coverage annotations, without changing production logic. Rechecked platform guards, launch callers and all discussion pages. git diff --check passed.

Native Windows / Python 3.13.12: 65 passed, 79 skipped. The 17 new platform/reader cases pass using replaced native calls. The real ACP launch/request/close probe also passes at this head from a space/non-ASCII path; the Windows launch does not enter the macOS loader or POSIX lifetime hooks.

Detached exact-head checkout, fresh uv environment, verified source imports and isolated home/temp/token paths. Setup: uv sync --frozen --python 3.13 --all-packages --extra dev. Test environment: PYTHONUTF8=1, UV_NO_SYNC=1, UV_PROJECT_ENVIRONMENT=$R/envs/953.

$R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/e46b3308eaef431f8bb15118118840f1'
uv run pytest tests/test_acp_leftovers.py tests/test_subagent_host_env.py tests/test_acp_client_teardown.py tests/test_acp_client_launch.py -n 0 -x --tb=short --basetemp "$R/checks/953-focused/pytest" --junitxml "$R/checks/953-focused/junit.xml"
# 64 passed, 79 skipped
uv run pytest "$R/test_windows_leftovers_guard.py" -s -n 0 -x --tb=short --basetemp "$R/checks/953-native-guard/pytest" --junitxml "$R/checks/953-native-guard/junit.xml"
# 1 passed

Limits: Linux/macOS-specific cases skip on this host. Passing the reader tests with fakes does not verify native macOS APIs or watcher behavior. No full-suite run, real vendor-adapter run, or integration with separate proposed changes.

@LivXue
LivXue requested review from 0xKT and gloryfromca October 15, 2026 02:32
Handsome-wzw
Handsome-wzw previously approved these changes Oct 15, 2026
LivXue and others added 2 commits October 15, 2026 02:44
… dead host left

An ACP agent runs in a session of its own and, while its host lives, only
AcpClient.close ends it. A host killed, crashed or OOM-killed before its
teardown left every agent it launched running. Linux was closed by the
parent-death wrapper and the launch-record sweep; on macOS both were no-ops.

- The exec wrapper now has a macOS binding. macOS has no PR_SET_PDEATHSIG,
  so before the exec the wrapper double-forks a watcher that launchd adopts.
  It stays in the agent's process group, closes every inherited descriptor,
  leaves the agent's directory, waits on the host's exit with kqueue
  (EVFILT_PROC / NOTE_EXIT) and then ends the whole group, so a launcher's
  grandchild goes too. It ends the group only while the agent leads it, and
  any failure leaves the agent as it was. close() ends the watcher with the
  rest of the group.
- The launch records and the sweep now run on macOS, with each /proc read
  replaced: process start time from sysctl KERN_PROC_PID, group members from
  proc_listpids(PROC_PGRP_ONLY), the launch mark from KERN_PROCARGS2, the
  boot from kern.bootsessionuuid and kern.boottime. macOS has no PID
  namespaces and hides the environment of its platform binaries, so only a
  member that is not one can show the mark. What cannot be read still leaves
  a group alone.
- The Linux wrapper string is unchanged.

Windows is not covered and stays tracked in #928.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>
The coverage gate runs on Linux, where none of the macOS readers execute,
so the diff measured 35.83% against a 90% threshold.

- The three functions that only declare or call native macOS APIs
  (_darwin_libc, _sysctl, _sysctl_by_name) are marked no cover, as the
  repo does for its native Windows declarations.
- The rest is tested on every platform against faked native answers: the
  start-time, environment and boot parsers, the member list growing until
  the group fits, the error paths, and the platform dispatch. The tests
  that read the real system on macOS stay as they were.

Co-authored-by: Claude (claude-opus-5-5) <noreply@anthropic.com>

@gloryfromca gloryfromca left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blockers; this can merge as far as I am concerned.

This revision rebases the change onto the merged Windows ACP teardown. I checked the range diff and the full diff against the updated main: the macOS commits retain their behavior, the POSIX host-binding wrapper and Windows job-object launcher remain mutually exclusive at the launch call site, and the conflict resolutions update only the shared documentation and wrapper wording. I also rechecked callers, history, compatibility, test strength, repository rules and domain terms, and architecture boundaries.

Verification: git diff --check github/main...HEAD passed. uv run pytest tests/test_acp_leftovers.py tests/test_acp_windows.py tests/integration/test_acp_windows_real_process.py tests/test_subagent_host_env.py tests/test_subagent_acp.py -x completed with 460 passed and 10 macOS-only tests skipped on Linux. Completed CI checks are green; sharded unit jobs were still running when reviewed. No review threads are open.

@LivXue

LivXue commented Oct 15, 2026

Copy link
Copy Markdown
Member Author

Windows compatibility re-review at c7e35e386f299d7d1d5cc855f4f87bb2653ec11f, against base 496be8807dfeeb4e4e599e719252ce2eca7763e2.

No Windows compatibility issue found in the inspected scope. Read the complete five-file diff, shared ACP launch/close callers, platform guards, tests and existing discussions. The macOS implementation and its test file are byte-identical to the previously reviewed head; the Windows Job Object implementation and tests are byte-identical to the new base. On Windows, the POSIX wrapping/mark/record/sweep paths still return before native POSIX calls, and the shared launch proceeds through windows.launch. git diff --check passed.

Native Windows / Python 3.13.12: 97 passed, 79 skipped. This includes 11 real-process tests covering host kill/abrupt exit/close, an agent exiting before its descendant, a nested job, host death around job assignment, independent connections, a .cmd launcher and stdout closure. An external real ACP probe also passed from a space/non-ASCII path, preserving argv and a Unicode/CRLF request value. It verified the Windows job-owned launcher and agent both ended on close, with fail-fast sentinels confirming that _darwin_libc, _host and _start_sweep were never entered.

Verification used a detached exact-head checkout, verified source imports, isolated home/temp/token paths and a fresh environment from uv sync --frozen --python 3.13 --all-packages --extra dev. Test environment: PYTHONUTF8=1, UV_NO_SYNC=1, UV_PROJECT_ENVIRONMENT="$R/envs/953".

$R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/0351d0adb12c4b0696a97af5e5511105'
# 85 passed, 79 skipped
uv run pytest tests/test_acp_leftovers.py tests/test_subagent_host_env.py tests/test_acp_client_teardown.py tests/test_acp_client_launch.py tests/test_acp_windows.py -n 0 -x --tb=short --basetemp "$R/checks/953-focused/pytest" --junitxml "$R/checks/953-focused/junit.xml"
# 11 passed, native Windows processes; integration marker explicitly enabled
uv run pytest tests/integration/test_acp_windows_real_process.py -m integration -n 0 -x --tb=short --basetemp "$R/checks/953-real-process/pytest" --junitxml "$R/checks/953-real-process/junit.xml"
# 1 passed, real launch/request/close with POSIX-hook sentinels
uv run pytest -c pyproject.toml -s "$R/test_windows_leftovers_guard.py" -n 0 -x --tb=short --basetemp "$R/checks/953-native-guard/pytest" --junitxml "$R/checks/953-native-guard/junit.xml"

Limits: Linux/macOS-specific cases skip on this host. Reader tests with replaced native calls do not verify macOS sysctl/libproc/kqueue behavior. No native macOS/Linux run, full suite or real vendor adapter was exercised.

@LivXue
LivXue merged commit b2b3d52 into main Oct 15, 2026
24 checks passed
@LivXue
LivXue deleted the fix/acp_orphans_on_macos branch October 15, 2026 03:43
Handsome-wzw added a commit that referenced this pull request Oct 16, 2026
…nce, a dead-link cancel (#957)

## Summary

Seven edges of a run on a registered machine, left by the review of #934
and by timing probes on the real machine. Each is a fix the host or the
node makes on its own; nothing changes for an entry without `machine`.

- **A Raven node killed outright no longer leaves agents behind or a
bare `exit -1`.** The next daemon brings every record left at `running`
to `lost` at its start (`Daemon.recover`), ends the agent when its pid
is still the process it was started as (group leader, the recorded
command, and the start mark the launch recorded: Linux's `/proc` start
tick, elsewhere `ps -o lstart`), and writes why; a record without the
mark, or a pid whose mark differs, is left alone and recorded as gone.
`agent/exit` and the attach answer carry that `reason`; the host hands
it on in place of the exit code (`ChannelEndpoint.exited(rc, reason)`),
so a cli run says "the Raven node on this machine was restarted while
the agent ran, and the agent is gone" and a picked-up acp run says the
same. On Linux the agent is also tied to the daemon's life
(`PR_SET_PDEATHSIG`, a SIGTERM). Protocol number unchanged: the field is
optional and old nodes never send it.
- **A failed remote run's error survives a draining-refused announce.**
The manager's two error branches read whether the announce was taken, as
the reply branch does (`announced = taken is not False`), and the acp
backend keeps the record of a prompt that went out and failed, the cli
backend that of a timed-out command, for the caller that reports it. The
next start reads how the task ended off the machine and announces the
error.
- **A cli run picked up after a restart binds its instance to the
session id in its output** (`_bind_picked_up`), as the dispatch's
deferred commit would have; with no id in the output the reply carries
the same "not resumable" warning the dispatch would. A `provisioned` id
or a stateless run binds nothing.
- **A cancel while the machine's link is known dead returns at once.**
`_cancel_turn` reads `ChannelEndpoint.offline` (parked, or the link not
alive), marks the session unsettled and leaves the held `session/cancel`
for the next link, instead of waiting the settle budget for a settlement
that cannot arrive. In the gap before the heartbeat notices nothing
changes.
- **An openai row's tunnel gives up a taken local port for another.**
The port is checked again just before ssh starts (`_still_free`), and
ssh's own "Address already in use" / "Could not request local
forwarding" ends in another try (`_TUNNEL_TRIES` = 3) rather than in a
request sent to whoever took the port; each attempt reads only what ssh
wrote in its own run of the log.
- **A run record that is not JSON, or not UTF-8, is set aside once**
(`<name>.json.unreadable-<ts>`), named in the log that once, instead of
being warned about at every start; a record a later version wrote is
left alone as before. (The first revision read the record as text and
let a `UnicodeDecodeError` out of the whole scan, found by the Windows
review; the record is read as bytes now and decoded by `json.loads`, so
a decoding failure is set aside like a JSON one, and one bad record
never stops the others from being read.)
- **A path written as remote that names no registered machine** is
refused in one sentence for `write_file` and `edit_file` too, never
taken for a relative path here. This has been the case since the debug
switch went (#934); a regression test pins it.

## Type

- [x] Fix
- [ ] Feature
- [ ] Docs
- [ ] CI / tooling
- [ ] Refactor
- [ ] Other

## Verification

Every fix has a test that failed on the old code first.

- `.venv/bin/python -m pytest -n 6 -q tests/test_node_server.py
tests/test_node_client.py tests/test_remote_runs.py
tests/test_subagent_manager.py tests/test_subagent_cli_remote.py
tests/test_remote_file_tools.py tests/test_subagent_acp_remote.py
tests/test_resume_remote.py tests/test_remote_resume.py
tests/test_subagent_acp.py` - all green on macOS (the Linux-only pact
test skipped there); the last run of the four fast files 331 passed, cli
remote 52, remote file tools 24, the acp remote recovery tests 3
- Full suite: 31577 passed, 234 skipped, 2 xfailed in 6 min 14 s; the 3
failures are the ones `main` has on this machine (a host without
LibreOffice, two simulation records). Changed-line coverage: 98.18% of
165 executable changed lines (`scripts/coverage_gate.py diff --base-ref
origin/main`); the 3 uncovered lines are the Linux-only `prctl` import
block, which CI's Linux shards run.
- `ruff check` / `ruff format --check`, `ty check` on the changed
modules, `lint-imports` (10 contracts kept),
`scripts/check_commit_file.py`, `scripts/check_source_language.py`,
`scripts/check_large_files.py`: clean.
- Real machine (ravenx2, an isolated registry copy and Raven home per
tree; the node of each tree installed under its own `node_dir`; the same
probe script on this branch and on `main`):

| Probe | `main` | this branch |
|---|---|---|
| P1 `kill -9` the daemon under a running cli turn | 4.9 s after the
kill: `CLI agent 'x' exited -1`; the loop the daemon had started was
still running (1 left) | 5.1 s: `CLI agent 'x': the Raven node on this
machine was restarted while the agent ran, and the agent is gone`; 0
left; a new daemon pid |
| P2 an acp prompt that fails after it went out, `reported_by_caller` |
the prompt failed (`exit 5`); no record left | record kept; the next
start announced the error with `exit 5` and removed the record |
| P3 cancel while the link is known dead; the cancel on the next link |
the cancel returned after 2.35-2.46 s, held until the link was back (a 2
s backoff in the probe; up to the 5 s budget in production); it landed
on the next link (task frames 3->4) | 0.00 s; landed the same way (3->4
within 0.1 s of the restore) |
| P4 the chosen local tunnel port is taken by a listener | the tunnel
used the taken port; the request timed out against the listener that
held it | the next port; HTTP 200 through the machine |
| P6 a derived-id cli create left running, picked up by the next start |
picked up ok, no binding (`commits=[]`) | picked up ok, the handle bound
to `edge-123` |

After the merge of `main` (which brought #947's MCP streams, #953's
host-death sweep and #963's install fixes into the same files): the
related groups (node server/client/bridge, remote runs, resume, cli
remote, acp remote, manager, remote file tools, acp client, import-cycle
budget) 1006 passed; diff coverage 97.30% of 185 changed lines (the 5
uncovered are the Linux-only `prctl` lines, run by CI's Linux shards);
the real-machine probes re-run on the merged tree: 8/8 PASS (P1 5.2 s, 0
orphans; P2 record kept and the error announced; P3 0.00 s and the
cancel landed on the next link; P4 the next port, HTTP 200; P6 bound to
`edge-123`).

Docs checked against the code before pushing: CONTEXT.md's Raven node
entry (records of failures, set-aside records, the daemon's recovery and
the `reason`), the module docstrings of `remote.tunnel`,
`remote_runs.pending`, `Daemon.recover`, `_still_ours`, `_death_pact`,
`ChannelEndpoint.exited/offline`, `_bind_picked_up`,
`_track_on_machine`, the `agent/exit` comment in `protocol.py`, and the
CHANGELOG entry.

- [x] Relevant tests pass locally
- [x] Relevant lint / type checks pass locally
- [x] User-facing docs or screenshots are updated when needed

## Risk

- [x] Security impact considered - the daemon ends only a pid that is
still the process it started: group leader of its own group, running the
recorded command, and started when the record says (`born`, written at
the launch); a reused pid fails the last mark even for the same program
and is left alone, the record then saying the agent is gone. No new
network surface; the `reason` is the node's fixed sentence, never user
or agent text.
- [x] Backward compatibility considered - `agent/exit`'s `reason` is
optional, protocol number unchanged; a host on this branch reads an old
node's exits as before, an old host ignores the field. Records written
before this branch read unchanged.
- [x] Rollback path is clear for risky changes - a plain revert; nothing
is migrated.

## Related Issues

Follow-up to #934 (the two non-blocking notes of its review) and to the
timing probes run on #921/#922/#934.

---------

Co-authored-by: Claude (claude-fable-5-1) <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.

3 participants