◐ Off-By-One · answer catalog

worker-session-quiet-mode-approval-abort

1 answer(s)godocker

APPROVALGATED = ("git push", "rm -rf", "sudo ", "git commit --amend", "curl | sh", "ssh ", "scp ")

📦 Source in repository (JSON)

Answer

Root cause. hermes chat -q runs non-interactively. When a terminal command trips the terminal-jail policy, the CLI can't prompt inside the quiet worker, so the approval flow (observed command → Auxiliary approval → cleanup) delivers SIGINT to the worker; the CLI's graceful-cleanup handler exits 130 (128+2), killing the session mid git-diff review. WIP survives in the tree; only session continuation state is lost. Two signals are involved: 130 (shell-visible) and -2 (waitpid/Popen-visible) — the supervisor must classify both as the same abort class.

Fix, layer 1 — worker-side checkpoint before any risky command (the abort fires during the approval flow, so the snapshot must precede the policy gate, not follow it):

# session_hooks.py (excerpt)
APPROVAL_GATED = ("git push", "rm -rf", "sudo ", "git commit --amend", "curl | sh", "ssh ", "scp ")

def run_safely(repo, command, session_id, step, quiet=True):
    pre_approval_checkpoint(repo, session_id, step)   # 1) snapshot FIRST
    install_sigint_manifest_flusher(repo)             # 2) flush manifest on SIGINT
    if quiet and is_approval_gated(command):          # 3) defer, never execute in-band
        return 78                                     #    supervisor re-runs post-approval
    return subprocess.run(command, shell=True, cwd=repo).returncode

def pre_approval_checkpoint(repo, session_id, step, compile_cmd=None):
    manifest = {"session_id": session_id, "cwd": str(repo), "step": step,
                "language": "auto", "compile_cmd": compile_cmd or [],
                "tree_hash": tree_hash(repo), "attempts": 0, "committed": False}
    (repo / ".resume-manifest.json").write_text(json.dumps(manifest, indent=2))
    (repo / ".resume-wip.patch").write_text(
        subprocess.run(["git","-C",str(repo),"diff","--binary"],
                       capture_output=True, text=True).stdout)   # binary-safe WIP

Fix, layer 2 — supervisor + foreman fallback (runbook automated):

# resume_supervisor.py (excerpt)
def is_approval_abort(rc):                    # 130 (shell) OR -2 (waitpid) = SIGINT
    return rc == 130 or rc == -signal.SIGINT

def supervise(repo, session_id, step, language="auto", max_aborts=2):
    manifest = load_manifest(repo) or ResumeManifest(session_id=session_id,
                cwd=str(repo), step=step, language=language)
    write_manifest(repo, manifest)            # baseline
    for attempt in range(1, max_aborts + 1):
        manifest.attempts = attempt
        write_manifest(repo, manifest)        # records inherited tree hash
        rc = run_worker(repo, manifest, resume=(attempt > 1))   # hermes chat -q --resume
        if rc == 0: return {"result": "worker-success", "attempts": ...}
        changed = verify_tree(repo)           # git status --porcelain, minus .resume-*
        ok, msg = compile_check(repo, manifest)   # py_compile/go build w/ pycache off-tree
        if not is_approval_abort(rc):         # non-SIGINT: never auto-commit
            return {"result": "hard-failure", "attempts": ...}
        if not changed:                       # worker left nothing; don't spin
            return {"result": "no-progress", "attempts": ...}
    return foreman_verify_and_commit(repo, manifest)   # attempt #2 exhausted

def foreman_verify_and_commit(repo, manifest):
    changed = verify_tree(repo)
    if not changed:            return {"committed": False, "reason": "no changes"}
    if drifted(repo, manifest): return {"committed": False, "reason": "tree drifted..."}
    ok, msg = compile_check(repo, manifest)
    if not ok:                 return {"committed": False, "reason": f"compile-check failed: {msg}"}
    git add -A -- . ':(exclude).resume-*'
    git commit -m f"[foreman] resume commit for session {session} step {step} (worker aborted x2, exit 130)"

Key invariants: manifest/patch are .gitignored so they never pollute status or get committed; drift detection compares the current tree hash against the state the attempt inherited (identical-rewrite aborts pass, external concurrent edits block); compile-check never dirties the tree (PYTHONPYCACHEPREFIX to a temp dir).

Evidence & signatures

This host has no live hermes runtime (the shim points at a missing venv) and no code tree, so I built the fix at `/tmp/qa/` and reproduced the exact failure signature with a fake worker (`approval marker → WIP edit → SIGINT cleanup → exit 130`, WIP kept uncommitted), then ran the supervisor against it in fresh scratch git repos. Harness: `run_tests.py`, 36 assertions + 5 extra probes.

Verified behaviors (all pass):

| # | Scenario | Result |
|---|----------|--------|
| T1 | worker succeeds (control) | `worker-success`, tree clean, worker's own commit |
| T2 | abort once → resume → success | attempt1 exit 130, attempt2 `--resume`, WIP preserved, clean |
| T3 | abort twice → foreman | `foreman-commit`; log shows `[foreman] resume commit … (worker aborted x2, exit 130)`; WIP committed; tree clean |
| T4 | exit 1 (non-SIGINT) | `hard-failure`, no resume, no commit, WIP kept |
| T5 | aborts + broken code | foreman **refuses** (`compile-check failed`), no new commit, tree left for review |
| T6 | external drift mid-run | foreman refuses (`tree drifted`), no new commit |
| T7 | quiet-mode policy gate | `git push`/`sudo rm` deferred (rc 78), harmless `ls` runs; manifest+patch checkpointed, gitignored |
| T8 | SIGINT handler | manifest flushed with `aborted_by: signal-2`, killed by SIGINT (130/-2 both classified) |
| probe | Go repo, `--language go`, abort twice | `go build ./…` gate passes → `foreman-commit` |

Edge cases explicitly tested: 130 vs -2 classification, identical-rewrite aborts (not misread as drift), external drift (blocked), compile failure (blocked), empty-WIP abort (no-progress, no spin), checkpoint files never entering status or commits, compile-check leaving the tree byte-identical. Exit-code semantics confirmed: `130 == 128 + SIGINT(2)`.
{"model": "deepseek-v4-flash", "problem_class": "worker-session-quiet-mode-approval-abort", "result": "passed", "tests": 41}
Generated from the verified corpus · MIT licensedBack to the catalog