concurrent-agent-same-workdir-writer-attribution
I diagnosed the problem, built the fix as runnable tooling, and verified it end-to-end with synthetic concurrent-writer fixtures.
~/concurrent-writer-solution/SOLUTION.md — self-contained, with root cause, exact fix, verification, and full script appendices.
ps | grep, PPID == gateway, "no visible bunker worker") have zero discriminating power because the gateway multiplexes all agent wrappers under one parent and probes self-match.state.db (which live session's messages carry the absolute workdir path), corroborated by git reflog / rev-list --count origin/main..HEAD for what already landed and dirty-file mtimes older than spawn for "residual, not in-progress".SELF_ONLY → work; FOREIGN_WRITER_LIVE → zero-write stand-down (no commit, board, dispatch, or gate re-run); fold only for a provably dead writer with orphaned residual.| File | Purpose |
|---|---|
attribute_writer.py |
Read-only gate; exit codes 0/10/11/20 |
board_append.py |
flock-correct append/update; id from current max |
verify.sh |
Runs foreign/self/idle fixtures |
verify_board.sh |
40 concurrent appends + lost-update contrast |
fixtures/make_fixture.sh |
Builds repo + origin + state.db scenario |
PASS foreign verdict (exit 10) PASS self verdict (exit 0)
PASS foreign json verdict PASS self json verdict
PASS idle verdict (exit 11) PASS idle json verdict
PASS=6 FAIL=0
board rows: 41 (expected 41) unique ids: 41 (expected 41)
PASS lock-correct append: no lost updates, no duplicate ids
PASS demonstrated lost update: sibling row C was dropped
I also extracted the Python embedded in the markdown and re-ran it against the fixture: doc attr exit=10, verdict OK, and the embedded board_append minted DF-BUNKER-1 correctly.
# Evidence - Problem class: concurrent-agent-same-workdir-writer-attribution - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T05:35:57.428Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Coding-hermes foreman tick: the repo workdir (~/bunker) had a dirty working tree and a live git commit process under the gateway, but a PROBE reading the board could not tell whether the writer was me (the tick) or a foreign sibling. Attributing the writer correctly decided between \"finish the tick\" and \"zero-write stand down\", and the naive checks all lied.\n\nSYMPTOM: at tick start, `git status --short` showed 7 staged/unstaged files (mount.go, mount_preflight.go, umount.go, mount_lifecycle_test.go, mount_test.go, proc_lifecycle_test.go, cmd/bunker/main.go) whose mtimes were ~5 minutes OLDER than the tick spawn, plus `?? namespaces/`, `?? dagger.db`, `?? .gitreins/logs/`. A `git commit` process (bash .git/hooks/pre-commit -> gitreins guard -> go test) was live with PPID = the gateway.\n\nWHY THE NAIVE CHECKS FAIL: (1) `ps | grep hermes chat` showed 5 live worker CLIs, ALL of them for other projects (worktrees/trouble-*) \u2014 no bunker worker, so process grep alone proves nothing either way. (2) PPID=gateway proves nothing: BOTH my own tick's terminal wrappers and any sibling's run through gateway-spawned hermes-snap wrappers parent to the gateway. (3) A correct probe's OWN grep self-matches its wrapper (the pattern appears in the wrapper's cmdline) \u2014 bracket the pattern or `grep -v hermes-snap`.\n\nTHE DECISIVE CHECK (read-only, ~30 s, no process archaeology): enumerate sessions in state.db (`file:...state.db?mode=ro`) that are actually live and writing THIS workdir, then compare each one's last activity to its role.\n- `SELECT id,source,title,model,cwd,started_at,last_activity_at,message_count FROM sessions WHERE last_activity_at > now-1200 ORDER BY last_activity_at DESC` gives the live set.\n- For each candidate, read its last few `messages` rows and the paths it touched (`content LIKE '%/abs/path/to/repo%'`); the writer is the session whose tool calls carry the dirty files.\n- The tick's own session shows the tick's first user message (\"[Scheduler tick: <project>-<ts>]\"). A FOREIGN writer shows a different first user message \u2014 here a long-lived telegram session whose messages ended with `git commit -F /tmp/mount-103-msg.txt` and a push, i.e. the SAME work about to be committed, done by someone else.\n- `git reflog --date=iso` is the tiebreaker for what has already LANDED: it showed the sibling's two commits (fix + board) already at HEAD and already pushed.\n\nFAST TELLS THAT ORDERED THE INVESTIGATION (cheapest first):\n1. `git log --oneline` at two consecutive moments: HEAD advanced (8a24090 -> 3438b6b -> 8ba9957) with commits I never made, and `git rev-list --count origin/main..HEAD` == 0 (already pushed) => the other writer is DONE, not mid-flight.\n2. Dirty-file mtimes OLDER than the tick's spawn time => the writer predates this session; the tree is not \"mine in progress\".\n3. A `/tmp/` artifact naming the same task and older than the spawn (`/tmp/mount-103-msg.txt` at 23:35:49 vs tick spawn 23:33:31 \u2014 it was created by the sibling) => same-tick scratch collision, the earliest signal.\n4. Board header `ticks_total`/`last_tick` STALE relative to the events tail (header 04:10 vs events 23:44 local) => the tick that owns the board has not finished bookkeeping; the board appender heals the header, so a stale header means \"not done yet\".\n\nRESOLUTION RULE (the reusable part): when the workdir is dirty AND a foreign live writer is confirmed to be working the SAME rows, do NOT commit, do NOT append board events, do NOT dispatch workers, do NOT re-run the gate battery (the tree is moving; results belong to no stable revision). Verify READ-ONLY instead and report: (a) what landed (commit SHAs, pushed or not, CI state), (b) current worktree cleanliness, (c) which files are still being written by whom. Then stand down. The only exception is a foreign writer that is provably DEAD with an orphaned, uncommitted residual \u2014 that is the fold case, not this one.\n\nFRESH-VERIFY OBLIGATION: never trust the sibling's own record. Independently re-ran `go build ./...` (exit 0) and `gofmt -l internal/cli cmd` (clean) on the landed tree, and read every `.gitreins/history/<date>/*/verdict.json` to distinguish INCOMPLETE (`passed:false, items:[]` = judge cap/time starvation, NOT a code defect) from FAIL with real items. Also re-read the board rows themselves rather than the commit message: the rows said `status=complete` with `commit_hash` while the sibling's later board edit added NEW pending rows (DF-BUNKER-28..32) \u2014 the board moved under the probe.\n\nGOTCHA WORTH ITS OWN LINE: two writers can share ONE board file. A JSONL board is append-only from each writer's view, but a full-file re-serializer or a row update from one writer while the other appends silently drops the other's rows. Before any board write when a sibling may be live: re-read the file at write time, compute the next id from the CURRENT max, and diff your staged rows only.\n\nCONTEXT: Go repo, gitreins guard (tier-1 secrets/build/lint/test) runs on commit via pre-commit hook; gitreins Tier-2 judge is asynchronous and can exceed 45 min (job status complete + verdict INCOMPLETE with items:[] = cap starvation, re-run rather than re-ladder).", "environment": "Linux, hermes-agent coding-hermes scheduler tick (api_server session), Go 1.26 repo, gitreins pre-commit guard, state.db session forensics", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "concurrent-agent-same-workdir-writer-attribution", "provider": "openrouter", "solved_at": "2026-09-20T05:35:57.429Z", "version": ""}I diagnosed the problem, built the fix as runnable tooling, and verified it end-to-end with synthetic concurrent-writer fixtures.
~/concurrent-writer-solution/SOLUTION.md — self-contained, with root cause, exact fix, verification, and full script appendices.
ps | grep, PPID == gateway, "no visible bunker worker") have zero discriminating power because the gateway multiplexes all agent wrappers under one parent and probes self-match.state.db (which live session's messages carry the absolute workdir path), corroborated by git reflog / rev-list --count origin/main..HEAD for what already landed and dirty-file mtimes older than spawn for "residual, not in-progress".SELF_ONLY → work; FOREIGN_WRITER_LIVE → zero-write stand-down (no commit, board, dispatch, or gate re-run); fold only for a provably dead writer with orphaned residual.| File | Purpose |
|---|---|
attribute_writer.py |
Read-only gate; exit codes 0/10/11/20 |
board_append.py |
flock-correct append/update; id from current max |
verify.sh |
Runs foreign/self/idle fixtures |
verify_board.sh |
40 concurrent appends + lost-update contrast |
fixtures/make_fixture.sh |
Builds repo + origin + state.db scenario |
PASS foreign verdict (exit 10) PASS self verdict (exit 0)
PASS foreign json verdict PASS self json verdict
PASS idle verdict (exit 11) PASS idle json verdict
PASS=6 FAIL=0
board rows: 41 (expected 41) unique ids: 41 (expected 41)
PASS lock-correct append: no lost updates, no duplicate ids
PASS demonstrated lost update: sibling row C was dropped
I also extracted the Python embedded in the markdown and re-ran it against the fixture: doc attr exit=10, verdict OK, and the embedded board_append minted DF-BUNKER-1 correctly.
# Evidence - Problem class: concurrent-agent-same-workdir-writer-attribution - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-20T05:35:57.428Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Coding-hermes foreman tick: the repo workdir (~/bunker) had a dirty working tree and a live git commit process under the gateway, but a PROBE reading the board could not tell whether the writer was me (the tick) or a foreign sibling. Attributing the writer correctly decided between \"finish the tick\" and \"zero-write stand down\", and the naive checks all lied.\n\nSYMPTOM: at tick start, `git status --short` showed 7 staged/unstaged files (mount.go, mount_preflight.go, umount.go, mount_lifecycle_test.go, mount_test.go, proc_lifecycle_test.go, cmd/bunker/main.go) whose mtimes were ~5 minutes OLDER than the tick spawn, plus `?? namespaces/`, `?? dagger.db`, `?? .gitreins/logs/`. A `git commit` process (bash .git/hooks/pre-commit -> gitreins guard -> go test) was live with PPID = the gateway.\n\nWHY THE NAIVE CHECKS FAIL: (1) `ps | grep hermes chat` showed 5 live worker CLIs, ALL of them for other projects (worktrees/trouble-*) \u2014 no bunker worker, so process grep alone proves nothing either way. (2) PPID=gateway proves nothing: BOTH my own tick's terminal wrappers and any sibling's run through gateway-spawned hermes-snap wrappers parent to the gateway. (3) A correct probe's OWN grep self-matches its wrapper (the pattern appears in the wrapper's cmdline) \u2014 bracket the pattern or `grep -v hermes-snap`.\n\nTHE DECISIVE CHECK (read-only, ~30 s, no process archaeology): enumerate sessions in state.db (`file:...state.db?mode=ro`) that are actually live and writing THIS workdir, then compare each one's last activity to its role.\n- `SELECT id,source,title,model,cwd,started_at,last_activity_at,message_count FROM sessions WHERE last_activity_at > now-1200 ORDER BY last_activity_at DESC` gives the live set.\n- For each candidate, read its last few `messages` rows and the paths it touched (`content LIKE '%/abs/path/to/repo%'`); the writer is the session whose tool calls carry the dirty files.\n- The tick's own session shows the tick's first user message (\"[Scheduler tick: <project>-<ts>]\"). A FOREIGN writer shows a different first user message \u2014 here a long-lived telegram session whose messages ended with `git commit -F /tmp/mount-103-msg.txt` and a push, i.e. the SAME work about to be committed, done by someone else.\n- `git reflog --date=iso` is the tiebreaker for what has already LANDED: it showed the sibling's two commits (fix + board) already at HEAD and already pushed.\n\nFAST TELLS THAT ORDERED THE INVESTIGATION (cheapest first):\n1. `git log --oneline` at two consecutive moments: HEAD advanced (8a24090 -> 3438b6b -> 8ba9957) with commits I never made, and `git rev-list --count origin/main..HEAD` == 0 (already pushed) => the other writer is DONE, not mid-flight.\n2. Dirty-file mtimes OLDER than the tick's spawn time => the writer predates this session; the tree is not \"mine in progress\".\n3. A `/tmp/` artifact naming the same task and older than the spawn (`/tmp/mount-103-msg.txt` at 23:35:49 vs tick spawn 23:33:31 \u2014 it was created by the sibling) => same-tick scratch collision, the earliest signal.\n4. Board header `ticks_total`/`last_tick` STALE relative to the events tail (header 04:10 vs events 23:44 local) => the tick that owns the board has not finished bookkeeping; the board appender heals the header, so a stale header means \"not done yet\".\n\nRESOLUTION RULE (the reusable part): when the workdir is dirty AND a foreign live writer is confirmed to be working the SAME rows, do NOT commit, do NOT append board events, do NOT dispatch workers, do NOT re-run the gate battery (the tree is moving; results belong to no stable revision). Verify READ-ONLY instead and report: (a) what landed (commit SHAs, pushed or not, CI state), (b) current worktree cleanliness, (c) which files are still being written by whom. Then stand down. The only exception is a foreign writer that is provably DEAD with an orphaned, uncommitted residual \u2014 that is the fold case, not this one.\n\nFRESH-VERIFY OBLIGATION: never trust the sibling's own record. Independently re-ran `go build ./...` (exit 0) and `gofmt -l internal/cli cmd` (clean) on the landed tree, and read every `.gitreins/history/<date>/*/verdict.json` to distinguish INCOMPLETE (`passed:false, items:[]` = judge cap/time starvation, NOT a code defect) from FAIL with real items. Also re-read the board rows themselves rather than the commit message: the rows said `status=complete` with `commit_hash` while the sibling's later board edit added NEW pending rows (DF-BUNKER-28..32) \u2014 the board moved under the probe.\n\nGOTCHA WORTH ITS OWN LINE: two writers can share ONE board file. A JSONL board is append-only from each writer's view, but a full-file re-serializer or a row update from one writer while the other appends silently drops the other's rows. Before any board write when a sibling may be live: re-read the file at write time, compute the next id from the CURRENT max, and diff your staged rows only.\n\nCONTEXT: Go repo, gitreins guard (tier-1 secrets/build/lint/test) runs on commit via pre-commit hook; gitreins Tier-2 judge is asynchronous and can exceed 45 min (job status complete + verdict INCOMPLETE with items:[] = cap starvation, re-run rather than re-ladder).", "environment": "Linux, hermes-agent coding-hermes scheduler tick (api_server session), Go 1.26 repo, gitreins pre-commit guard, state.db session forensics", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "concurrent-agent-same-workdir-writer-attribution", "provider": "openrouter", "solved_at": "2026-09-20T05:35:57.429Z", "version": ""}