Repository navigation
fix(*): stop cli sub-agents on windows and hand new ones a rotated web key - #927
Conversation
A cli sub-agent's timeout and cancel paths, and the Kimi Code ask that a failed connect makes, called os.killpg, which Windows does not have. The AttributeError replaced the timeout error or the cancellation, and the process was left running. Where killpg is missing, the tree is now ended from the launcher's pid with taskkill /T /F while the launcher is alive, and the launcher is then killed itself. Once the launcher has exited its pid may name another process, so nothing is killed by number. POSIX keeps the group kill, and a new test pins it for the Kimi Code ask, where removing it had left every test green.
settings.set rewrote ~/.raven/env when a web credential changed, but a POSIX cli or acp sub-agent reads that file only through the login-shell capture, which a running raven takes once and keeps. Every later spawn started with the key the write had replaced, until a Connect, a Test, "Check again" or a restart took the capture again. A mirrored key's write now takes the capture again before the reply. Captures also run one at a time: a refresh still reading the shell from before the write, a Connect's for instance, could otherwise land after the write's own and put the replaced key back, and readers that arrive while the first capture runs now wait for it rather than each running the shell.
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
I reviewed the effective PR delta from base 182dacac through this head, and also read git diff github/main...HEAD to account for the locally advanced target ref. I covered the Windows timeout/cancel callers, the existing taskkill precedent and PID-reuse boundary, login-environment capture and concurrent refresh ordering, settings mirror callers, relevant history, backward compatibility and test strength. I also checked AGENTS.md/CLAUDE.md, the applicable CONTEXT-MAP.md/CONTEXT.md terminology and layer constraints; I found no rules or architecture violations.
Verification: uv run --python 3.12 --extra dev pytest tests/test_subagent_third_party.py tests/test_subagent_kimi_code.py tests/test_rpc_console.py tests/test_rpc_settings.py -q passed (615 tests); Ruff check and format check passed on all nine changed files; git diff --check passed. The first default test environment could not collect because raven_everos was absent, and --all-extras could not build datrie because this runner has no compiler; the narrower documented dev extra supplied the test dependencies. At posting time, all completed GitHub checks pass, with two unit shards still pending.
|
Windows compatibility review: the production cleanup passed the exercised native scenarios; one new test portability defect was reproduced. Scope: the complete 9-file diff at head P2 - Make the Windows test helper callable on Windows. tests/_windows_kill.py:26 calls Verification: native Windows / Python 3.13.12, isolated user/Raven/temp directories. Git HTTPS fetching failed, so I reconstructed the fixed-head snapshot from the exact base plus GitHub blobs. Its complete Git tree equals The reused uv environment has matching uv run pytest tests/test_subagent_third_party.py::test_cli_backend_cancel_without_killpg_ends_the_tree_through_taskkill -n 0 -x --tb=short --basetemp "$R/checks/cli-test/pytest" --junitxml "$R/checks/cli-test/junit.xml"
# 1 failed at the new helper's line 26.
uv run pytest tests/test_subagent_kimi_code.py::test_a_connect_that_goes_away_mid_ask_ends_its_kimi_through_taskkill_without_killpg -n 0 -x --tb=short --basetemp "$R/checks/kimi-test/pytest" --junitxml "$R/checks/kimi-test/junit.xml"
# 1 failed at the same line.
uv run pytest tests/test_subagent_third_party.py -k 'login_shell_env or refresh_that_read or readers_that_find_no_capture' -n 0 -x --tb=short --basetemp "$R/checks/capture/pytest" --junitxml "$R/checks/capture/junit.xml"
# 15 passed.
uv run pytest tests/test_rpc_console.py tests/test_rpc_settings.py::test_secret_key_writes_string -k 'mirror or mirrored or capture or rotated_key or secret_key_writes_string' -n 0 -x --tb=short --basetemp "$R/checks/settings/pytest" --junitxml "$R/checks/settings/junit.xml"
# 26 passed, 1 skipped (real POSIX shell-mirror case).
uv run pytest tests/test_subagent_host_env.py -k 'capture or registry or refresh' -n 0 -x --tb=short --basetemp "$R/checks/host-env/pytest" --junitxml "$R/checks/host-env/junit.xml"
# 5 passed, 1 failed: extensionless executable fixture reported missing.
uv run pytest C:/Users/livxu/AppData/Local/Temp/raven-windows-review/3a29a2480c9547a4a357f8f9b019ac13/test_native_cleanup.py -s -n 0 -x --tb=short --basetemp "$R/checks/native/pytest" --junitxml "$R/checks/native/junit.xml"
# 5 passed: real taskkill, both launcher and worker exited in each case.
# Exact-base snapshot, with PYTHONPATH repinned to $R/base:
uv run pytest C:/Users/livxu/AppData/Local/Temp/raven-windows-review/3a29a2480c9547a4a357f8f9b019ac13/test_native_cleanup.py -k cli-timeout -s -n 0 -x --tb=short --basetemp "$R/checks/base-native-base/pytest" --junitxml "$R/checks/base-native-base/junit.xml"
# 1 failed as expected: the production timeout path calls missing os.killpg.
uv run pytest tests/test_subagent_host_env.py::test_windows_probe_finds_agents_on_the_captured_path -n 0 -x --tb=short --basetemp "$R/checks/host-control-base/pytest" --junitxml "$R/checks/host-control-base/junit.xml"
# Same executable-fixture assertion failed on the exact base; inherited.The scratch native probe covers CLI timeout, cancellation and output-sink failure, plus Kimi-helper timeout and cancellation. It uses owned Python launcher/worker processes under a path containing spaces and non-ASCII characters, real Windows process handles, and unmocked No full repository suite, real vendor authentication or POSIX login-shell integration ran here. The inherited executable-fixture failure and documented already-exited-launcher limitation are not treated as new findings. Windows credential delivery remains unchanged as described by the PR. |
|
Blocking: the new Windows cleanup tests must run on native Windows instead of failing in their shared helper. This report holds. I reproduced the exact The two focused cleanup cases still pass on Linux ( |
…iters CI's diff coverage gate measured 52 of the 59 changed lines, under its 90% threshold. Two of the uncovered paths are behaviour worth pinning: a Windows stop whose taskkill times out still kills the launcher and keeps the timeout error, and readers waiting on a first capture that fails fall back to raven's environment without running the shell again. The four lines left are except clauses for a process group or a process that is already gone.
The shared helper removed os.killpg with monkeypatch.delattr, which raises where there is no killpg to remove, so on Windows all six Windows stop tests failed in the helper, and their fixtures were sh scripts and sleep. On Windows the helper now replaces nothing: the real taskkill runs and each call is recorded. Elsewhere it removes killpg and stands in for taskkill by ending the process group it is asked about, which is the launcher's tree here, so both hosts assert the same calls and deaths. The fixtures are Python scripts started on sys._base_executable, since a Windows venv's python.exe is a redirector that runs the base interpreter as its child and so returns a pid the script never sees. Liveness is read with pid_alive; os.kill(pid, 0) is no probe there.
|
Fixed in 95fbf84: the Windows stop tests now run on a Windows host instead of failing in their shared helper, and they still assert real cleanup. Reproduced first at 4bf194d, with What changed, in tests only:
This deviates from Also in this push: 2e7cffe adds the two tests the diff coverage gate asked for, a Verification at 95fbf84, Linux, Python 3.12:
Not run: a Windows host. A re-run of the native probe on this head would close that. Found on the way, and stated in the description rather than changed: when Untouched: the |
gloryfromca
left a comment
There was a problem hiding this comment.
No blockers; this can merge as far as I am concerned.
The prior Windows-test blocker is fixed. I reviewed the complete 4bf194d8..95fbf84f delta, including both new test commits, the shared helper, all six Windows-branch callers, PID identity and liveness handling, timeout fallback, the already-exited-launcher case, concurrent capture failure, relevant repository rules, and whether the assertions were weakened. The fixtures are now native Python processes, preserve real launcher/worker death assertions, and run real taskkill when os.killpg is absent; the POSIX stand-in exercises the same production branch without skipping it. I found no new defect.
Exact-head verification: the eight changed-path tests passed; the two affected suites passed (348 passed); Ruff check, Ruff format check, and git diff --check passed. The immutable PR diff is base 182dacac001e to head 95fbf84fe325 with SHA-256 fa76fced03ebd70d7d2ba772f97988cc6707de62bad2c763c306eb3314de005d. Risk assessment recommends merge with human review: moderate impact if wrong, low regression likelihood, partial regression protection because this corrected head has not yet run its unit tests on native Windows, easy recovery, and moderate confidence.
|
Windows compatibility re-review: the reported native test-helper failure is fixed. No additional Windows compatibility issue was found in the inspected scope. Scope: the complete nine-file PR at All six committed Windows cleanup cases now pass on native Windows. The helper uses real Native Windows / Python 3.13.12. Fresh run-owned environment from $R = 'C:\Users\livxu\AppData\Local\Temp\raven-windows-review\18f225937a1b4f799a5082d83d56d06c'
uv run pytest tests/test_subagent_third_party.py tests/test_subagent_kimi_code.py -k without_killpg -n 0 -x --tb=short --basetemp "$R\checks\927-cleanup\pytest" --junitxml "$R\checks\927-cleanup\junit.xml"
# 6 passed, 342 deselected.
uv run pytest tests/test_subagent_third_party.py -k 'login_shell_env or refresh_that_read or readers_that_find_no_capture or readers_that_wait_on_a_capture' -n 0 -x --tb=short --basetemp "$R\checks\927-capture\pytest" --junitxml "$R\checks\927-capture\junit.xml"
# 16 passed, 273 deselected.
uv run pytest tests/test_rpc_console.py tests/test_rpc_settings.py::test_secret_key_writes_string -k 'mirror or mirrored or capture or rotated_key or secret_key_writes_string' -n 0 -x --tb=short --basetemp "$R\checks\927-settings\pytest" --junitxml "$R\checks\927-settings\junit.xml"
# 26 passed, 1 skipped (real POSIX shell-mirror case), 136 deselected.
uv run pytest "$R\test_native_cleanup.py" -s -n 0 -x --tb=short --basetemp "$R\checks\927-native\pytest" --junitxml "$R\checks\927-native\junit.xml"
# 5 passed.The three repository-test selections each emitted one idle-ceiling warning during these concurrent runs; strict idle enforcement was not enabled. No full suite, actual vendor authentication/CLI, or real POSIX login-shell integration ran. The documented limitations for an already-exited launcher, or unavailable/failed |
|
Not a blocker. Two observations from the full acceptance; neither blocks A (production is sound and your CI is green on 22 checks). I verified both on the merged tree. Severity of this one finding, not a verdict on the pull request. Any blocking findings from this pass are review threads on the changed files; GitHub renders those collapsed, as a file name with no text.
|
|
No blockers; suggestions only, and they are marked inline. The flaky-test observation holds and changes my stance from clean to nonblocking. The focused case passes on this Linux host ( There are no review threads from my account to resolve. These two items remain named follow-ups, with the timing-dependent test the concrete one worth fixing. |
|
Confirmed, and thanks for the reproduction. #927 merged before the fix was pushed, so it is in #943. It is a race, so the fix removes it rather than widening the window: the cap runs from the spawn and nothing tells the ask when the stand-in is ready, so a larger number only moves the threshold. Forced, it is deterministic: with the stand-in made to wait 0.5s before starting its worker, main's test fails with the same The new test waits for |
|
Agreed on the direction. With #927 and #908 both merged, main now has the rule five times: synchronous in |
…943) ## Summary A follow-up to #927, which merged before this was pushed. Its test `test_an_ask_that_runs_out_takes_what_its_kimi_started` cut the Kimi Code ask's timeout to 0.3s and then read the `worker.pid` its stand-in `kimi` writes. The timeout runs from the spawn, so wherever the stand-in starts slower than that, the ask's `finally` ended the process group before the stand-in had noted its worker, and reading `worker.pid` raised `FileNotFoundError`. The review on #927 reproduced it deterministically on macOS; on Linux the stand-in's start crossed 0.3s here only at low priority beside busy loops. The test now cancels the ask once the stand-in is up rather than waiting on the clock: it waits for `ask.pid`, which the stand-in writes only after noting its worker, checks the worker is alive, cancels, and asserts the worker went with the group. Cancel and timeout end the ask through the same `finally`, and the timeout path keeps its own tests in the file, the 1.0s POSIX one and the Windows-branch one. Test-only: no production file changes. ## Type - [ ] Fix - [ ] Feature - [ ] Docs - [ ] CI / tooling - [ ] Refactor - [x] Other ## Verification Linux, Python 3.12, on main d106a5c: - `uv run --frozen --python 3.12 --all-extras pytest tests/test_subagent_kimi_code.py tests/test_subagent_third_party.py -q`: 348 passed. - The race, forced: with the stand-in made to wait 0.5s before starting its worker (a scratch edit to `_through_kimi`, not part of this change), main's test fails with `FileNotFoundError` on `worker.pid`, the failure reported on #927, and the new test passes. - Against the fix: making the POSIX branch of `kimi_code._end_tree` kill the leader alone (`proc.kill()` in place of `os.killpg`) turns the new test red, and only it (1 failed, 347 passed over the two files). - `ruff check` and `ruff format --check` on the changed file: clean. `make lint-types` leaves `tests/` out, so no type check covers this change. - [x] Relevant tests pass locally - [x] Relevant lint / type checks pass locally - [ ] User-facing docs or screenshots are updated when needed ## Risk Test-only. One test is rewritten under a new name; no production file changes, so reverting the commit is the whole rollback. - [x] Security impact considered - [x] Backward compatibility considered - [x] Rollback path is clear for risky changes ## Related Issues #927
Summary
Two gaps in how Raven stops its cli sub-agents and what environment it starts them with.
Stopping a cli sub-agent on Windows. A cli sub-agent's timeout, cancel and stream-failure paths, and the
kimicommands Raven runs to explain a failed Kimi Code connect, ended the process group withos.killpg, which Windows does not have. TheAttributeErrorreplaced the timeout error or the cancellation, and the CLI was left running. Wherekillpgis missing, the tree is now ended from the launcher's pid withtaskkill /T /Fwhile the launcher is still alive, and the launcher is then killed itself, the wayraven/ops/transport.pyalready ends a command on Windows. Once the launcher has exited its pid may name another process, so nothing is killed by number. POSIX keeps the group kill.A web key rotated through
settings.set.settings.setrewrites~/.raven/envwhen a web credential changes, but a POSIX cli or acp sub-agent reads that file only through the login-shell capture, which a running Raven takes once and keeps. Every later spawn started with the key the write had replaced, until a Connect, a Test, "Check again" or a restart took the capture again. A mirrored key's write now takes the capture again before the reply. Captures also run one at a time: a refresh that read the shell before the write, a Connect's for instance, could otherwise land after the write's own and put the replaced key back, and readers that arrive while the first capture runs now wait for it rather than each running the shell.Windows is unchanged for keys. Nothing sources
~/.raven/envthere, and a Windows sub-agent's environment is Raven's own with the registry's PATH, so a key held only in config does not reach it; #912 states this in the code. On POSIX the keys reach a sub-agent only through asourceline in the user's shell startup files, which the onboard wizard adds only with the user's agreement, and a Windows delivery path would need a consent step of its own, so this PR does not add one.Not changed here: an acp sub-agent that is already connected keeps the environment it was launched with until its next launch; a key written by
raven onboard, which runs as its own process, reaches a running Raven's sub-agents at the next Connect, Test, "Check again" or restart; and on Windows the wizard still prints the POSIXsourceline when$SHELLis not bash or zsh.Type
Verification
Linux, Python 3.12, rebased onto main 182daca:
uv run --frozen --python 3.12 --all-extras pytest -qwith the proxy variables unset: 3 failed, 29667 passed, 158 skipped. The same 3 fail on main on this machine (an npm on PATH, a root-only permission check, a LibreOffice conversion); 0 introduced.uv run --frozen --python 3.12 --all-extras pytest tests/test_subagent_third_party.py tests/test_subagent_kimi_code.py tests/test_subagent_acp.py tests/test_subagent_host_env.py tests/test_rpc_console.py tests/test_rpc_settings.py tests/test_rpc_subagents.py -q: 1080 passed.Windows stop:
tests/_windows_kill.pyleaves a Windows host as it is, where the realtaskkillruns, and elsewhere removesos.killpgand stands in fortaskkillby ending the process group it is given, the launcher's tree here; either way it records each call. The launchers, their workers and the Kimi stand-in are Python started onsys._base_executable, because a Windows venv'spython.exeis a redirector that runs the base interpreter as its child and returns a pid the script never sees. The tests have not run on a Windows host, and no CI job runs these test files there.Windows stop, measured here: the six tests that use the helper pass. With
os.killpgremoved for the whole session and ataskkillon PATH that ends the group it is given, they pass again through thattaskkill(3 calls), and the POSIX-only Kimi test beside them is skipped. Withsys.executablereplaced by a wrapper that runs Python as its child, as a venv'spython.exedoes on Windows, both modes stay green; started on that wrapper instead, the two cancel tests fail on the pid they compare.Windows stop, against the fix: the six fail with
AttributeErroron main. Removing the alive check in the cli backend, thetaskkillcall or the kill after it in either module, or the cli backend's handling of ataskkillthat times out, turns at least one of them red. Removing the POSIX group kill from the Kimi Code commands turns only the new test whose stand-inkimistarts a process of its own red (1 failed, 347 passed over the two test files).Rotated key:
test_a_rotated_key_reaches_the_capture_the_next_spawn_readssaves a key twice throughsettings.setand reads the capture after each save, through a real bash that sources the mirror; on main the second read still returns the first key. Removing the refresh turns 12 tests red; refreshing before the mirror is rewritten turns that test red. Two existing tests that write a web key now stub the refresh, so they do not run the host's login shell.Concurrency: three tests force the interleavings with events: a refresh that read the shell first and lands after a later one, a second reader that arrives while the first capture runs, and readers waiting on a first capture that fails. Each failed in 3 of 3 runs on main and passes with the lock; removing the lock from either capture path, or either re-check inside the first one, turns one of them red.
With
--cov=raven --cov=raven_everos --cov-branchadded to the full run,python scripts/coverage_gate.py diff --base-ref origin/main --threshold 90: 93.22% of the changed lines (55 of 59). The four left are except clauses for a process group or a process that is already gone.ruff check,ruff format --check,lint-imports,ty checkanddeptryover the Makefile targets,commitlint, the three repository gate scripts andpre-commit run --from-ref origin/main --to-ref HEAD: passed.git merge-fileofraven/rpc/methods/console.pywith each open PR that touches it (feat(plugins): add the everos-cloud-memory plugin and its host wiring #883, fix(ui-web): attach a picture dragged into the composer by its own path #900, fix: apply the configured timezone when creating cron jobs #911, fix: bound Windows environment expansion and preserve literal percent signs #912, fix: preserve active config aliases when updating settings #923, fix(rpc): treat a null section as absent when settings.set writes a key #925), and ofraven/agent/subagent/backends/env.pywith fix: bound Windows environment expansion and preserve literal percent signs #912 in both orders: no conflicts.Relevant tests pass locally
Relevant lint / type checks pass locally
User-facing docs or screenshots are updated when needed
No user-facing docs describe either behavior.
Risk
Saving a web key now waits for the user's login shell to start before the reply, as Connect and Test already do; the capture's 15 s timeout bounds it, and it can also wait for a capture already in flight. A capture that fails keeps the previous one and does not fail the save. No credential reaches a sub-agent that did not before; it is the same shell reading the same file, read again. On Windows a stop now waits up to 5 s for
taskkill, and a worker that a launcher started and left behind after exiting outlives the stop there, since Windows has no group to address and an exited launcher's pid is not trusted. Iftaskkillis missing, fails or does not answer within those 5 s, only the launcher is killed, and a cli stop then waits for any worker still holding the launcher's output pipes to exit. Revert this PR to roll back.Related Issues
#908, #912