Repo: coding-hermes/auger · File: auger.py (cmdanswer) · Base: 64e076a
I diagnosed the bug against the real repo (coding-hermes/auger @ 64e076a), applied the fix to auger.py, and added an offline regression test that fails on the unfixed code and passes after. Solution written to ~/AUG-034-atomic-refusal.md; the fix lives in /tmp/auger.
auger answer (partial write + duplicate decision.id)Repo: coding-hermes/auger · File: auger.py (cmd_answer) · Base: 64e076a
Class: python-cli-non-atomic-refusal-partial-write
auger answer --question-id Q-does-not-exist ... printed
refused: question 'Q-does-not-exist' does not exist — a decision cannot close a question that is not stored
and exited rc=1, yet the decision table had gained a full row (plus its option rows and the embedded evidence memory). Because the refusal reads as "nothing was stored", the user re-ran the command without --question-id; DuckBrain enforces no unique key on decision.id, so the retry stored a second row under the same id (D-004, D-004). Every render then duplicated: the dump block printed twice, and the derived ACTIVE CONFIGURATION lines and their contradiction warnings appeared 4× (2 copies × 2 rows).
cmd_answer did the writes and the validation in the wrong order:
cmd_answer()
├─ _project() (read)
├─ insert_decision() ── WRITE ← decision row lands
├─ insert(option, …) ── WRITE ← option rows land
├─ remember() ── WRITE ← evidence memory lands
└─ close_question()
└─ node_exists(question) ← REFUSES here, SystemExit
close_question() is correct to refuse a link to a question that is not stored — an edge to nothing would read as evidence a question was answered (the same rule facet() and edge() already apply). The defect is that the only validation of --question-id lived inside the function that ran last, after the writes it was supposed to protect. A refusal that leaves rows behind is not a refusal: the user's retry logic is conditioned on the message, not on inspecting the store.
Two properties were missing:
decision.id — the table declares id as the primary column, but DuckBrain enforces nothing. A pre-insert check is the only enforcement.Hoist the question check to the top of cmd_answer (before any insert) and add a pre-insert duplicate check. close_question keeps its own node_exists as defence in depth.
@@ def cmd_answer(a):
scope = (a.scope or "").strip().lower()
if scope and scope not in DECISION_SCOPES:
raise SystemExit(
f"unknown decision scope {a.scope!r}: expected one of {', '.join(DECISION_SCOPES)}"
)
- did = a.id or f"D-{len(select(ns, 'decision', f'project_id=eq.{pid}')) + 1:03d}"
+ # AUG-034: a refusal must be ATOMIC. Every validation that CAN refuse runs before the first
+ # write. The question link is validated HERE, not only in close_question() below, because
+ # close_question() runs after the decision, its options and the embedded evidence are already
+ # stored: a missing question then left a full row behind under an rc=1 "refused", and the
+ # user's natural retry (minus the bad --question-id) stored a SECOND row with the same id.
+ qid = (a.question_id or "").strip()
+ if qid and not node_exists(ns, "question", qid):
+ raise SystemExit(
+ f"refused: question {qid!r} does not exist — a decision cannot "
+ f"close a question that is not stored"
+ )
+ # AUG-034: decision.id uniqueness. DuckBrain declares `id` as the primary column but enforces
+ # no unique key, so a re-run stores a second row under the same id and every downstream
+ # render duplicates. Refuse an explicit --id BY NAME; an auto id skips over ids already taken.
+ if a.id:
+ did = a.id
+ if select(ns, "decision", f"id=eq.{did}&select=id&limit=1"):
+ raise SystemExit(f"refused: {did} already recorded; use --status to revise")
+ else:
+ n = len(select(ns, "decision", f"project_id=eq.{pid}")) + 1
+ did = f"D-{n:03d}"
+ while select(ns, "decision", f"id=eq.{did}&select=id&limit=1"):
+ n += 1
+ did = f"D-{n:03d}"
row = {
@@
closed, beat2_warnings = "", []
- qid = (a.question_id or "").strip()
if qid:
close_question(ns, pid, did, qid, warnings=beat2_warnings)
closed = f", closed {qid}"
Resulting order — every refusal now happens with zero writes:
cmd_answer()
├─ _project() (read)
├─ validate scope ← may refuse, writes nothing
├─ node_exists(question) ← may refuse, writes nothing
├─ duplicate check on did ← may refuse, writes nothing
├─ insert_decision() ── WRITE
├─ insert(option, …) ── WRITE
├─ remember() ── WRITE
└─ close_question() (re-checks as defence in depth)
Offline regression test tests/test_aug034_atomic_refusal.py replaces auger.db with an in-process fake of the declared-tables API and asserts what exists after an rc=1 refusal:
test_missing_question_refusal_writes_nothing — refused rc=1 leaves decision, option, edge and evidence memories empty.test_duplicate_explicit_id_is_refused — a second --id D-001 is refused (D-001 already recorded; use --status to revise) and the table still holds one row.test_retry_after_refusal_stores_exactly_one_row — refuse, then retry without --question-id; stored ids are exactly ["D-004"].$ cd /tmp/auger
$ python3 -m pytest tests/test_aug034_atomic_refusal.py -q
3 passed
$ git stash push -- auger.py # base 64e076a
$ python3 -m pytest tests/test_aug034_atomic_refusal.py -q
FAILED test_missing_question_refusal_writes_nothing
FAILED test_duplicate_explicit_id_is_refused
FAILED test_retry_after_refusal_stores_exactly_one_row
AssertionError: the retry doubled the record: ['D-004', 'D-004']
3 failed
$ git stash pop
Live read-back form (once a token is present):
$ auger -n "$NS" answer --id D-004 --chosen x --confidence 0.5 \
--question-id Q-does-not-exist; echo "rc=$?"
refused: question 'Q-does-not-exist' does not exist — a decision cannot close a question that is not stored
rc=1
$ curl -s -H "x-api-key: $TOKEN" \
"http://<ip-address>:3000/api/ns/$NS/tables/decision?id=eq.D-004" | jq length
0 # pre-fix this returned 1
$ auger -n "$NS" answer --id D-004 --chosen x --confidence 0.5
$ curl -s -H "x-api-key: $TOKEN" \
"http://<ip-address>:3000/api/ns/$NS/tables/decision?id=eq.D-004" | jq length
1 # pre-fix the retry made this 2
A "refused" exit must be atomic: every validation that can refuse runs before the first write, because the user's retry logic is conditioned on the message, not on inspecting the store.
Any multi-write operation whose validation fires mid-sequence turns a clean refusal into a corrupt partial state, and if ids are not unique-keyed the natural human retry (minus the failed optional argument) doubles the record. Two habits prevent the whole class: validate everything first, then write; and never trust a declared primary key to be enforced — add an explicit pre-insert uniqueness check and refuse the duplicate by name.
# Evidence - Problem class: python-cli-non-atomic-refusal-partial-write - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-23T08:11:21.171Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: a Python CLI (stdlib argparse, no DB of its own; writes go to a REST table API) printed 'refused: question X does not exist - a decision cannot close a question that is not stored' and exited rc=1, yet the target table had gained a full row (decision + option rows). A user who sees 'refused' assumes nothing was stored, re-runs the command without the refused optional argument, and now the store holds TWO rows with the SAME primary id (the API table has no unique key on the id column). Every downstream render then duplicates: the dump printed the decision block twice, and derived 'ACTIVE CONFIGURATION' lines and their contradiction warnings appeared 4x (2 copies x 2 rows). ROOT CAUSE: cmd_answer performs insert(decision) + insert(options) FIRST and only then calls close_question(), which validates the referenced question with node_exists() and raises SystemExit. The refusal-by-design is correct (storing a link to nothing would read as evidence a question was answered) but the ORDER is wrong: validation must precede the writes it protects. THE FIX (what we shipped as a board row with acceptance criteria): (1) hoist the node_exists() check to the top of cmd_answer, before any insert - the refusal then costs nothing and leaves nothing behind; (2) add a uniqueness guarantee on decision.id (declared-table primary key or a pre-insert duplicate check: 'D-001 already recorded; use --status to revise'). GENERAL RULE: a 'refused' exit must be atomic - every validation that CAN refuse must run before the first write, because the user's retry logic is conditioned on the message, not on inspecting the store. This generalizes beyond CLIs: any multi-write operation whose validation fires mid-sequence converts a clean refusal into a corrupt partial state, and if ids are not unique-keyed the natural human retry (minus the failed optional arg) doubles the record. VERIFIED: reproduced at repo HEAD (curl GET ?id=eq.D-004 returned the full row after the rc=1 refusal); the duplicate was created live by the retry path; filed as board row AUG-034 with the repro commands.", "environment": "Linux host, stdlib-only CLI, writes via DuckBrain declared-tables HTTP API (POST /api/ns/<ns>/tables/<t>)", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "python-cli-non-atomic-refusal-partial-write", "provider": "openrouter", "solved_at": "2026-09-23T08:11:21.171Z", "version": "3.11"}I diagnosed the bug against the real repo (coding-hermes/auger @ 64e076a), applied the fix to auger.py, and added an offline regression test that fails on the unfixed code and passes after. Solution written to ~/AUG-034-atomic-refusal.md; the fix lives in /tmp/auger.
auger answer (partial write + duplicate decision.id)Repo: coding-hermes/auger · File: auger.py (cmd_answer) · Base: 64e076a
Class: python-cli-non-atomic-refusal-partial-write
auger answer --question-id Q-does-not-exist ... printed
refused: question 'Q-does-not-exist' does not exist — a decision cannot close a question that is not stored
and exited rc=1, yet the decision table had gained a full row (plus its option rows and the embedded evidence memory). Because the refusal reads as "nothing was stored", the user re-ran the command without --question-id; DuckBrain enforces no unique key on decision.id, so the retry stored a second row under the same id (D-004, D-004). Every render then duplicated: the dump block printed twice, and the derived ACTIVE CONFIGURATION lines and their contradiction warnings appeared 4× (2 copies × 2 rows).
cmd_answer did the writes and the validation in the wrong order:
cmd_answer()
├─ _project() (read)
├─ insert_decision() ── WRITE ← decision row lands
├─ insert(option, …) ── WRITE ← option rows land
├─ remember() ── WRITE ← evidence memory lands
└─ close_question()
└─ node_exists(question) ← REFUSES here, SystemExit
close_question() is correct to refuse a link to a question that is not stored — an edge to nothing would read as evidence a question was answered (the same rule facet() and edge() already apply). The defect is that the only validation of --question-id lived inside the function that ran last, after the writes it was supposed to protect. A refusal that leaves rows behind is not a refusal: the user's retry logic is conditioned on the message, not on inspecting the store.
Two properties were missing:
decision.id — the table declares id as the primary column, but DuckBrain enforces nothing. A pre-insert check is the only enforcement.Hoist the question check to the top of cmd_answer (before any insert) and add a pre-insert duplicate check. close_question keeps its own node_exists as defence in depth.
@@ def cmd_answer(a):
scope = (a.scope or "").strip().lower()
if scope and scope not in DECISION_SCOPES:
raise SystemExit(
f"unknown decision scope {a.scope!r}: expected one of {', '.join(DECISION_SCOPES)}"
)
- did = a.id or f"D-{len(select(ns, 'decision', f'project_id=eq.{pid}')) + 1:03d}"
+ # AUG-034: a refusal must be ATOMIC. Every validation that CAN refuse runs before the first
+ # write. The question link is validated HERE, not only in close_question() below, because
+ # close_question() runs after the decision, its options and the embedded evidence are already
+ # stored: a missing question then left a full row behind under an rc=1 "refused", and the
+ # user's natural retry (minus the bad --question-id) stored a SECOND row with the same id.
+ qid = (a.question_id or "").strip()
+ if qid and not node_exists(ns, "question", qid):
+ raise SystemExit(
+ f"refused: question {qid!r} does not exist — a decision cannot "
+ f"close a question that is not stored"
+ )
+ # AUG-034: decision.id uniqueness. DuckBrain declares `id` as the primary column but enforces
+ # no unique key, so a re-run stores a second row under the same id and every downstream
+ # render duplicates. Refuse an explicit --id BY NAME; an auto id skips over ids already taken.
+ if a.id:
+ did = a.id
+ if select(ns, "decision", f"id=eq.{did}&select=id&limit=1"):
+ raise SystemExit(f"refused: {did} already recorded; use --status to revise")
+ else:
+ n = len(select(ns, "decision", f"project_id=eq.{pid}")) + 1
+ did = f"D-{n:03d}"
+ while select(ns, "decision", f"id=eq.{did}&select=id&limit=1"):
+ n += 1
+ did = f"D-{n:03d}"
row = {
@@
closed, beat2_warnings = "", []
- qid = (a.question_id or "").strip()
if qid:
close_question(ns, pid, did, qid, warnings=beat2_warnings)
closed = f", closed {qid}"
Resulting order — every refusal now happens with zero writes:
cmd_answer()
├─ _project() (read)
├─ validate scope ← may refuse, writes nothing
├─ node_exists(question) ← may refuse, writes nothing
├─ duplicate check on did ← may refuse, writes nothing
├─ insert_decision() ── WRITE
├─ insert(option, …) ── WRITE
├─ remember() ── WRITE
└─ close_question() (re-checks as defence in depth)
Offline regression test tests/test_aug034_atomic_refusal.py replaces auger.db with an in-process fake of the declared-tables API and asserts what exists after an rc=1 refusal:
test_missing_question_refusal_writes_nothing — refused rc=1 leaves decision, option, edge and evidence memories empty.test_duplicate_explicit_id_is_refused — a second --id D-001 is refused (D-001 already recorded; use --status to revise) and the table still holds one row.test_retry_after_refusal_stores_exactly_one_row — refuse, then retry without --question-id; stored ids are exactly ["D-004"].$ cd /tmp/auger
$ python3 -m pytest tests/test_aug034_atomic_refusal.py -q
3 passed
$ git stash push -- auger.py # base 64e076a
$ python3 -m pytest tests/test_aug034_atomic_refusal.py -q
FAILED test_missing_question_refusal_writes_nothing
FAILED test_duplicate_explicit_id_is_refused
FAILED test_retry_after_refusal_stores_exactly_one_row
AssertionError: the retry doubled the record: ['D-004', 'D-004']
3 failed
$ git stash pop
Live read-back form (once a token is present):
$ auger -n "$NS" answer --id D-004 --chosen x --confidence 0.5 \
--question-id Q-does-not-exist; echo "rc=$?"
refused: question 'Q-does-not-exist' does not exist — a decision cannot close a question that is not stored
rc=1
$ curl -s -H "x-api-key: $TOKEN" \
"http://<ip-address>:3000/api/ns/$NS/tables/decision?id=eq.D-004" | jq length
0 # pre-fix this returned 1
$ auger -n "$NS" answer --id D-004 --chosen x --confidence 0.5
$ curl -s -H "x-api-key: $TOKEN" \
"http://<ip-address>:3000/api/ns/$NS/tables/decision?id=eq.D-004" | jq length
1 # pre-fix the retry made this 2
A "refused" exit must be atomic: every validation that can refuse runs before the first write, because the user's retry logic is conditioned on the message, not on inspecting the store.
Any multi-write operation whose validation fires mid-sequence turns a clean refusal into a corrupt partial state, and if ids are not unique-keyed the natural human retry (minus the failed optional argument) doubles the record. Two habits prevent the whole class: validate everything first, then write; and never trust a declared primary key to be enforced — add an explicit pre-insert uniqueness check and refuse the duplicate by name.
# Evidence - Problem class: python-cli-non-atomic-refusal-partial-write - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-23T08:11:21.171Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: a Python CLI (stdlib argparse, no DB of its own; writes go to a REST table API) printed 'refused: question X does not exist - a decision cannot close a question that is not stored' and exited rc=1, yet the target table had gained a full row (decision + option rows). A user who sees 'refused' assumes nothing was stored, re-runs the command without the refused optional argument, and now the store holds TWO rows with the SAME primary id (the API table has no unique key on the id column). Every downstream render then duplicates: the dump printed the decision block twice, and derived 'ACTIVE CONFIGURATION' lines and their contradiction warnings appeared 4x (2 copies x 2 rows). ROOT CAUSE: cmd_answer performs insert(decision) + insert(options) FIRST and only then calls close_question(), which validates the referenced question with node_exists() and raises SystemExit. The refusal-by-design is correct (storing a link to nothing would read as evidence a question was answered) but the ORDER is wrong: validation must precede the writes it protects. THE FIX (what we shipped as a board row with acceptance criteria): (1) hoist the node_exists() check to the top of cmd_answer, before any insert - the refusal then costs nothing and leaves nothing behind; (2) add a uniqueness guarantee on decision.id (declared-table primary key or a pre-insert duplicate check: 'D-001 already recorded; use --status to revise'). GENERAL RULE: a 'refused' exit must be atomic - every validation that CAN refuse must run before the first write, because the user's retry logic is conditioned on the message, not on inspecting the store. This generalizes beyond CLIs: any multi-write operation whose validation fires mid-sequence converts a clean refusal into a corrupt partial state, and if ids are not unique-keyed the natural human retry (minus the failed optional arg) doubles the record. VERIFIED: reproduced at repo HEAD (curl GET ?id=eq.D-004 returned the full row after the rc=1 refusal); the duplicate was created live by the retry path; filed as board row AUG-034 with the repro commands.", "environment": "Linux host, stdlib-only CLI, writes via DuckBrain declared-tables HTTP API (POST /api/ns/<ns>/tables/<t>)", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "python-cli-non-atomic-refusal-partial-write", "provider": "openrouter", "solved_at": "2026-09-23T08:11:21.171Z", "version": "3.11"}