Repository navigation
feat(*): a Raven daemon on a registered machine, reached over one watched ssh link - #920
Handsome-wzw wants to merge 13 commits into
Conversation
…ched ssh link A registered machine gets this Raven's own code as a daemon (raven/node): started by the relay the host's ssh runs there (`python -I -m raven.node --attach --dir <node_dir>`) when none answers on its socket under the node directory, and left on its own once idle. It answers file calls with Raven's own tools fenced to the directories each call names (fs/read, list, find, grep, stat, write, edit, pull), starts acp agents under task ids of the host's in their own process groups in the machine user's login environment, keeps each agent's frames in the task's record on disk so a connection that goes leaves the agent running and the next one takes it over from the frame the host last read (agent/attach), lists the tasks it runs or remembers, and probes the machine and an agent's command. It opens no network port; only --stdio ties the agents to the connection. `raven ops connection install-node <id>` puts it on a machine after one look and one question, and `--prune` removes older installs. The host keeps one link per machine (raven/node/client.py): the ssh line runs the relay, a heartbeat watches the link, a lost link parks the agents on it and is tried again on a backoff for as long as any waits, and a link that comes back attaches every task again by its id, sending what the host wrote meanwhile and any cancel asked in between. The registry's package tells a remote path from a local one and lends the tools the pool through one seam (raven/ops/remote_path.py); the machine lane stays debug-only (RAVEN_DEBUG_MACHINES=1). First of three changes on this design; the next runs acp sub-agents on a machine through this daemon, the one after reads and writes files there by address. Nothing dispatches to a machine yet. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: the node installer must use uv instead of pip.
I reviewed the full github/main...HEAD diff and covered the repository rules (AGENTS.md, CLAUDE.md, CONTEXT-MAP.md, and the Raven node additions to CONTEXT.md), affected ACP/node/file-path callers, the single-commit history, backward compatibility of the generalized ACP endpoint, test changes, and the documented layer-seat constraints. I found no weakened tests.
Verification after uv sync --all-extras --all-groups:
uv run pytest tests/test_acp_client_teardown.py tests/test_acp_remote.py tests/test_cli_ops_connection_commands.py tests/test_node_bundle.py tests/test_node_client.py tests/test_node_install.py tests/test_node_relay.py tests/test_node_server.py tests/test_ops_connection_add.py tests/test_ops_remote_path.py tests/test_ops_transport.py -q- 443 passed, 4 skipped because/bin/tcshand/bin/cshare unavailable.uv run pytest tests/test_subagent_acp.py tests/test_acp_protocol.py tests/test_acp_journal.py -q- 365 passed.- Focused Ruff, source-language, large-file, commit, and diff-whitespace checks passed (the
makeexecutable is unavailable, so the two repository check scripts were invoked directly).
The initial focused pytest attempt exercised no tests because the fresh environment lacked the local raven_everos development package; synchronizing the declared extras/groups resolved that setup issue.
One blocker is marked inline.
|
Windows compatibility review: two findings. Scope: complete 34-file PR diff at
Native Windows / Python 3.13.12, uv run python 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\probe_windows.py' node-ssh
# 12 remote-command arguments instead of 1; native producer/parser mismatch
uv run pytest 'tests/test_node_bundle.py' 'tests/test_ops_remote_path.py' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr920\node-tests-pytest'
# 93 passed, 1 skipped (symlink privilege)
uv run pytest 'tests/test_node_client.py' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr920\client-tests-pytest'
# 24 passed, 29 skipped (POSIX-shell fixtures)
uv run pytest 'tests/test_node_server.py' 'tests/test_node_relay.py' -n 0 -x --collect-only --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr920\collect-pytest'
# collection failed: ModuleNotFoundError: fcntl; exit 1
uv run pytest 'tests/test_acp_client_teardown.py' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr920\teardown-pytest'
# 1 failed: signal.SIGKILL absent; exit 1
uv run pytest 'tests/test_acp_client_teardown.py' -k 'without_process_groups or platform_is_read' -n 0 -x --tb=short --basetemp 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\5f9cc24f22014021985a6c637edc57e3\pr920\windows-teardown-pytest'
# 2 passed, 2 deselectedThe argument probe uses the real |
|
Blocking: 1 finding needs correction before this revision can merge; it is the first of the notes below. Scope: none of this PR's 34 files is under Notes:
Verification:
Checked and deliberately not reported:
|
LivXue
left a comment
There was a problem hiding this comment.
Requesting changes: 1 blocking finding, with 5 nonblocking notes and 1 question, is in the PR-level summary comment. None is filed inline, as no file in this PR is under raven/agent/**, the part of the tree I review; the summary also carries the scope statement and the verification log.
…when the machine has none The install ran `python -m venv` and `python -m pip install` on the machine, against the repository's rule that uv is the only Python package manager. Everything Python now goes through uv: `uv venv` of the machine's own Python 3.12 or of one uv keeps inside the node directory, `uv pip install` for the pinned packages and the optional search package. A machine without uv gets one under `<node_dir>/.uv` first, from uv's own installer fetched with the machine's curl or wget and told to touch nothing else; the probe looks for curl and wget, and for a uv an earlier install put there; with no uv and no way to fetch one the install stops and says so. The plan counts the room uv takes and says when it is fetched. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
…h argv goes as it is The socket's fallback for a long node directory lay in the temp directory itself, so the relay forwarded the host's lines to whatever listened at that path, and a file there the user could not remove kept the daemon from starting: the fallback now lies in a directory of the user's own (0700, checked, never a link or another's), the relay forwards only to a socket of the user's, and a daemon refused its socket's place says so in one line and leaves. The task records -- the agent's frames as they were -- were made with the default mode: tasks/, each record and its three files are the owner's alone now, as the host's journal is, and the installer makes the node directory so. A page read, a digest, a chunk and a checked write ran on the daemon's one loop, which also answers the heartbeat: they run in a thread now, writers one at a time under a lock. The ssh argv went to the connection as one shell-quoted line, which a Windows host's own grammar split into twelve words: it goes as the argv, which no host reads again. A call on its way to a node counts as use, so a link is not closed under it as idle. A drive-relative or extended Windows path (C:file, \\?\C:\...) no longer reads as a machine's. The daemon's tests skip where there is no fcntl, the POSIX teardown tests lend a SIGKILL where the platform has none, the login-shell test checks a directory of its own instead of /nonexistent, a node is run with nothing importable beyond its requirements, and remote.py says what does reach a model: a path on the machine, written the way scp writes it. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
Thank you for the two reviews. Every point is in the commits now on this branch; the uv-only installer is the commit before them.
Windows: |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
Reviewed the new-head delta and its integration with the full PR diff, callers, and history. The earlier uv-only blocker is fixed: node setup now uses uv venv and uv pip install, obtains a private uv when possible, and refuses installation when uv cannot be obtained. The host-safe SSH argv, owner-only socket/records, off-loop whole-file work, in-flight call accounting, Windows collection guards, import-budget coverage, and remote-path documentation changes also match the failures previously reported.
One nonblocking test-portability issue is marked inline. It is introduced and reproducible under an ordinary umask 0002, but it affects only the assertion, has the umask 0022 workaround, and does not leave an operator without a working product path, so it does not meet the round-four blocker bar.
Verification: the affected suite produced 462 passed, 4 skipped, 1 failed on Python 3.12.3; the sole failure is the inline umask assumption. The isolated case failed the same way on Python 3.13.2 and passed under umask 022. ACP regressions passed (365). Ruff, all 10 import contracts, source-language and large-file gates, and git diff --check passed. make is unavailable in this environment, so I ran the two canonical gate scripts directly with their documented revision range.
Coverage included AGENTS.md/CLAUDE.md and CONTEXT routing, the diff and new commits, callers, history, backward compatibility, test weakening, and the stated architecture boundaries. The Windows module skips occur before POSIX imports and do not skip those tests on POSIX.
…test sets its own mode The record opener handed os.fdopen a splatted keyword map, which no overload of it matches under the type check: the binary and the text mode are two calls now. The test of another user's directory asserted the mode mkdir leaves, which is the umask's (0775 under 0002, where it failed): the test sets 0755 first and asserts it is left so. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the complete delta from the previously assessed head. The explicit chmod(0755) fixes the only outstanding suggestion under both tested umasks, and splitting the binary/text os.fdopen calls preserves runtime behavior while satisfying the type-check overloads. I found no new issue.
Verification: the isolated ownership test passed under umask 0002 and umask 022; all 90 node-server tests passed; the previously affected 12-file suite passed with 463 tests and 4 environment skips; focused Ruff and ty check raven/node/server.py passed; git diff --check passed.
Coverage for this revision included the new diff, its own_file callers, commit history, backward compatibility, test-strength preservation, and the already-reviewed AGENTS.md/CLAUDE.md, CONTEXT terminology, and architecture constraints. This delta changes no dependency, domain term, public interface, or architectural edge.
|
Windows compatibility re-review: the previously reported SSH argv and POSIX-import/teardown failures are addressed in the exercised paths; one test expectation needs updating. Scope: all 35 changed files at P2 - Update the unknown-machine fixture for Windows drive-relative paths. Reproduced. tests/test_ops_remote_path.py:101 still expects The native launch probe confirmed that the real node preparation passes an argv list, preserves a key path containing spaces, and supplies exactly one remote-command argument to the intercepted connection. A real local file was readable through absolute, drive-relative and extended Windows spellings with the machine lane enabled. Both POSIX daemon test modules now skip before importing fcntl; the four teardown tests pass. Native Windows / Python 3.13.12. Git fetching failed; the isolated fixed-SHA snapshots were reconstructed and every blob plus the complete Git tree was verified. A fresh environment was created with uv run pytest tests/test_node_bundle.py tests/test_ops_remote_path.py tests/test_acp_client_launch.py tests/test_acp_client_teardown.py tests/test_acp_remote.py tests/test_node_client.py tests/test_node_install.py tests/test_node_server.py tests/test_node_relay.py -k 'not path_that_names_no_registered_machine and not each_agent_gets_an_owner_only_ssh_log' -n 0 -x --tb=short --basetemp "$R/checks/920-final-host/pytest" --junitxml "$R/checks/920-final-host/junit.xml"
# 235 passed, 58 skipped, 13 deselected. POSIX daemon/shell/install and symlink-privilege cases skipped.
uv run pytest tests/test_ops_remote_path.py -k path_that_names_no_registered_machine -n 0 -x --tb=short --basetemp "$R/checks/920-final-path/pytest" --junitxml "$R/checks/920-final-path/junit.xml"
# 11 passed, 1 failed (a:b.txt), 79 deselected.
uv run pytest C:/Users/livxu/AppData/Local/Temp/raven-windows-review/2d5904b4d92f4f238e9db27e2ade2ddf/test_windows_resolved_paths.py -k 'not executable_spellings' -s -n 0 -x --tb=short --basetemp "$R/checks/920-final-native/pytest" --junitxml "$R/checks/920-final-native/junit.xml"
# 2 passed, 1 deselected; scratch argv/path probes, no SSH connection.
# Exact API-base control, with source imports pinned to that snapshot:
uv run pytest tests/test_acp_remote.py::test_each_agent_gets_an_owner_only_ssh_log -n 0 -x --tb=short --basetemp "$R/checks/920-platform-control/pytest" --junitxml "$R/checks/920-platform-control/junit.xml"
# 1 failed: the same POSIX 0700 mode assertion as at the inspected head.The host selection excludes the separately reproduced path-error group and The daemon ownership/umask discussion is already resolved and is not repeated here. Remote daemon hardening was code-reviewed; no live SSH, remote installation, remote daemon execution or full suite ran. Windows skips do not establish those POSIX behaviors. |
|
No blockers; suggestions only, and they are marked inline. The new Windows evidence supersedes my earlier clean stance. I verified the reported mismatch at I am carrying this as the one nonblocking follow-up already identified in the Windows re-review; I am not duplicating it as another inline finding. The production correction is sound, and updating the fixture is straightforward. |
…host reads as a drive On a Windows host one letter before the colon is a drive, so `a:b.txt` is this computer's there and the test that expects a refusal failed (reviewed 2026-10-12); a two-letter name reads as a machine's everywhere. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the complete delta from the previously assessed head. Replacing the ambiguous one-letter fixture with ab:b.txt fixes the Windows expectation without weakening coverage: a:b.txt remains exercised as a Windows drive-relative local path, while the two-letter name remains an unknown remote host on both platform branches. I found no new issue.
Verification: tests/test_ops_remote_path.py passed all 91 tests; direct probes exercised both _WINDOWS branches and preserved the intended distinction; focused Ruff and git diff --check passed.
Coverage for this revision included the changed test, the parser behavior it asserts, callers and history, backward compatibility, test-strength preservation, and the already-reviewed AGENTS.md/CLAUDE.md, CONTEXT terminology, and architecture constraints. This delta changes no production code, dependency, domain term, interface, or architectural edge.
|
The fixture is updated in 7b3757c, now on this branch: the colon-in-a-name case names a two-letter host ( |
|
Windows compatibility re-review: the drive-relative fixture is corrected; one intermittent test failure remains. Scope: the complete 35-file PR at P2 - Do not require reattachment to be the last heartbeat request. Reproduced intermittently on native Windows. tests/test_node_client.py:901 checks Native Windows, Python 3.13.12; fresh run-owned environment from $R = 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\abf44f96254d43bc81b1be33079fb26b'
uv run pytest tests/test_ops_remote_path.py tests/test_acp_client_launch.py tests/test_acp_client_teardown.py tests/test_node_bundle.py tests/test_node_client.py tests/test_node_server.py tests/test_node_relay.py -n 0 -x --tb=short --basetemp "$R\checks\920-host\pytest" --junitxml "$R\checks\920-host\junit.xml"
# 116 passed, 32 skipped, 1 failed (heartbeat assertion).
uv run pytest tests/test_node_client.py::test_a_heartbeat_unanswered_loses_the_link_tells_its_agents_and_reconnects -n 0 -x --tb=short --basetemp "$R\checks\920-heartbeat\pytest" --junitxml "$R\checks\920-heartbeat\junit.xml"
# 1 passed.
uv run pytest tests/test_node_client.py -n 0 -x --tb=short --basetemp "$R\checks\920-node-client\pytest" --junitxml "$R\checks\920-node-client\junit.xml"
# 14 passed, 29 skipped, 1 failed (same assertion).All 91 remote-path tests passed, including the corrected |
|
No blockers; suggestions only, and they are marked inline. The new Windows evidence supersedes my clean stance. I confirmed the race at I am carrying the intermittent assertion as the one nonblocking follow-up already identified in the Windows re-review, without duplicating it as my own inline finding. Production reconnection behaved correctly in the probe, and the test can assert that the expected attachment occurred without requiring heartbeat traffic to stop. |
…equests, not as its last The test of a link that comes back read the new link's last request as the attach, and its heartbeat, every 10 ms there, can follow the attach before the check: it now looks for the attach among the requests (reproduced on Windows, 2026-10-12). Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
Fixed in 418923f: the reconnect test looks for the attach among the new link's requests instead of reading its last request, so a heartbeat that follows the attach no longer fails it. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the complete delta from the previously assessed head. The reconnect test now searches the replacement client's requests for the exact agent/attach method and payload instead of requiring it to remain last after heartbeat scheduling resumes. That fixes the reproduced race without weakening the behavior under test. I found no new issue.
Verification: all 55 tests/test_node_client.py tests passed with xdist disabled, focused Ruff passed, and git diff --check passed.
Coverage for this revision included the changed assertion, the heartbeat/reconnect ordering and callers it exercises, commit history, backward compatibility, test-strength preservation, and the already-reviewed AGENTS.md/CLAUDE.md, CONTEXT terminology, and architecture constraints. This test-only delta changes no production behavior, dependency, interface, domain term, or architectural edge.
A review date in a source comment is the task's context, not the code's: the four comments that carried one keep their reason and drop the day. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the complete delta from the previously assessed head. It removes transient review-date wording while retaining the durable explanations for Windows argv handling and heartbeat interleaving. No executable behavior changed, and I found no new issue.
Verification: the directly associated ACP launch/remote and node-client suites passed with 145 tests and 4 unavailable-shell skips; focused Ruff, the source-language gate, and git diff --check passed.
Coverage for this revision included all changed lines, their documented callers and history, backward compatibility, test-strength preservation, and the already-reviewed AGENTS.md/CLAUDE.md, CONTEXT terminology, and architecture constraints. This comment-only delta changes no dependency, domain term, public interface, or architectural edge.
|
Windows compatibility re-review: the reported heartbeat-test race is fixed. No additional Windows compatibility issue was found in the inspected scope. Reviewed the complete 35-file PR at Native Windows / Python 3.13.12; fresh run-owned environment prepared with $R = 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\0605c160aa864b11bad6b7d18f24455f'
uv run pytest tests/test_ops_remote_path.py tests/test_acp_client_launch.py tests/test_acp_client_teardown.py tests/test_node_bundle.py tests/test_node_client.py tests/test_node_server.py tests/test_node_relay.py -n 0 -x --tb=short --basetemp "$R\checks\920-host\pytest" --junitxml "$R\checks\920-host\junit.xml"
# 127 passed, 33 skipped.The complete selected node-client file passed, including the previously failing reconnect test. Skips cover POSIX-only SSH/daemon fixtures and unavailable symlink privilege. No live SSH, remote installation/daemon operation, or full repository suite was run. |
|
No blockers in the reviewed scope. Scope: this re-review covers the six commits since First-round notes, at this head:
New notes, all nonblocking:
Verification:
Checked and deliberately not reported:
|
…p's index, stay in node_dir Four notes of the review, each measured on this branch: - A page read decoded and split the whole file, two C calls that hold the interpreter for as long as the file takes (7.5 s on 1 GiB), so a node's heartbeat went unanswered and a host's turn stood still. `read_file` now reads the file a chunk at a time, counts the line breaks `splitlines` knows as the bytes come, holds back what could begin a break at a chunk's edge, and decodes only the page's own lines; the file is never whole in memory. Same lines, same footer, and a byte that is not UTF-8 refuses the page it is on as it refused the file. - In a worker thread a page read and the digest taken beside it could land inside a write of the same file and leave with the old page and the new digest, which a later write carrying that digest as `expect` would take as leave. The whole-file calls now go through one gate: readers together, a writer alone, a waiting writer before new readers. - uv reads no pip configuration, so a machine that reaches only the mirror pip is configured with could not be installed. The probe reads that index the way pip does (`PIP_INDEX_URL`, then `index-url` in pip's files, user over system, install over global), the plan names it, and `uv pip install` gets it as `--default-index`. - The uv fetched for a machine without one wrote a receipt under the home directory and was whichever version was current; it is now one pinned version, installed unmanaged, which writes no receipt and changes no PATH. And `node_dir` is made owner-only only when the install makes it: one that was there is the owner's. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
The four nonblocking notes are fixed in 01b0e5ad2 (carried to #921, #922 and #934 the same way as the earlier commits):
Verification: |
gloryfromca
left a comment
There was a problem hiding this comment.
Blocking: the install confirmation must not expose credentials embedded in the configured package-index URL.
Reviewed the complete delta from 9fa6129cdcf7 to b6351d970434, including the repository rules and canonical Machine/Raven-node terms, all six changed files, the affected file-tool and installer callers, relevant history, compatibility, test assertions, and the daemon's read/write architecture. I found one blocker, filed inline. It meets the post-fourth-round bar: this commit introduces the disclosure; authenticated private indexes are an ordinary configuration for the package-index support added here; and an operator who invokes install-node has no escape because the plan is printed before either confirmation or installation, including with --yes. I found no weakened tests.
Verification: uv run pytest tests/test_filesystem_tools.py tests/test_node_install.py tests/test_node_server.py -q passed all 175 tests; 1,000 randomized valid-Unicode/chunk-boundary cases matched str.splitlines(); Ruff, the source-language gate, and git diff --check passed. I also verified the pinned uv 0.13.0 installer URL resolves and its unmanaged mode disables PATH changes and receipts.
…in pip may keep inside it pip takes an index with a sign-in in the URL (`https://user:password@host/simple`), and the plan that names the index is printed before the confirmation, and under `--yes`, so the password reached the terminal and any transcript of it. The plan now shows the host and the path only, saying so when a sign-in was left out; uv still gets the full URL, and a failed step's sentence, which quotes uv's own last line, is shown the same way. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
Reviewed the revision delta from b6351d970434 and the credential flow through the install command, plan display, uv execution, and bounded failure text. The fix removes package-index userinfo from every operator-visible sentence while preserving the full shell-quoted URL for uv. The added tests cover plan text, execution, failure output, and IPv6, and they strengthen rather than weaken the existing assertions.
I also rechecked the repository rules, relevant callers and history, backward compatibility, and the existing SSH/node architecture constraints. Verification: uv run pytest tests/test_node_install.py -q (58 passed), uv run ruff check raven/node/install.py tests/test_node_install.py, the source-language gate, and git diff --check github/main...HEAD all pass. No security candidate survived the focused scan.
|
Windows compatibility re-review: no new Windows compatibility issue found in the inspected scope. Scope: the complete 37-file PR at Native checks covered drive-relative/UNC/extended-path classification, host SSH argv handling, ACP teardown and node reconnection. The changed paging tests preserve CRLF and Unicode line boundaries across chunk sizes and report invalid UTF-8 on the requested page. Pure installer-plan tests passed for the inherited remote package index, credential redaction, pinned unmanaged uv and command quoting; no installer was executed. This corroborates the existing resolved credential-display discussion, without filing it again. Native Windows / Python 3.13.12; fresh per-head environment prepared with $R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/09bd90f70d7a4516ab71a23a267eb9e3'
uv run pytest tests/test_ops_remote_path.py tests/test_acp_client_launch.py tests/test_acp_client_teardown.py tests/test_node_bundle.py tests/test_node_client.py tests/test_node_server.py tests/test_node_relay.py -n 0 -x --tb=short --basetemp "$R/checks/920-host/pytest" --junitxml "$R/checks/920-host/junit.xml"
# 127 passed, 33 skipped; exit 0.
uv run pytest tests/test_filesystem_tools.py::test_a_page_is_the_lines_splitlines_gives_whatever_the_chunk_edge tests/test_filesystem_tools.py::test_read_file_pages_a_file_without_holding_all_of_it tests/test_node_install.py::test_the_index_pip_is_configured_with_is_handed_to_uv_and_named_in_the_plan tests/test_node_install.py::test_a_sign_in_inside_the_index_url_reaches_uv_and_never_a_sentence tests/test_node_install.py::test_a_machine_without_uv_fetches_one_version_of_it_under_the_node_dir_and_nowhere_else tests/test_node_install.py::test_the_steps_build_a_virtualenv_with_uv_then_fill_it_with_uv tests/test_node_install.py::test_uv_makes_its_python_inside_the_node_dir -n 0 -x --tb=short --basetemp "$R/checks/920-changed/pytest" --junitxml "$R/checks/920-changed/junit.xml"
# 13 passed; exit 0.Limits: the skips cover POSIX-only daemon/SSH fixtures and unavailable symlink privilege. The reader/writer gate and remote shell installation changes were code-reviewed only; their POSIX integration tests did not run on Windows. No live SSH, remote installation/daemon operation, prospective merged-tree test or full repository suite was run. |
|
Blocking: 1 finding needs correction before this revision can merge; see the inline note. Scope: this re-review covers the two commits since What I examined: the Confirmed findings:
Verification:
The previous round's four nonblocking notes were addressed in This summary supersedes the No note is filed against |
…han a chunk costs its length The paged read held the line in progress whole and split it again with every chunk, so a file with no line break in it cost time square in its size: a 256 MiB single-line file took half a minute where the whole-file read took under a second, and one near 1 GiB would have outlasted the node's call timeout. Minified bundles, single-line JSON and machine-written CSV are that shape. Each chunk is split once now, and the bytes of the line in progress are kept only while that line may be on the page; off the page it is counted, not held. The same 256 MiB single-line file reads in a third of a second, on the page or off it, and the page is still exactly what splitlines gives. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
The blocking finding is fixed in 6a0ec2c and answered on its thread: the paged read looks at each chunk once, so a line longer than a chunk costs its length and no more, and the 256 MiB single-line file reads in a third of a second here, on the page or off it. The three observations on |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
The long-line regression is fixed: each chunk is split once, requested line bytes are retained only while needed, and both on-page and off-page probes scale linearly. The new property tests preserve splitlines behavior and directly prevent the repeated-scan failure rather than weakening coverage.
I reviewed the revision delta, helper callers, relevant history and backward compatibility, repository rules, and the existing node/file-tool architecture constraints. Verification: the filesystem, node-client, and node-server suites pass all 178 tests; Ruff and git diff --check github/main...HEAD also pass.
Named maintainer-deferred follow-ups from the record: make the machine file gate per-file rather than global, avoid exposing authenticated index URLs through process argv, and turn a nonnumeric configured index port into a handled install error rather than a traceback. These remain nonblocking for this change.
|
No blockers in the reviewed scope. Scope: this supersedes my Confirmed findings:
Still open, not filed against this change:
The author has said these three want a change of their own and will be taken up apart from this PR, which is a reasonable split; they are recorded here so they are not lost. Verification:
The two conclusions this replaces: my |
|
Windows compatibility re-review: no additional Windows compatibility issue found in the inspected scope. The long-line paging regression's new native tests pass. Scope: the complete 37-file PR at Native Windows / Python 3.13.12, fresh per-head environment from $R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/d1d0b660deeb48e3a2e4d079a3316cbc'
uv run pytest tests/test_filesystem_tools.py::test_a_page_is_the_lines_splitlines_gives_whatever_the_chunk_edge tests/test_filesystem_tools.py::test_a_line_longer_than_a_chunk_is_whole_on_its_page_and_only_counted_off_it tests/test_filesystem_tools.py::test_a_long_line_is_looked_at_once_whatever_its_length tests/test_filesystem_tools.py::test_read_file_pages_a_file_without_holding_all_of_it -n 0 -x --tb=short --basetemp "$R/checks/920-paging/pytest" --junitxml "$R/checks/920-paging/junit.xml"
# 12 passed; 1 advisory idle-ceiling warning.
uv run pytest tests/test_ops_remote_path.py tests/test_acp_client_launch.py tests/test_acp_client_teardown.py tests/test_node_bundle.py tests/test_node_client.py tests/test_node_server.py tests/test_node_relay.py -n 0 -x --tb=short --basetemp "$R/checks/920-host/pytest" --junitxml "$R/checks/920-host/junit.xml"
# 127 passed, 33 skipped.Paging coverage includes CRLF/Unicode separators, chunk boundaries, long lines on and off the requested page, and the property that bytes are split once. Host checks cover Windows path classification, SSH argv, ACP teardown and node reconnect behavior. No native performance benchmark or remote daemon was run; skipped cases require POSIX fixtures or symlink privilege. The already recorded node/installer follow-ups remain separate. No live SSH, installation, UI execution or full repository suite. |
LivXue
left a comment
There was a problem hiding this comment.
Blocking: this full-PR review found five correctness issues (2 P1, 3 P2) in node recovery and daemon state management. The five inline comments describe the failing states and repair directions.
This review includes raven/node and supersedes any merge-readiness impression from my earlier agent-only and Windows compatibility notes. The earlier long-line paging finding remains resolved; its 12 regression cases pass.
Reviewed head: 6a0ec2c12cf4ae928de4e4b6464c85e99516937b. Verification used native Windows, Python 3.13.12, and a freshly synced isolated environment. All 4,375 blobs in the source archive were checked against the GitHub tree, and the source was checked again after the probes.
- Relevant repository tests: 338 passed, 10 failed, 61 skipped. All 10 failing test IDs also failed in the same-machine merge-base control (
829eb28e1db877dffdd3df4f835d707e1f049d3a); the comparison used JUnit test IDs, not just counts. - Controlled recovery probes: 4 failed, 1 passed. Daemon state probes: 2 failed, 2 passed. These are six violating states covering the five findings, with three positive controls.
uv run --frozen --no-sync lint-imports: 10 contracts kept.
Repository test command:
uv run --frozen --no-sync pytest tests/test_node_bundle.py tests/test_node_client.py tests/test_node_install.py tests/test_node_server.py tests/test_node_relay.py tests/test_acp_client_launch.py tests/test_acp_client_teardown.py tests/test_acp_remote.py tests/test_ops_remote_path.py tests/test_cli_ops_connection_commands.py tests/test_filesystem_tools.py tests/test_ops_connection_add.py tests/test_ops_transport.py -n 0 -q --tb=short
Verification limits: no real SSH, remote installation, or POSIX daemon end-to-end run. The socket finding uses the production path function with a fixed POSIX UID substitute; the launch-limit probe uses production request/launch logic with mocked subprocess creation and POSIX directory setup. An initial selection also included unchanged tests/test_read_file_image.py, whose unguarded os.geteuid call prevents Windows collection; that file was excluded from the completed selection, and the base has the same call.
The existing file-gate/index observations are not filed again. A separate error-redaction hypothesis was excluded because the real local uv failure did not establish the proposed credential leak.
…ry, a launch reserves its place A full review of the link's recovery and the daemon's state found five states they got wrong. Each is reproduced by a test of its own before the fix, and three of them on a real machine as well: a dead link replaced by a plain call left its agent parked for good; a cancel asked while the link was dead never arrived and the task ran on; eleven launches at once started nine against a limit of eight. A link the heartbeat had not yet seen die was replaced by the next call without its agents: the pool now attaches whatever waits for the machine on every link it makes, and the call hears why when that link goes too. A replay the new link could not carry escaped the reconnect with the frames already dropped and the endpoints after it in no list: a frame leaves the held list only once the link took it, the endpoint being replayed and every one not yet reached are parked again with what they hold, and the link is tried again. A cancel is wanted from the moment it is asked until the node takes it or says there is nothing to take; a link that dies under the request, or had died unnoticed, keeps it for the next link. The daemon's long-path socket is named for the install as well as the directory, as the socket under run/ is, so two installs on one long directory listen apart. A launch takes its place against the agent limit before its first await, so launches served together cannot pass the check as one. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
All five are fixed in 093966f and answered on their threads, each with the test that reproduces your probe. The three a real machine can show were run there before and after the fix, from the same script (the relay killed and a plain call made before the heartbeat could notice; a cancel asked in that gap; eleven launches at once): the run before reproduced each as you described it, the run after passes. The node test groups pass with the seven new tests, and the fix is carried to #921, #922 and #934. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
Reviewed the delta from 6a0ec2c12cf4: ordinary link replacement now reattaches parked endpoints; replay removes frames only after delivery and reparks the remaining endpoints on failure; cancellation intent survives failed delivery; long socket fallbacks include the install identity; and launches reserve capacity before their first await. The new tests exercise each reported race or collision and do not weaken prior assertions.
I also checked the affected callers, reconnect and daemon lifecycle, relevant history and backward compatibility, repository rules, and the existing node architecture constraints. uv run pytest tests/test_node_client.py tests/test_node_server.py -q passes all 155 tests; Ruff and git diff --check github/main...HEAD pass. The previously named file-gate scope, authenticated-index argv, and invalid-index-port follow-ups remain nonblocking.
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
Reconfirmed the checked-out and live PR head are both 093966f8fa6d; the already-reviewed revision delta and its verification leave the three previously named maintainer-deferred follow-ups as nonblocking. This revision can merge as it stands.
|
Windows compatibility re-review: no additional Windows compatibility issue found in the inspected scope. Scope: all 37 changed files at Native Windows / Python 3.13.12: 144 passed, 33 skipped. The selected tests cover Windows path classification, ACP argv/teardown, bundling, file paging and the new ordinary-call reconnect, failed replay and cancellation-retention cases. Those recovery cases use controlled client substitutes. Socket fallback namespacing and launch reservations were checked in the POSIX daemon code; its Unix-socket/process-group tests do not run on this host. Exact verification commandsFresh isolated uv environment and detached checkout; source imports verified against this head, with separate home/temp/token directories. Setup succeeded: $R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/c281ddf96c3446c98e4f4127722b59c6'
$cases = @(
'tests/test_ops_remote_path.py',
'tests/test_acp_client_launch.py',
'tests/test_acp_client_teardown.py',
'tests/test_node_bundle.py',
'tests/test_node_client.py',
'tests/test_node_server.py',
'tests/test_node_relay.py',
'tests/test_filesystem_tools.py::test_a_page_is_the_lines_splitlines_gives_whatever_the_chunk_edge',
'tests/test_filesystem_tools.py::test_a_line_longer_than_a_chunk_is_whole_on_its_page_and_only_counted_off_it',
'tests/test_filesystem_tools.py::test_a_long_line_is_looked_at_once_whatever_its_length',
'tests/test_filesystem_tools.py::test_read_file_pages_a_file_without_holding_all_of_it'
)
uv run pytest @cases -n 0 -x --tb=short --basetemp "$R/checks/920-host/pytest" --junitxml "$R/checks/920-host/junit.xml"Limits: no real SSH/remote install, POSIX daemon end-to-end execution, full repository suite or Windows ACL audit. Platform skips do not establish equivalent Windows support. |
…ackoff for the agents that wait The previous fix had a link replaced by an ordinary call bring the parked agents along, but only when the relaunch succeeded. When it failed -- the machine unreachable at that moment -- the call raised and nothing tried again: the agents stayed parked with no reconnect task, where the same failure under the heartbeat would have been retried on the backoff. On a real machine, the relay killed and the relaunch made to fail once, the agent was still parked thirty seconds after the machine was reachable again. A failed relaunch with agents waiting now schedules the same reconnect the heartbeat loss does; the agent is attached again on the first retry once the machine answers. Co-authored-by: Claude (claude-fable-5-1) <noreply@anthropic.com>
|
One more commit, a6cc40d: the ordinary-call path of the first finding was fixed only for a relaunch that succeeds. A relaunch that fails with agents parked now schedules the reconnect the heartbeat loss does, so the agents come back on the backoff once the machine answers, as the module's contract says. Reproduced on a real machine before the fix (the relay killed, the relaunch made to fail once: parked = 1, no reconnect task, still parked 30 s after the machine was back) and gone after it (a reconnect task scheduled at once, the agent attached again on the first retry after the machine answered, nothing left running there); the unit test fails on the previous head and passes here. Carried to #921, #922 and #934. |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
Reviewed the two-file delta from 093966f8fa6d, the surrounding acquisition/reconnect state transitions, the added regression case, and the existing project constraints. The failed ordinary-relaunch path now schedules the same guarded backoff recovery used after heartbeat loss, while an in-progress reconnect remains unique. uv run pytest tests/test_node_client.py -q passes all 61 tests, Ruff passes on both changed files, and git diff --check github/main...HEAD is clean. The three previously named maintainer-deferred node follow-ups remain nonblocking.
|
Windows compatibility re-review at Reconciled the complete 37-file PR diff with the previous review at No additional Windows issue found in this retry change. Native host/paging checks: 145 passed, 33 skipped. The new controlled-client regression confirms that a failed ordinary relaunch schedules a retry and reattaches the parked endpoint when a later launch succeeds. POSIX daemon and stand-in shell cases remain skipped. Exact verification commandsNative Windows / Python 3.13.12, fresh isolated uv environment and detached exact-head checkout; imports verified, with separate home/temp/token directories. Setup succeeded: $R = 'C:/Users/livxu/AppData/Local/Temp/raven-windows-review/615c117f238046cca9805459d4b37a77'
$cases = @(
'tests/test_ops_remote_path.py',
'tests/test_acp_client_launch.py',
'tests/test_acp_client_teardown.py',
'tests/test_node_bundle.py',
'tests/test_node_client.py',
'tests/test_node_server.py',
'tests/test_node_relay.py',
'tests/test_filesystem_tools.py::test_a_page_is_the_lines_splitlines_gives_whatever_the_chunk_edge',
'tests/test_filesystem_tools.py::test_a_line_longer_than_a_chunk_is_whole_on_its_page_and_only_counted_off_it',
'tests/test_filesystem_tools.py::test_a_long_line_is_looked_at_once_whatever_its_length',
'tests/test_filesystem_tools.py::test_read_file_pages_a_file_without_holding_all_of_it'
)
uv run pytest @cases -n 0 -x --tb=short --basetemp "$R/checks/920-host/pytest" --junitxml "$R/checks/920-host/junit.xml"
# 145 passed, 33 skippedLimits: no real SSH, remote installation, POSIX daemon end-to-end run, full repository suite or Windows ACL audit. Passing host tests does not establish equivalent Windows daemon support. |
…node # Conflicts: # raven/acp_client/client.py
|
Merge conflict: @Handsome-wzw the main branch tip has advanced to d106a5c and this PR no longer merges onto it cleanly. Re-running the merge check against the live tip: reports a content conflict in raven/acp_client/client.py (the other files auto-merge). The judge has cleared the current head, so a rebase onto the latest main, resolving raven/acp_client/client.py, is the one step left before this PR can enter the acceptance lane. Posted by the review board after re-running the merge check. |
|
Blocking: the branch must be rebased onto current main and the conflict resolved. Confirmed against live |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; suggestions only, and they are marked inline.
Reviewed the merge revision against live main, including the raven/acp_client/client.py conflict resolution, its process and remote-endpoint callers, and the retained PR diff. The resolution preserves the node endpoint/argv work while integrating main's process-tree teardown and leftover-agent bookkeeping. The live merge check and git diff --check github/main...HEAD pass; the focused ACP lifecycle and node client/server suites pass all 242 tests, and Ruff passes on the integrated files. This settles my merge-conflict blocker. The three previously named maintainer-deferred node follow-ups remain nonblocking.
Summary
First of three changes that let a Raven dispatch work to the machines in
its machine registry on a design where the machine owns the work: a Raven
daemon runs there, the ssh is only the way to it, and an ssh that goes
mid-task does not end the task. This one puts the daemon on a machine and
keeps the link to it; nothing dispatches to a machine yet. The next change
runs acp sub-agents through it, the one after reads and writes files there
by address.
It follows #893 (the ssh line and the remote working directory) and
replaces #897 and #901, closed when the design changed: their code is in
here, reworked to the daemon.
After the review's second round, four more things hold: a page read
(
read_file, here and on a machine) reads the file a chunk at a timeand decodes only the page's own lines, so no one step holds the
interpreter for the whole file and the file is never whole in memory;
the whole-file calls on a machine go through one gate (readers
together, a writer alone, a waiting writer first), so a read never
answers with the old page and the new digest; the install reads the
index pip is configured with on the machine the way pip does, names it
in the plan and hands it to
uv pip install; and a machine without uvgets one pinned version as an unmanaged install, which writes no
receipt under the home directory and changes no PATH, while a node
directory that was there keeps its mode.
What a daemon is (
raven/node/):The host's ssh runs a relay there (
python -I -m raven.node --attach --dir <node_dir>,raven/node/relay.py): it bridges the ssh sessionto the daemon's unix socket under the node directory, starts the daemon
when none answers (
--serve, detached, its words inlog/serve.log),and leaves with the ssh. The daemon leaves on its own after ten minutes
with no connection and no running agent; the next relay starts it again,
so nothing is registered as a service and a reboot costs one connect.
It opens no network port; the socket is the registered user's own
(mode 600) in
run/(0700), or, when that path is too long for asocket, in a directory of the user's own under the temp directory
(0700, checked before use: a directory of the user's, never a link,
never another user's, narrowed when wider). The relay forwards only to
a socket of the user's and starts no daemon beside anything else; a
daemon whose socket's place holds a thing it cannot remove says so in
one line and leaves rather than listen beside it. ssh login is the
authentication.
host drives it with
AcpClient(over anEndpointseam this changeadds: a process, or a channel of a link). Protocol 3.
fs/read,list,find,grep,stat,write,edit,pull) run Raven's own file tools there, fenced to therootseachcall names, so a remote read comes back exactly as a local one would.
A write may carry
expect, the digest the file had when it was read,and is refused with
CHANGEDwhen it no longer matches. A page readwith its digest, a stat with one, a chunk and a checked write run in a
thread of the daemon's, writers one at a time under a lock, so the
loop that answers the heartbeat and forwards every agent's frames is
not held for the length of a large file.
agent/launch,frame,stderr,exit,cancel,close):the command named is started in the machine user's login environment
(captured once by the daemon itself, under
bashorzsh-lic),in its own process group, under a task id of the host's. Its ACP
frames travel on a numbered channel of the connection that holds it,
and are kept in the task's record on disk (
<node_dir>/tasks/<task>/:task.json,frames.jsonl,stderr.log; the directories 0700 and thefiles 0600, as the host's own journal is) as they arrive. A connection
that goes leaves the agent running;
agent/attachon a laterconnection replays the record from the frame the host last read and
takes the task over, and a finished task is replayed from its record
alone, even by a daemon that never ran it.
agent/cancelends thewhole process tree: the tree is read off
psbefore the first signal,so a command the agent put in a session of its own (Claude Code's shell
tool does) goes too.
task/listnames the tasks running or remembered;records are kept seven days.
node/shutdownends agents only under--stdio, the one mode that ties them to the connection (tests, and afallback).
probe/machine,probe/agent) andsession/dir, which makesa session directory and says where it is as the machine resolves it.
model, reads a config or opens a port; beside Raven's code the machine
needs uv (or curl or wget, to fetch one with), a Python 3.12 if it has
one (else uv brings one), loguru and pillow (about 55 MB, more with a
fetched uv or Python), ripgrep-bin when its index has it. A test runs
the node with nothing importable beyond the standard library, its own
code and those two packages, and asks every file call, so the next
import added under
raven/agent/toolsis caught here rather than onevery installed node.
The same code at both ends (
raven/node/bundle.py): the code is named bya digest of the package's files, and
node/hellocompares it, so adaemon of other code is refused with the command that updates it. An
install lives in
<node_dir>/<version>-<digest>.The host side (
raven/node/client.py):idle for ten minutes with no agent on it and no call on its way to it
is closed the next time any link is asked for. A heartbeat every 5 s;
one unanswered for 15 s means the link is lost.
holding what the host writes meanwhile (up to 1000 frames); the link is
tried again on a backoff (2, 4, 8, 16 s) for as long as any agent
waits; a link that comes back attaches every task again from the frame
the host last read, sends what waited and any cancel asked in between.
Only a task the machine no longer knows ends an endpoint, in one
sentence. A link closed on purpose just closes: the daemon keeps its
agents, and
tasks()on a later link lists them. The listing says"link lost: ...; N tasks continue on the machine; reconnecting".
never its address: not installed (with the install command),
unreachable, login refused, other code, a refused handshake, a call
that ran out of time, or the daemon's own last word.
The registry's package (
raven/ops/remote_path.py) tells a remote pathfrom a local one (a UNC or extended Windows path, and on a Windows host a
drive-relative one, are this computer's), resolves it against the
registry, and lends the tools the pool through one seam (
use_nodes,nodes) -- the tools must not import the node package, which importsthem.
raven/acp_client/remote.pykeeps the ssh command, the machinelookup, the posix wrapping of a remote command and the one-sentence
reading of an ssh failure from #893, and loses the launch command, the
env preamble and the session-directory ssh that #897 would have used:
the daemon does those. The ssh command is handed to the connection as an
argv, and
AcpClient.launchtakes one as it is, so no host's own grammarreads it again (a Windows host's parser split the line into twelve
words). What reaches a model is a path on the machine written the way
scp writes it, address included, because that is how a file there is
named to the tools; the way in -- the key path, ssh's options, ssh's own
messages -- reaches no config file, log line or model, and an error names
a machine by id and name only. The lane stays
debug-only (
RAVEN_DEBUG_MACHINES=1, ruled on #897): with it off nodaemon is started and no install is planned, each saying why.
Installing (
raven ops connection install-node <id>,raven/node/install.py): one look at the machine (the install, its uv ora curl or wget to fetch one with, a Python 3.12 if it has one, the free
space), then a question, then everything through uv, as the repository's
dependency rule asks: a uv fetched under the node directory from its
installer when the machine has none, a virtualenv uv makes of the
machine's Python 3.12 or of one it brings, the pinned packages with
uv pip, the code as a tarball on ssh's stdin, each step one ssh calland a failed step removing the half-made install; the node directory is
made 0700. A machine with neither uv nor a way to fetch one is refused
with the installer's address.
--dirkeeps installs elsewhere andrecords it on the row;
--pruneremoves other versions. Nothing else onthe machine changes.
raven ops connection doctorreports the registryas before.
Also:
CONTEXT.mddefines the Raven node and seatsraven.nodeinner inthe import contract (
pyproject.toml,lint-imports); the repo layouttable gets its row; the gateway, the TUI and the RPC entrance close the
node pool with the ACP pool on shutdown.
Type
Verification
uv run pytest tests/test_node_bundle.py tests/test_node_server.py tests/test_node_client.py tests/test_node_relay.py tests/test_node_install.py tests/test_acp_client_teardown.py tests/test_acp_remote.py tests/test_ops_remote_path.py tests/test_cli_ops_connection_commands.py tests/test_ops_connection_add.py tests/test_ops_transport.py tests/test_subagent_acp.py tests/test_acp_client*.py tests/test_cli_gateway*.py-- 859 passed on this tree; diff coverage of this change's lines from
these groups alone passes the 90% gate. Covered, beside what feat(*): run a Raven node on a registered machine to answer file calls there #901's
tests covered (parity of every file call with the tool's own answer,
the fence, each malformed field, the handshake, the stdio loop, the
install's scripts and one real install on this computer, through uv):
link and another user's directory refused; wider bits narrowed); a
daemon refused its socket's place says so and leaves; the relay
forwards to nothing that is not the user's and starts no daemon
beside it; a task's records are 0700 and 0600; a ping is answered
while a file's digest is taken; a call on its way keeps its link;
the ssh argv reaches the connection as it is and
AcpClient.launchtakes an argv; the installer fetches uv when the machine has none
(curl or wget) and refuses a machine with neither; a drive-relative
path is this computer's on a Windows host and scp's elsewhere, an
extended or UNC path this computer's anywhere; the node answers
every file call with only its requirements importable.
a second connection attaches from the frame the host had not read and
hears the exit; a finished task is replayed from its record by a
daemon that never ran it; a record left "running" by a daemon that
died reads as lost; an attach for a task nobody knows is refused by
code; the task list and the sweep of old records; shutdown from a host
leaves a daemon's tasks running.
the first, the daemon leaving when idle and leaving at once when
another serves the same directory;
--serveand--attachneed--dir.sshthat hands the line to a real loginshell and starts the real relay and daemon: a request made while the
link is down is answered after the link is back, on the same daemon;
a daemon told to stop takes its agents and the turn says so; a link
closed on purpose leaves the agent for the next link to list, attach
and cancel; a heartbeat lost parks the endpoint and the reconnect
attaches it again; a task the machine no longer knows ends its
endpoint in one sentence; an attach the link cannot carry parks every
waiting endpoint again; held frames are capped; a file fetched whole
and checked, and refused outside the roots.
bridges bytes both ways, reaches the same daemon again without
starting another, and gives up in one sentence when none comes up.
is read off
ps; the daemon's own group is never signalled.uv run pytest --collect-only-- no import error;lint-imports--10 kept, 0 broken. The daemon's two test modules skip on win32 before
importing it (fcntl, unix sockets), and the two POSIX teardown tests
lend a SIGKILL where the platform has none.
uv run pyteston the final tree of the three changes, which includesthis one -- 29217 passed, 121 skipped, 3 failed:
test_rpc_files.py::test_a_host_without_libreoffice_says_so,test_simulation_scenario.py::test_a_fresh_trial_refreshes_the_subagent_homes_before_it_runsand
test_simulation_suite.py::test_a_stopped_run_is_marked_in_its_record_and_finished_with_its_evidence,which fail the same way on main on this machine. The groups above
also pass under
--idle-ceiling-strict, CI's idle ceiling.Real machines (the final tree of the three changes, two registered
Linux machines, a scratch registry and
RAVEN_HOME):with the machine's curl under the node directory, then a virtualenv
of the machine's own Python 3.12, 16 s in all; on the machine whose
home disk was full, under
/tmp, with a Python uv brought, 11 s.Both nodes answered the handshake with this code's digest.
session/dir11 to 18 ms;agent/launch13 ms; a request's round trip 12 ms.killed mid-turn, the link read as lost in 5 s, the turn finished 27 s
after the kill with the agent's reply, 25 of 25 lines written, the
same daemon on both sides of the gap; a cancel left 0 processes 3 s
later; three sessions at once answered in 8 s; with every link closed
the daemon stayed with its records and no agent process remained.
The second round's four notes (
tests/test_filesystem_tools.py tests/test_node_install.py tests/test_node_server.py tests/test_node_client.py tests/test_read_file_image.py-- 337passed, 100% diff coverage of that commit's lines): a page is the
lines
splitlinesgives whatever the chunk edge, over every break itknows; a 120 MiB file pages with every ping answered within 0.25 s; a
read with a slowed digest beside a write of the same file answers with
the old page and the old digest and the write lands after; a waiting
writer goes before new readers; the probe reads
pip.confandPIP_INDEX_URLas pip does under a real shell, the plan names theindex and the install hands it to uv; the uv step fetches one pinned
version unmanaged; a node directory that was there keeps its mode and
one the install makes is owner-only. On a registered Linux machine
with the node reinstalled: a page read of a 1 GiB file there beside
pings every 50 ms kept the longest wait of a ping at 0.34 s; the reads
sent during a write all answered with a page and a digest of one
version; the probe found the mirror that machine's pip is configured
with; an install into a fresh node directory, forced down the fetched
path, put uv 0.13.0 under it unmanaged in 24 s, with the receipt under
the home directory untouched and the directory at 700.
The full review's five findings (
tests/test_node_client.py tests/test_node_server.py tests/test_node_relay.py-- 223 passed, theseven new tests failing on the code before the fix, diff coverage of
that commit's lines past the gate): a link the heartbeat has not yet
seen die, replaced by an ordinary call, brings its parked agents along
(
agent/attachon the new link, nothing left parked); a replay the newlink cannot carry keeps every frame it did not take, parks the endpoint
being replayed and every one not yet reached, and the link is tried
again; a cancel asked on a dead link, or failing on the new one, stays
wanted until a link delivers it, and a refusal clears it; two installs
on one long node directory get sockets apart; a launch still capturing
the login environment is counted, so a second one at the limit is
refused before the first spawns. On a registered Linux machine, one
script run before and after the fix (the local relay killed with the
heartbeat kept out of the way, then a plain call; a cancel asked in
that gap; eleven launches at once against the limit of eight): before,
the agent stayed parked and never answered, the cancelled task ran on
with its record saying running, nine launches started and ten tasks ran
there; after, the call took 0.3 s and the agent answered through the
new link, the task ended within the reattach with its record saying
so, eight started and three were refused with eight running, and
nothing was left running on the machine. The other half of the first
finding, a relaunch that fails under the call with agents parked: on the
machine, the relay killed and the relaunch made to fail once, before the
agent was still parked 30 s after the machine was reachable again and no
reconnect was under way; after, the reconnect the heartbeat loss
schedules runs here too, the agent was attached again on the first retry
after the machine answered and answered through the restored link.
scripts/check_source_language.py,scripts/check_commit_messages.pyand
scripts/check_large_files.pyon the branch range,commitlintonthe commit and the prospective squash header,
ruff-- all clean.Relevant tests pass locally
Relevant lint / type checks pass locally
User-facing docs or screenshots are updated when needed
The command describes itself in
--help; user docs follow with thechange that runs sub-agents on a machine.
Risk
Opt-in and inert: nothing starts a daemon unless the debug lane is on and
a caller asks for a machine, and an install runs only when the owner asks,
after saying what it will do. On a machine it adds one directory under
~/.raven-node(or the row'snode_dir): the install, arun/with thesocket, lock and pid of this install, a
tasks/with the records, alog/; removing the directory uninstalls it. The daemon runs as theregistered user, listens on a socket in a directory only that user can
open (and the relay forwards to a socket of that user's only), reads and
writes only inside the roots each call names, and leaves when idle. A
task's record holds the agent's ACP frames, which may quote what the
agent read: it is the user's alone (0700, 0600) and is removed after
seven days. The way in stays in the ssh argv, in memory; no error names
the address, and a model sees it only as part of a path on the machine,
written the way scp writes it. Rollback is a plain revert; an installed
daemon is then an inert directory that leaves once idle.
Related Issues
Follows #893; replaces #897 and #901.