Skip to content

feat(tool): govern background command job lifecycle (cap, memory floor, shutdown reaping) - #2761

Merged
topcheer merged 8 commits into
mainfrom
r71-bgjob-lifecycle
Sep 25, 2026
Merged

topcheer merged 8 commits into
mainfrom
r71-bgjob-lifecycle

Conversation

@topcheer

Copy link
Copy Markdown
Owner

Problem

start_command background jobs were the only background work class that survived session exit as orphan processes. TUI shutdownAll() cancelled sub-agents, swarm teammates, ext/cmd panes and knight tasks — but never the CommandJobManager. A detached dev server or a 24h-timeout build kept running after quit//restart, holding ports and memory with no way to stop it from the new session.

There was also no ceiling on concurrently running background jobs and no memory-pressure awareness: every retry piled another build onto an already-swapping machine (our own CI/shared machines see exit-137 OOM kills from exactly this pattern).

Change (harness-engineering "cheap caps" floor)

Per the Harness Engineering paper (Rec 18: do ship the cheap caps; Hermes: safety-gated parallel batches), this adds bounded resources to the command loop:

  • ShutdownAll(wait) on CommandJobManager: cancel all running jobs, wait up to wait, return reaped count. Cancelled jobs stay readable as terminal entries so late read_command_output still explains the stop.
  • Admission gate at spawn (sampled at start time — no new goroutines):
    • refuse when running jobs ≥ cap (default 8, GGCODE_MAX_RUNNING_JOBS);
    • refuse when process memory (MemStats.Sys) exceeds ceiling (GGCODE_JOB_MEM_LIMIT > GOMEMLIMIT×1.5 > 3 GiB);
    • refusal messages are actionable (stop_command / poll / override env). Ownership never transfers on refusal — no phantom job entries.
  • Exit reaping wired everywhere: TUI shutdownAll (quit / ctrl+d / /restart exec handoff), RunPipe, ACP server exit.
  • Registry.JobManager() accessor; manager wired in RegisterBuiltinTools (single line, mirrors codeIndex).

Not changed

  • No new detector/checker; no UX flow changes; refusal only affects new spawns — running jobs are never killed automatically (disclosure over silent eviction).
  • Detached-job semantics preserved within a live session (still runs until natural exit or stop_command).

Tests

5 new tests in command_jobs_lifecycle_test.go: ShutdownAll reaping + terminal-entry readability, empty-manager no-op, cap refusal + no phantom entries + slot reuse, memory-pressure refusal, env-limit resolution (valid/invalid).

go test -tags goolm -p 1 -parallel 1 ./internal/tool/ ./internal/tui/ ./cmd/...  ✅
gofmt / go vet ✅

Junjun Zhang and others added 8 commits September 25, 2026 16:04
…ecl check (#2734)

jsClassRe required the class name to be followed immediately by '{' or
'<', so 'class Foo extends Bar {' / 'implements' forms -- the dominant
class shape in real JS/TS -- were never counted. Old/new counts both 0
meant the duplicate-class check was silent dead code for those forms.

Add an optional extends/implements heritage list before the terminator;
4 new tests pin match forms, non-class exclusions, duplicate detection,
and the legitimate heritage-change no-warning case.
Multiple self-declared scope constraints formed an implicit AND: after two
different scope declarations every edit violated one of them, burning the
cvMaxWarnings quota with false positives while real violations went silent.
Scope declarations now REPLACE prior scope constraints; avoid constraints
stay additive. 4 regression tests in zz_issue2733_test.go.

Co-Authored-By: ggcode <noreply@ggcode.dev>
…et.TCPConn assertion (#2747)

Production conn is always *tls.Conn (defaultDialIRC wraps tls.Client), so the
type assertion never matched and #2113 F2 anti-wedge protection was dead code.
Call SetWriteDeadline directly through the interface (tls.Conn propagates to
the wrapped TCP conn) and clear it after the write.

Co-Authored-By: ggcode <noreply@ggcode.dev>
…cation (#2754)

case "test", "pytest" flipped testsRan for any run_command whose first
token was 'test' - but bare 'test' is the POSIX shell conditional builtin
('test -f x', 'test -d dist && rm -rf dist'), not a test run. This
silently disarmed the git_commit/git push reversibility gate, same
false-verification family as #2255 and #2552. Narrowed to bare test
runners (pytest/py.test/vitest/jest); #1194 semantics preserved.

Co-Authored-By: ggcode <noreply@ggcode.dev>
…2748)

runStatuslineCommand now returns (text, ok); handleStatuslineMsg only
replaces the cached text on success, honoring the documented contract.
Failed refresh still clears running/dirty so refresh cadence is unchanged.
Phase 3 of the reproducer lifecycle tracker only checked the tool name
(run_command/start_command), so any intermediate command like 'git diff'
or 'ls' discharged the re-run obligation. Now the command must match the
reproducer script shape (reproducerCommandRe) or share a distinctive
token with the recorded reproducer snippet. Also replaces the unused
reproducerFertilityWindow constant with reproducerRerunGraceIterations
and fixes the stale comment in checkIncomplete.

Co-Authored-By: ggcode <noreply@ggcode.dev>
…r, shutdown reaping)

start_command background jobs were the only background work class that
survived session exit as orphan processes: TUI shutdownAll cancelled
sub-agents, swarm teammates, panes and knight tasks, but never the
CommandJobManager — detach=true dev servers and long builds kept running
after quit//restart, holding ports and memory. There was also no ceiling
on concurrently running jobs or on process memory, the documented OOM
vector on shared machines (exit-137 builds).

Implements the harness-engineering "cheap caps" floor for the command
loop, aligned with Hermes-style safety-gated parallel batches:

- CommandJobManager.ShutdownAll(wait): cancel all running jobs, wait up
  to `wait`, return the reaped count. Cancelled jobs stay readable as
  terminal entries so late read_command_output still explains the stop.
- Admission gate at spawn: refuse new jobs when running >= cap
  (default 8, GGCODE_MAX_RUNNING_JOBS) or when process memory
  (MemStats.Sys) exceeds the ceiling (GGCODE_JOB_MEM_LIMIT >
  GOMEMLIMIT*1.5 > 3GiB). Refusal messages are actionable (stop_command /
  poll / override env), sampled at spawn time with no new goroutines.
- Registry.JobManager() accessor; manager wired in RegisterBuiltinTools.
- Reaping wired at every exit: TUI shutdownAll (quit/ctrl+d//restart
  exec handoff), RunPipe, and ACP server exit.

Co-Authored-By: ggcode <noreply@ggcode.dev>

Co-Authored-By: ggcode <noreply@ggcode.dev>
@topcheer

Copy link
Copy Markdown
Owner Author

合并说明:reviewer 书面 approve(四点实证:准入拒绝非驱逐+运行中永不静默杀/OS 实测采样+env 可调/terminal 条目保留迟到读取无悬空/detach 豁免 timeout 不豁免 ShutdownAll 语义分层正确;捆装 7 修复探针回归 ok)。CI 全绿。执行合并。

@topcheer
topcheer merged commit 4a63302 into main Sep 25, 2026
9 checks passed
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