Skip to content

fix(cli): stop no longer kills the run it is executing inside - #414

Merged
Jason Robert (jrob5756) merged 4 commits into
mainfrom
fix/399-stop-kills-invoker
Aug 11, 2026
Merged

fix(cli): stop no longer kills the run it is executing inside#414
Jason Robert (jrob5756) merged 4 commits into
mainfrom
fix/399-stop-kills-invoker

Conversation

@jrob5756

Copy link
Copy Markdown
Collaborator

Closes#399

Problem

An agent smoke-testing conductor stop from its own workflow's bash
tool inherited that workflow's background environment and pointed
stop at the very run driving it — the process printed "Stopped" and
was killed by what it printed.

Fix

  • Added conductor.cli.self_run, which identifies the run stop is
    executing inside via three signals, tried in order:
    CONDUCTOR_RUN_ID (set on every --web-bg child and inherited by
    descendants), the legacy CONDUCTOR_WEB_BG/CONDUCTOR_WEB_PORT pair
    (for PID files predating the run_id field), and POSIX process
    ancestry (/proc/<pid>/statusPPid: walk + os.getsid(0)).
  • conductor stop now excludes that run from targeting by default:
    • --all means "stop all other runs"
    • the no-flag auto-stop skips it
    • --port <own port> is refused (exit 1), naming the fix
    • a self-only stop/stop --all prints a refusal and exits 0
    • --allow-self restores the previous behavior exactly, printing a
      warning whenever it actually causes the caller's own run to be
      signalled
  • Process-ancestry detection is POSIX-only; Windows relies on the
    env-var signals alone (documented caveat).
  • Updated docs/cli-reference.md (Self-Exclusion + Exit Codes
    sections), CHANGELOG.md, and AGENTS.md to match.

Testing

  • uv run pytest tests/test_cli/test_stop.py tests/test_cli/test_self_run.py tests/test_cli/test_markup_injection.py — 76 passed.
  • New tests/test_cli/test_self_run.py covers own_run_pids()
    identity signals, the /proc ancestry walk (cycle/cap handling),
    partition_own_run classification, and describe_own_run
    formatting.
  • tests/test_cli/test_stop.py gains a TestStopSelfExclusion class
    covering --all, no-flag, --port, and --allow-self behavior;
    existing tests stub own_run_pids to keep prior targeting tests
    deterministic.

Jason Robertand others added 2 commits August 11, 2026 15:46
An agent smoke-testing `conductor stop` from its own workflow's `bash`
tool inherited that workflow's background environment and terminated
itself -- the process printed "Stopped" and was killed by what it
printed.
Add `conductor.cli.self_run`, which identifies the run `stop` is
executing inside via three signals, tried in order: `CONDUCTOR_RUN_ID`
(set on every `--web-bg` child and inherited by descendants), the
legacy `CONDUCTOR_WEB_BG`/`CONDUCTOR_WEB_PORT` pair (for PID files
predating the `run_id` field), and POSIX process ancestry. That run is
now excluded from targeting by default:
- `--all` now means "stop all *other* runs"
- the no-flag auto-stop skips it
- `--port <own port>` is refused (exit 1), naming the fix
- a self-only `stop`/`stop --all` prints a refusal and exits 0
- `--allow-self` restores the previous behavior exactly, printing a
warning whenever it actually causes the caller's own run to be
signalled
Process-ancestry detection is POSIX-only; Windows relies on the
env-var signals alone.
…t gaps
Code review of PR #414 found several genuine issues in the self-exclusion
logic itself and in the test suite's ability to catch a regression of it.
Crash fixes in self_run.py (all verified with real reproductions):
- partition_own_run crashed on a non-string run_id (e.g. int) in a PID
file, taking down `stop` for every run, not just the malformed one.
Now logs a warning and treats it as absent, matching
pid.py::scan_pid_files' "skip, don't raise" discipline.
- _read_ppid's `except OSError` didn't catch UnicodeDecodeError, which a
non-UTF-8 /proc/<pid>/status Name: line (settable by any unprivileged
process via prctl(PR_SET_NAME)) can raise. This aborted the ancestry
walk before it ever reached the os.getsid(0) fallback, defeating the
safety net for any of up to 64 ancestor processes. Now decodes with
errors="replace" since only the PPid: line is ever parsed.
- describe_own_run crashed on `"workflow": null` via Path(None) --
precisely the message meant to reassure a user that their own run is
protected. Now falls back to "unknown" the same way a missing key does.
Test-suite hardening:
- The one test combining a self-run and an other-run under --all
asserted only generic substrings/call counts, and could not detect the
own/others classification being fully inverted (verified by swapping
them and rerunning -- the test still passed). Now uses distinct PIDs
and asserts on which PID was actually signalled.
- Added coverage for the no-flag path with a mixed self+other
population (auto-stop-the-sole-other and list-remaining-others
branches), previously fully unexercised -- arguably the most common
real trigger for issue #399.
- Added a unit test for partition_own_run with genuinely mixed input,
and regression tests for each of the three crash fixes above.
- OwnRunPartition now validates its own/others disjointness in
__post_init__ (with slots=True), turning a future swap bug into an
immediate, loud construction-time failure instead of a silent
behavioral inversion.
Doc/comment fixes:
- Removed the dead `seen`-set cycle guard in own_run_pids (the
pre-existing `parent in pids` check already terminates any cycle) and
corrected the docstring that misattributed cycle termination to it.
- Removed the redundant _clear_default_stub test fixture that
duplicated the module-level no_self_run fixture without actually
overriding it (different fixture names don't override in pytest).
- Fixed a rule-F misattribution in describe_own_run's docstring, an
ambiguous "naming the fix" phrase in CHANGELOG.md, a muddled causal
explanation and a self-contradictory "restores ... exactly" sentence
in docs/cli-reference.md, and matched stop()'s docstring backtick
style to its sibling commands.
- Documented the case-insensitive run_id comparison in the module
docstring and docs/cli-reference.md.
Verification: full test suite (5885 passed, 47 skipped, 1 pre-existing
unrelated flake in test_event_log.py confirmed present on the
unmodified branch), ruff, and ty all pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jrob5756
Jason Robert (jrob5756) marked this pull request as ready for review August 11, 2026 20:29
Jason Robertand others added 2 commits August 11, 2026 16:48
Reconciles PR #414's self-exclusion feature (--allow-self, issue #399)
with PR #383's escalating stop ladder (--force/--json, issue #344),
both of which modified conductor stop's targeting logic on overlapping
lines.
- src/conductor/cli/app.py: stop() now partitions running PID-file
entries into self/others (via self_run.partition_own_run) before
applying the ladder-based _stop_process/--force/--json machinery, so
self-exclusion and confirmed termination compose instead of one
clobbering the other.
- tests/test_cli/test_stop.py: rewrote TestStopSelfExclusion to drive
the real ladder via _stops_cleanly() plus a wrapping spy on
_stop_process (rather than asserting on direct os.kill calls, which
no longer reflects how stop() signals a process), and updated the
ambiguous-listing self-exclusion test to expect exit code 1, matching
#383's fix for that case.
- tests/test_cli/test_stop_ladder.py: added the same no_self_run
autouse fixture used in test_stop.py -- these pre-existing ladder
tests use small arbitrary PIDs (1, 2, 4242) that can collide with
real process ancestry in shallow-PID-namespace environments, which
self-exclusion would otherwise misclassify as the caller's own run.
- CHANGELOG.md, docs/cli-reference.md: concatenated both PRs' entries;
merged the duplicated 'Exit Codes' section in the stop docs into one
table covering both self-exclusion and ladder outcomes.
Verified: ruff check, ruff format --check, ty check src, and the full
test suite (targeted stop/self_run/pid/markup suites plus
pytest -m "not install_scripts") all pass. The one failure seen
(test_event_log.py::test_filenames_unique_for_simultaneous_starts) is
a pre-existing, environment-specific flake reproduced identically on a
clean origin/main checkout, unrelated to this merge.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
main advanced again after the first merge, adding PR #413 (fold the
conductor status/stop dashboard-URL column instead of cropping it,
render Started at minute precision). Only tests/test_cli/test_stop.py
conflicted, on the same TestStopAutoDetect class our self-exclusion
merge touched.
- Added the os/re imports #413 needs alongside our existing imports.
- Kept #413's new test_lists_started_at_minute_precision case.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jrob5756
Jason Robert (jrob5756) merged commit 190dcbe into mainAug 11, 2026
9 checks passed
Jason Robert (jrob5756) pushed a commit that referenced this pull request Aug 11, 2026
Resolves a CHANGELOG.md ordering conflict with #414/#413/#388, which
merged into main while this PR's review was in progress. No other
files conflicted; workflow.py's auto-merge is clean (this branch's
_context_window_anomaly_warned latch and main's new
_pricing_hook_silent_warned latch are independent additions to the
same __init__).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for freeto 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.

conductor stop kills the run that invoked it — an agent smoke-testing 'conductor stop' terminated its own workflow

1 participant

@jrob5756