Skip to content

docs(skills): refactor-protocol — a session owns the processes it backgrounds - #345

Merged
JArmandoAnaya merged 1 commit into
mainfrom
docs/background-process-cleanup
Aug 5, 2026
Merged

docs(skills): refactor-protocol — a session owns the processes it backgrounds#345
JArmandoAnaya merged 1 commit into
mainfrom
docs/background-process-cleanup

Conversation

@JArmandoAnaya

Copy link
Copy Markdown
Contributor

One rule in refactor-protocol, and the machine cleanup that produced it. No source changes.

What changed

## Cleanup gains a ### Background processes you spawned subsection. The section was
about the worktree only; a worktree is not the only thing a session leaves behind.

  • Collect the PID at spawn with $! — never reconstruct the list later with jobs -p.
  • Clean up in a trap … EXIT, so the path that skips cleanup does not exist.
  • Cleanup must be able to report its own failure — never 2>/dev/null on it, and verify
    the PIDs are actually gone rather than trusting that kill ran.
  • Kill by explicit PID, not kill -- -<PGID> (the leader is dead, so the PGID is a free
    number) and not pkill -f on the loop body (matches innocent shells).
  • Hunt orphans by PPID == 1, not by grepping for the command you remember writing.

Why — #332 is the worked example

LOADPIDS=$(jobs -p) is always empty. Command substitution forks, and the forked shell
inherits no job table. Reproduced directly:

$ zsh -c 'for j in 1 2 3; do (sleep 120) & done; LOADPIDS=$(jobs -p); \
          echo "LOADPIDS=[$LOADPIDS]"; jobs -p; kill $LOADPIDS'
LOADPIDS=[]
[2] 37481 running   [3] 37482 running   [4] 37483 running     <-- direct jobs -p sees them
zsh:kill:1: not enough arguments

kill got nothing, 2>/dev/null ate the error, and ; (not &&) let the script exit zero
reporting success.

#332 closed cleanly — PR #338 merged, worktree removed, git metadata pruned — and left
twenty-four spin loops reparented to PID 1. Two batches of twelve, from two different
worktrees (visionset-brace-expansion at 22:09, visionset-332-race-gate at 22:19), because
the same technique was reused and the same line failed the same way both times. Between them
they burned ~24 CPU-hours, held 24 on a load average of 45–95, and were a direct cause of
#339 and #340 both having to invoke the declared-fallback rule instead of completing
scripts/check.sh. Nothing inside #332 could see any of it.

All twenty-four were verified individually (PPID 1, spin-loop command line, matching start
time, cwd a deleted worktree, no children, group leader dead) and killed by explicit PID with
plain SIGTERM. No escalation to SIGKILL was needed. Load average before the first kill:
57.43 57.81 56.30. #332 is closed and is not reopened by this.

Corrections to my own earlier reporting

  • I first reported twelve orphans and wrote that the earlier batch "probably died to the
    harness timeout". Wrong — all twenty-four were alive the whole time; the first batch
    surfaced only when a later ps sorted by CPU put it in view. I asserted a negative from a
    filtered ps without running the positive check. The PPID == 1 bullet above is that
    lesson, and it is why the rule says to hunt by parentage rather than by command text.
  • The PR body's numbers are the corrected ones throughout.

Coordination note — resolved, not pending

The brief warned that visionset-rename-sweep might hold uncommitted edits to this same file.
It does not: that worktree is gone and its edits merged as PR #341 (72e52d7), which is
the origin/main this branch is cut from. Its three bullets are in ## Scope discipline
above my subsection and are untouched here.

One other worktree is active — visionset-pytest-q-trap — but git status there (read-only)
shows it modifying .agents/skills/backend/python-setup/SKILL.md, a different file. No
collision. cf. #333.

Found, not fixed

Test plan

Docs only — one Markdown file, no executable change. bash scripts/check.sh ran in full,
all three suites, on a machine no longer saturated (load 57 → 16 after part 1). No fallback
was invoked.

    501s  python tests                        pass
      1s  ruff (lint)                         pass
      0s  ruff (format)                       pass
     20s  mypy                                pass
      1s  import contracts                    pass
     21s  frontend build                      pass
     53s  frontend tests                      pass
     29s  frontend lint                       pass
      4s  openapi drift                       pass
      2s  generated client drift              pass
      3s  mcp tool reference drift            pass
      1s  version sync                        pass
      3s  annotator + app e2e (chromium)      see below
     35s  browser cycle, real server          pass

annotator + app e2e failed twice on Port 5273 is already in use — never on an
assertion. playwright.config.ts pins 5273 with --strictPort, and two other worktrees
(visionset-223-polyline-kernel, visionset-pytest-q-trap) were cycling their own
check.sh runs concurrently; the port was verifiably free at launch each time and taken by
the time vite bound it. Re-run standalone with the port confirmed free:
185 passed (2.2m), 0 failures. browser cycle passed on every attempt (35s, 32.9s).

Found, not fixed (second)

Two concurrent check.sh runs on one machine cannot both run the e2e suite. Port 5273 is
fixed and --strictPort, so the loser fails with a message that reads like a broken dev
server rather than "somebody else is using the port". Same family as #336 — a local-gate
ergonomics gap, not a product bug. Untouched.

…kgrounds

`kill $(jobs -p)` is always a no-op: command substitution forks, and the
forked shell has an empty job table. #332 hit it twice, from two worktrees,
and left twenty-four spin loops running after the task closed cleanly.

Collect PIDs with $! at spawn, clean up in a trap ... EXIT, never redirect
the cleanup's stderr away, and verify the PIDs are gone before claiming so.
@JArmandoAnaya
JArmandoAnaya enabled auto-merge (squash) August 5, 2026 07:42
@JArmandoAnaya
JArmandoAnaya merged commit 10032b1 into main Aug 5, 2026
14 checks passed
@JArmandoAnaya
JArmandoAnaya deleted the docs/background-process-cleanup branch August 5, 2026 07:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant