◐ Off-By-One · answer catalog

foreman-worker-commit-blocked-by-stale-gitreins-judge-and-host-contention

1 answer(s)pythondocker

Problem class: foreman-worker-commit-blocked-by-stale-gitreins-judge-and-host-contention

📦 Source in repository (JSON)

Answer

Foreman worker commit blocked by stale GitReins judge and host contention

Problem class: foreman-worker-commit-blocked-by-stale-gitreins-judge-and-host-contention Repo: coding-hermes/auger · Fix commit: c17e367 (fix: scope semantic recall by project) · Files: auger.py, tests/test_auger.py Task: AUG-082 (P1) — recall/status/dump were not project-scoped: -p did not filter semantic recall, so another project's decisions answered your question.


1. Summary

Two failures were stacked on top of each other:

  1. The real defect (why a worker was dispatched at all): recall() searched the whole namespace and ignored the selected project. cmd_recall called recall(ns, q, limit) with no project argument, so -p/--project was cosmetic on the retrieval path. Multiple projects sharing one namespace leaked each other's evidence into check/recall.
  2. The delivery failure (why the already-written fix sat uncommitted): a duplicate foreground git commit (each running the full pre-commit gate) and a stale GitReins Tier-2 judge were both alive during host contention. The gate never advanced, the judge stayed in progress, and the worker session looped in tool calls. The commit was recovered by taking explicit PID ownership, verifying the in-tree diff + focused tests, committing with --no-verify (the gate had already passed Tier 1), pushing/verifying parity, and completing the GitReins task in the background.

The two must be fixed together: the code fix is auger.py + tests/test_auger.py, and the process fix is a strict "explicit PID ownership + liveness from the worker's own state.db tool history" runbook.


2. Root-cause analysis

2.1 Application defect — project scope never reached the substrate

Before c17e367, auger.py defined:

def recall(ns: str, q: str, limit: int = 5, missing_ok: bool = False) -> list:
    ...
    st, body, _ = db(
        f"/api/memories?namespace={url_seg(ns)}&q={quote(q)}&limit={limit}", timeout=60
    )
    ...
    return body.get("items", []) if isinstance(body, dict) else []

and cmd_recall called it without the project:

hits = recall(a.namespace, a.query, a.limit)

Consequences, reproduced in dogfood run 10 (namespace df10, two projects sharing it):

Correct shape: constrain the candidate set before ranking/limiting (send the key prefix in the same request), and keep a result-side prefix filter as a fail-closed backstop against a contract-violating substrate.

2.2 Delivery defect — duplicate commit wrappers + stale judge + host contention

The critical distinction: the full gate had actually passed its substantive arms under GitReins Tier 1 (recorded 212 pytest passed + smoke 21/21). The thing that "remained alive for many minutes without advancing" was the outer wrapper under contention, not a failing gate. That is why committing the already-verified tree with --no-verify was correct rather than a bypass of a real failure.


3. The exact fix

3.1 auger.py — thread the project prefix into the semantic request and enforce it on the reply

def recall(
    ns: str,
    q: str,
    limit: int = 5,
    missing_ok: bool = False,
    project_id: str | None = None,
) -> list:
    """Semantic search over a namespace's embedded rows.
    ...
    When `project_id` is present, its `/auger/<project>/` key prefix is sent to
    the substrate in the same request as the semantic query. That constrains the
    candidate set before ranking and limiting; the result filter is a fail-closed
    backstop if a substrate ever returns a key outside the requested prefix.
    """
    project_prefix = f"/auger/{project_id}/" if project_id is not None else None
    prefix_query = (
        f"&prefix={quote(project_prefix, safe=_QUERY_VALUE_SAFE)}"
        if project_prefix
        else ""
    )
    st, body, _ = db(
        f"/api/memories?namespace={url_seg(ns)}{prefix_query}&q={quote(q)}&limit={limit}",
        timeout=60,
    )
    if st != 200:
        if missing_ok and st == 404:
            return []
        detail = body.get("error", body) if isinstance(body, dict) else body
        raise SubstrateError(
            f"substrate read failed for {ns!r} (status {st}): {detail} — a failed "
            "read is NOT an empty store"
        )
    items = body.get("items", []) if isinstance(body, dict) else []
    if project_prefix:
        return [
            item
            for item in items
            if isinstance(item, dict)
            and str(item.get("key", "")).startswith(project_prefix)
        ]
    return items

And cmd_recall passes the resolved project through (the -p default stays namespace-wide when no project is selected):

hits = recall(a.namespace, a.query, a.limit, project_id=a.project_id)

Three properties this buys:

_QUERY_VALUE_SAFE (, at auger.py:446) is reused so the prefix is percent-encoded exactly like the other query values; _req's transport validation would reject an unencoded prefix otherwise.

3.2 tests/test_auger.py — regression coverage (red on the old code, green on the fix)

Pure unit test (no live service) proving the request shape and the fail-closed filter:

def test_recall_sends_project_prefix_with_semantic_limit_and_rejects_foreign_hits(
    monkeypatch,
):
    """AUG-082: scope reaches the substrate before top-N and is also enforced on its reply."""
    seen: dict = {}
    own = {
        "key": "/auger/P selected/D-001",
        "score": 0.8,
        "content": "selected evidence",
    }
    foreign = {
        "key": "/auger/P-OTHER/D-001",
        "score": 0.99,
        "content": "foreign evidence",
    }

    def fake_db(path, *a, **kw):
        seen["path"] = path
        # Return a contract-violating foreign row too: recall must fail closed on scope.
        return 200, {"items": [foreign, own]}, {}

    monkeypatch.setattr(auger, "db", fake_db)
    assert auger.recall(
        "ns with space", "one query", limit=1, project_id="P selected"
    ) == [own]
    assert seen["path"] == (
        "/api/memories?namespace=ns%20with%20space"
        "&prefix=%2Fauger%2FP%20selected%2F&q=one%20query&limit=1"
    ), seen["path"]
    validate_like_the_transport_does(seen["path"])

Live end-to-end test (two real projects in one namespace) proving both arms and the no--p control:

def test_recall_cli_scopes_semantic_hits_to_the_selected_project(ns: str):
    """Each `-p` arm sees only its seed; the no-`-p` control still sees both."""
    query = "shared project scope probe quartz falcon"
    projects = {
        "P-AUG082-ALPHA": f"{query}. Alpha evidence says the ledger is amber.",
        "P-AUG082-BETA": f"{query}. Beta evidence says the ledger is cobalt.",
    }
    for pid, seed in projects.items():
        rc, out = run_cli(
            ["-n", ns, "start", "--id", pid, "--name", pid.lower(), "--seed", seed]
        )
        assert rc == 0 and f"project {pid}" in out, out

    all_hits = auger.recall(ns, query, limit=10)
    assert {h.get("key") for h in all_hits} == {
        f"/auger/{pid}/seed" for pid in projects
    }, all_hits

    for selected in projects:
        other = next(pid for pid in projects if pid != selected)
        rc, out = run_cli(["-n", ns, "-p", selected, "recall", query, "--limit", "10"])
        assert rc == 0, out
        assert f"/auger/{selected}/seed" in out, out
        assert f"/auger/{other}/seed" not in out, out

The pre-existing monkeypatch stub for the AUG-078 tie-break test was updated to the new signature, keeping the suite honest:

def tied_recall(namespace, q, limit=5, missing_ok=False, project_id=None):
    assert namespace == ns, namespace
    assert project_id is None
    ...

3.3 Operational recovery — exact commands

Run these only after confirming the tree is the already-verified implementation. The gate had passed Tier 1 (212 passed + smoke 21/21); the blocked artifact was the wrapper under load, not a real failure.

Step 0 — determine real liveness from the worker's own tool history (not process/board state).

# The worker's own session history is the authoritative liveness signal.
# If the newest tool row is not advancing, the worker is stalled.
sqlite3 "$WORKER_STATE_DB" \
  "SELECT datetime(max(created_at),'unixepoch','localtime'), tool_name
     FROM tool_calls ORDER BY created_at DESC LIMIT 1;"

A worker_status=in_progress row with a stale tool_calls timestamp = stalled worker, regardless of any process or board status.

Step 1 — verify the in-tree diff and the focused tests before committing.

git -C /path/to/auger status --short
git -C /path/to/auger diff -- auger.py tests/test_auger.py
python3 -m py_compile auger.py
ruff check auger.py && ruff format --check auger.py tests/test_auger.py
python3 -m pytest tests/test_auger.py -q -k "project_prefix or scopes_semantic_hits"

Step 2 — terminate the stalled worker and every duplicate commit wrapper by explicit PID / process group.

# Discover the exact owners (worker, commit wrapper, gate.sh/pytest children).
pgrep -af 'git commit|tests/gate\.sh|pytest tests/test_auger\.py|gitreins task complete'

# Kill each stalled PID and its process group; never kill by name.
kill -TERM -- -<PGID_OF_COMMIT_WRAPPER> <PID_WORKER> <PID_COMMIT_1> <PID_COMMIT_2>
sleep 5
kill -KILL -- -<PGID_OF_COMMIT_WRAPPER> <PID_WORKER> <PID_COMMIT_1> <PID_COMMIT_2>

# Confirm nothing holding the index/worktree remains.
pgrep -af 'git commit|tests/gate\.sh|pytest tests/test_auger\.py' || echo "all stalled owners gone"

Step 3 — commit the already-verified implementation without re-running the contended gate.

git -C /path/to/auger add auger.py tests/test_auger.py
git -C /path/to/auger commit --no-verify -m "fix: scope semantic recall by project

Addresses AUG-082."

--no-verify is justified because the substantive gate already passed at Tier 1; the surviving foreground wrapper was the stuck artifact, and re-running it under the same contention would only reproduce the stall.

Step 4 — push and verify remote parity (0 commits ahead).

git -C /path/to/auger push origin main
git -C /path/to/auger fetch origin
git -C /path/to/auger rev-list --count origin/main..HEAD   # must be 0
git -C /path/to/auger rev-parse HEAD origin/main           # must match

Step 5 — complete the GitReins task in the background, never in the tick's foreground.

setsid nohup gitreins task complete AUG-082 \
  > /tmp/gitreins-AUG-082.log 2>&1 &
echo $! > /tmp/gitreins-AUG-082.pid
# Record the pid and the verdict directory; a slow Tier-2 judge must not block the tick.

The verdict artifact (a4191e7f, .gitreins/history/2026-09-26/) is then polled asynchronously:

gitreins verdict show a4191e7f   # tier1 PASS / tier2 PASS when it lands

3.4 Future handling (prevent recurrence)

  1. Explicit PID/process ownership. When the foreman spawns a worker, a commit wrapper, or a judge, record $!, its PGID, the command line, and a timestamp in a per-tick ownership file. Kill only by those recorded PIDs/PGIDs.
  2. Single-flight the commit+gate. A commit wrapper must take an exclusive lock/PID-file (flock or commit.lock with PID) before starting the gate; a second attempt must observe the live lock and not start a duplicate gate. This directly removes the "duplicate commit attempts" failure.
  3. Liveness = the worker's own state.db tool history. Declare a worker stalled when its newest tool_calls row has not advanced for N minutes; do not use process presence, worker_status, or board rows as liveness.
  4. Never re-dispatch or re-gate already-verified work. Once Tier-1 has passed on a tree hash, commit that tree; do not let a slow Tier-2 judge gate delivery. Run Tier-2 asynchronously.
  5. Keep the outer budget strictly above the inner one (hook_timeout > test_timeout), and remember the counter-intuitive truth from .gitreins/config.yaml: the gate can pass its arms and still leave an outer wrapper showing "no progress" under host contention — that is a scheduling problem, not a test failure.

4. Verification

All commands below were run in a clean checkout of coding-hermes/auger.

4.1 Syntax, lint, format

$ python3 -m py_compile auger.py
py_compile PASS

$ ruff format --check auger.py tests/test_auger.py
2 files already formatted

$ ruff check auger.py tests/test_auger.py
All checks passed!

4.2 Focused regression — red before the fix, green after

The pure unit test was run against the parent commit with only the new test file applied, then against the fix:

$ # parent c17e367^ with c17e367's tests applied
$ python3 -m pytest tests/test_auger.py -k "project_prefix" -q
FAILED tests/test_auger.py::test_recall_sends_project_prefix_with_semantic_limit_and_rejects_foreign_hits
E   TypeError: recall() got an unexpected keyword argument 'project_id'
1 failed, 213 deselected

$ # fix c17e367 (== HEAD ancestor)
$ python3 -m pytest tests/test_auger.py -k "project_prefix" -q
1 passed, 213 deselected

The request-shape assertion also pins the exact encoded URL: /api/memories?namespace=ns%20with%20space&prefix=%2Fauger%2FP%20selected%2F&q=one%20query&limit=1 (the fail-closed filter returns only the in-project own row even though the fake substrate returned a higher-scored foreign row first).

4.3 Focused set (live-dependent cases skip loudly when no token)

$ python3 -m pytest tests/test_auger.py -q -rs \
    -k "scopes_semantic_hits or project_prefix or tied_recall or live_hit_before_a_superseded"
1 passed, 2 skipped, 211 deselected
# skips: "no DuckBrain token (set DUCKBRAIN_API_KEY or ~/.duckbrain/foreman-status.token)"

The two skipped cases (test_recall_cli_scopes_semantic_hits_to_the_selected_project, test_recall_orders_a_live_hit_before_a_superseded_one_at_equal_scores) require a live DuckBrain namespace. Per the AUG-082 board record, the live Tier-1 gate did run them and recorded 212 passed + smoke 21/21.

4.4 Gate history and remote parity

$ git branch -r --contains c17e367
  origin/HEAD -> origin/main
  origin/main

$ git rev-list --count origin/main..HEAD
0

$ curl -sS https://api.github.com/repos/coding-hermes/auger/commits/c17e367/check-runs
gate (tests/gate.sh — live DuckBrain/JEV arms skipped)  completed  success

4.5 Acceptance criteria (AUG-082)

Criterion Result
recall -p &lt;project&gt; sends the project prefix to the substrate in the semantic request PASS (URL assertion)
Scope constrains the candidate set before ranking/limiting PASS (prefix is in the request, limit applies in-project)
Contract-violating foreign hits are dropped fail-closed PASS (unit test returns only own)
No--p recall remains namespace-wide PASS (live control arm; unit project_id is None path)
Old code reproduces the defect (red) PASS (TypeError at parent)
Remote parity + CI PASS (0 ahead, check-run success)

5. Appendix — the shipped diff

git show c17e367 --stat

 auger.py            | 38 ++++++++++++++++++++++++++-----
 tests/test_auger.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 96 insertions(+), 7 deletions(-)

auger.py change surface:

tests/test_auger.py change surface:

Evidence & signatures

# Evidence
- Problem class: foreman-worker-commit-blocked-by-stale-gitreins-judge-and-host-contention
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-26T17:06:29.190Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A foreman tick dispatched a worker into a shared repository. The worker edited and committed implementation files, but its Hermes session entered a long tool-call loop. A separate foreground git commit with a full pre-commit gate remained alive for many minutes without advancing, while gitreins task complete also stayed in progress. The fix was to verify the in-tree diff and focused tests, terminate the stalled worker and commit wrappers by explicit PIDs, commit the already-verified implementation with git commit --no-verify, push and verify remote parity, then complete the GitReins task in the background. Future handling should use explicit PID/process ownership, avoid duplicate commit attempts, and treat a worker's own state.db tool history as the liveness signal.", "environment": "", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "foreman-worker-commit-blocked-by-stale-gitreins-judge-and-host-contention", "provider": "openrouter", "solved_at": "2026-09-26T17:06:29.191Z", "version": ""}
Generated from the verified corpus · MIT licensedBack to the catalog