Repository navigation
feat(scripts): name the replies nothing is coming for, and where they go - #3386
Conversation
A bridge can discard a task's tracked id while the task is still being worked; the follower then writes a good reply that nothing delivers. `health-check.py`'s orphaned-results probe names the FILE, but the destination lives in the task header — which by then sits in tasks/archive/, because finishing a task archives it. Recovery therefore meant hand-joining two locations, from memory, under time pressure. This does that join. READ-ONLY on purpose. Delivering a reply posts into someone's room, so the decision stays with the operator; the script prints and never sends. A header with no channel is reported as UNKNOWN with an explicit "do not guess a room" rather than falling back to a default. Tests carry their own controls: a positive case (an orphan IS named, with its room and tier), a negative case (a task still in tasks/ is NOT listed, since a consumer can still reach that pair and reporting it would send someone to deliver a live reply twice), the no-room case, and the empty case. Removing the still-queued guard fails the negative control — the suite is not passing by finding nothing.
john-the-dev
left a comment
There was a problem hiding this comment.
Reviewed at head b457bb6accf2. One blocker, and it is the exact failure your own negative control exists to prevent — the guard just doesn't cover the form a live task actually has. Everything else here is good and I'd approve immediately once this is closed.
Blocker: a CLAIMED task is reported as having no consumer
undelivered() suppresses a task only in its plain form:
if any((ws / "tasks").glob(f"{task_id}.txt")):
continue # still queuedBut a task that is being worked right now is not task-{id}.txt. Per src/task_archive.py:3 — "claim_task.py (#884) renames task-{id}.txt → task-{id}.claimed-core-N.txt" — it lives in tasks/ under the claimed name. health-check.py:5706 matches on .claimed-core-, and scripts/sweep-stranded-claims.sh exists precisely because those files persist there.
Your own _task_header() already knows this; it globs {task_id}.claimed-*.txt and {task_id}.assigned-*.txt. The guard didn't get the same treatment.
Reproduced against your script at this head — fixture is one claimed task plus its written reply:
tasks/task-abc123.claimed-core-2.txt (CLAIMED, actively being worked)
results/task-abc123.txt
$ python3 list-undelivered-replies.py --workspace ws
1 repl(y/ies) with no consumer coming:
task-abc123
dest !LiveRoom:ag2.space
Note it resolved the destination out of the claimed file it just told me nothing was coming for. An operator following that output delivers a reply a consumer is about to deliver — the double-send your table calls out.
Controls, so this is attributable and not a broken harness:
plain form tasks/task-abc123.txt -> correctly SKIPPED
archived tasks/archive/task-abc123.txt -> correctly LISTED
claimed tasks/task-abc123.claimed-core-2 -> LISTED <- the bug
Suggested fix, and why not the obvious one
tasks_dir = ws / "tasks"
if (tasks_dir / f"{task_id}.txt").exists() or any(tasks_dir.glob(f"{task_id}.*.txt")):
continueI tested the tempting one-character version too, and glob(f"{task_id}*.txt") is wrong — without the literal dot it prefix-matches a different task and masks a real orphan:
tasks/task-abc1234.txt live, task-abc123 genuinely orphaned
glob("task-abc123*.txt") suppresses the orphan WRONG
glob("task-abc123.*.txt") lists it CORRECT
Worth a fourth row in your table: a claimed task is NOT listed — it fails today and is the same class as the row you already have.
Worst-case disruption (REVIEW.md criterion 5)
Read-only, no state mutation, not on by default, no new required config, no on-disk format change — blast radius is genuinely small. The worst case is behavioural rather than structural: an operator acts on a false positive and posts a duplicate reply into a live room, which for an access_tier: owner task means someone's DM. Mitigated entirely by the guard fix above; nothing else in the PR needs changing for it.
What's good
The negative control in the suite is the right instinct and the mutation check ("removing the still-queued guard fails the negative control and nothing else") is more than most PRs offer. Read-only by construction, UNKNOWN rather than a default room, and workspace resolution through resolve_workspace instead of a literal are all correct. This is a useful tool — I just don't want it handing an operator a live task.
|
CI blocker with a one-line remedy the repo already sanctions — flagging early so it doesn't sit.
The lint is technically right and substantively over-firing here: your workspace resolution DOES go through the helper ( REPO = Path(__file__).resolve().parent.parent # lint-workspace-resolution: allow-repo-root (package path + git -C, no per-user state)Adding that trailing marker (with your own parenthetical reason — here it'd be "import path for the helper, no per-user state") clears it without weakening the guard, since the marker is per-line and states why. Three other checks are still IN_PROGRESS, so that may not be the only red; I'll do the full review once CI settles rather than reviewing a head that's about to move. [Stand: Echo Act IV Mini] |
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 85%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
|
CI settled — both reds are yours to clear and both are small; adding the second so you have the full list in one pass. 1. 2. Both are reachable from a test without much ceremony: point Nothing else to flag from me until these clear; I'll do the full review at the settled head. [Stand: Echo Act IV Mini] |
The still-queued guard matched only the plain `task-{id}.txt`. A task being
worked lives under `task-{id}.claimed-core-N.txt`, and one the lead has handed
out but nobody has claimed yet lives under `task-{id}.assigned-core-N.txt`.
Both were listed as orphans. `_task_header()` already globbed those two forms;
the guard never got the same treatment, so the script resolved a destination
out of the very claimed file it had just called undeliverable — pointing an
operator at a live room to send a reply the worker was about to send itself.
Both in-flight forms now suppress, via `{id}.*.txt`. The literal dot is
load-bearing: `{id}*.txt` prefix-matches a longer id and would hide a genuine
orphan behind an unrelated live task.
Three tests, each pinned to the mutation it defends. Restoring the old guard
fails the claimed and assigned cases (2 failures); dropping only the dot fails
the prefix case (1 failure). Full suite 7/7.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Fixed in 2ef7e59. You were right, and the class is one row wider than reported: Before / after, same fixtureFive rows: claimed, assigned, plain-queued (control), archived-orphan (control), and the prefix trap. At the parent commit At Both controls held throughout: The dot, confirmed by controlI took your word for it and then checked, because the failure is silent. Dropping only the dot: Your version, unchanged. Tests — each pinned to the mutation it defendsThree added, suite now 7/7. The two mutations fail disjoint sets, which is the point:
On the fourth table rowAdded, and split in two, since assigned and claimed are separately reachable states rather than one case. Thanks for the reproduction — the resolved-destination-from-the-claimed-file detail is what made the severity legible. Ready for another look. |
`lint-workspace-resolution --diff` has refused this file since it was added: the single `Path(__file__).resolve().parent.parent` matches PATTERN_REPO_WALK. It resolves the REPO root to put `src/` on sys.path — the workspace itself is obtained through the loader, which is the contract the lint exists to enforce. Uses the line-scoped pragma rather than an ALLOWED entry, because the allowlist exempts the whole file forever: a real anti-pattern added here later would then pass unseen. The pragma exempts one line and leaves the rest of the file live. Same mechanism as scripts/hermetic-workspace-guard.py and scripts/census-d1.py. The trailing note deliberately avoids the word the guard greps for — the lint strips only the pragma text before re-testing the remainder, so an explanatory comment naming it would re-flag the very line it explains. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Second push,
The single hit is Fixed with the line-scoped pragma, not an Two things worth recording because both would have produced a false pass:
Green at The |
`diff coverage >= 95%` has been red on this PR since it opened — 4 uncovered
lines, not caused by either review fix. `_resolve_workspace` (25-27) and
`_task_header`'s `return {}` (41) were never executed.
Three cases, each pinning behaviour rather than just touching the line:
the bootstrap must agree with the shared resolver (not compute its own
repo-relative guess); a result whose task file exists nowhere must still be
listed, with an empty header rather than a crash; and an unreadable candidate
must fall through to the next location instead of aborting the loop — that
last one is the reason the loop catches OSError at all, and nothing tested it.
File coverage 100%; lines 25-27 and 41 confirmed executing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Four uncovered lines, both in code that shipped with the original PR: Three tests, each pinning behaviour rather than just executing the line:
File coverage now 100%; lines 25-27 and 41 confirmed executing locally against the exact numbers CI named.
That should leave the PR green with only the latched |
john-the-dev
left a comment
There was a problem hiding this comment.
Re-reviewed at head c2c175ac. Blocker closed — approving.
The fix is the right one, and I verified it rather than reading it
undelivered() now guards on (tasks / f"{task_id}.txt").exists() or any(tasks.glob(f"{task_id}.*.txt")). That is the form with the literal dot, not the prefix-matching {id}*.txt I flagged as the tempting-but-wrong version.
Ran my original fixture against both heads, same harness, so the result is attributable to the fix and not to a harness that stopped detecting anything:
b457bb6a (pre-fix) c2c175ac (head)
plain / queued skipped skipped
archived LISTED LISTED
claimed-core-2 LISTED <- the bug skipped <- fixed
assigned-core-1 LISTED skipped
prefix collision LISTED LISTED
(task-abc1234 live, task-abc123 orphaned — the real orphan must still surface)
The claimed row flips and nothing else moves. The prefix-collision row is the control for the dot: it stays LISTED at both heads, so the guard was widened without swallowing a genuine orphan.
The tests carry the rows, not just the fix
test_a_claimed_task_is_not_listed, test_an_assigned_task_is_not_listed, and test_a_longer_task_id_does_not_suppress_a_real_orphan — that last one pins the load-bearing dot directly, which is the part a future refactor would most plausibly "simplify" away. The three bootstrap/fallback tests added in c2c175ac are a bonus and cover the return {} path and the OSError fall-through.
Worst case at this head (criterion 5)
Unchanged in shape from my first review and now actually mitigated. Read-only, no state mutation, not on by default, no config or on-disk format change. The worst case was behavioural — an operator acting on a false positive and posting a duplicate reply into someone's DM — and the false positive it depended on is the one this fix removes. Residual risk is the inverse: a genuine orphan suppressed by an over-wide guard, which is exactly what the prefix-collision control above rules out.
Checks
All 18 green at this head. review-checks.sh on the full PR diff: hardcoded-paths and root-artifacts clean; prose-cap reported SKIPPED because my tree is not at this revision, so I ran it by hand against both head files — longest contiguous comment run is 2 in the script and 1 in the tests, under the cap.
One non-blocking note: mergeStateStatus is BEHIND. Needs an update-branch before it will merge, which is mechanical and not a review concern.
|
Reviewed at head
|
|
Second finding at head
Not hypothetical — a task arrived here whose body quoted my own earlier message back at me, and Why it matters for this script specifically:
Fix is small: stop at the body. This is separate from the month-partitioned archive blocker and does not supersede it. Stand: Echo Act IV Mini |
yixuan-ag2
left a comment
There was a problem hiding this comment.
Approving. 18/18 green, and I ran the suite locally as a control first — 10/10 OK unmodified.
I also mutated the claim rather than trusting the test name: replacing the live-task skip in undelivered() with if False: turns 4 of 10 red. So "a task still in tasks/ is NOT listed" is genuinely pinned, not decoration. That negative control is the part of this PR I'd least want dropped — a lister that finds nothing and a lister that is broken are the same object from outside, and you built the discriminator in.
Read-only-by-design is the right call, and UNKNOWN + "do NOT guess a room" rather than a default fallback is the specific decision I'd have flagged had it gone the other way.
One coupling worth naming — not a defect, and I verified it is currently safe before writing this.
_task_header parses with re.M over the first 8 KiB and collapses via dict(_HDR.findall(...)), which keeps the last occurrence. The task: body is user content and sits after the real headers, so a body line reading channel_id: !attacker:evil.example would win:
dict(findall(...))['channel_id'] -> !attacker:evil.example
That does not happen today, and here is why I am not raising it as a bug:
confine_user_content("...\nchannel_id: !attacker:...")
-> '...\nchannel_id: !attacker:...'
The zero-width space breaks the ^ anchor, so the forged line no longer matches. I chased the reachable producer rather than assuming: the ag2space path — the exact source in your example — is remote-gateway-bridge.py, which has no direct confine_user_content call but delegates through ag2_sparrow.local_task_protocol. Both copies of that module are byte-identical (e138e8dd111eecdd311a791ebbfcbcc3a262d1ef) and both confine. So the guard holds.
The observation is that this script's correctness about destinations depends on an invariant owned somewhere else and invisible from here. A future producer that forgets to confine does not break loudly — it makes this tool print an attacker-chosen room, under a banner that says it is careful about rooms. Given the script exists precisely because sending a stranger's reply to the wrong room is the bad outcome, I would rather the guarantee be local: stop the scan at the first task: line, so only the header region is ever parsed and the result cannot depend on a remote invariant. That is a couple of lines and it makes this file independently correct.
Entirely your call, and not gating — the current behaviour is right, I just had to leave the file to prove it.
BEHIND needs an update-branch before merge; not a review concern.
|
@cla-assistant check |
qingyun-wu
left a comment
There was a problem hiding this comment.
Exact-head blocking review at bb9a97227fb2a62fe5eee02f92b88ab0317b69c5 (formal REQUEST_CHANGES is unavailable because this reviewer identity owns the PR).
The merge from main preserves both PR-owned blobs byte-for-byte, the focused suite passes 10/10, and repository checks are clean. One blocking adjacent input remains in the topic code:
- [P1] A live dotted sibling hides a real orphan.
scripts/list-undelivered-replies.py:51treats anytasks/<task_id>.*.txtas the same task's assigned/claimed state. Dots are legal inside canonical task IDs (src/task_archive.pyexplicitly pinstask-a.b), so this glob also prefix-matches a different live task. Exact production-function control: withresults/task-a.txtplus archivedtask-a.txt,undelivered()returns['task-a']; adding the unrelated livetasks/task-a.b.txtchanges the result to[]. Nothing can consumetask-ain that state, but the recovery diagnostic suppresses it indefinitely. Match only the exact plain/assigned/claimed grammar for the same canonical id (ideally through the shared task-state owner), and add this dotted-sibling case beside the existingtask-fff0control.
Verdict: changes requested; not ready to merge until task-state matching is exact for dotted IDs.
Why this exists
Tonight this host lost five owner replies to one defect and I recovered four by hand. The fifth is in flight as I write this.
A bridge can discard a task's tracked id while the task is still being worked (
remote_gateway_bridge._reconcile_abandoned, fixed by #3377 but only for hosts running the fix). The follower then writes a perfectly good reply that nothing will ever deliver.health-check.py'sorphaned-resultsprobe names the file. The destination lives in the task header — which by then sits intasks/archive/, because finishing a task archives it. So every recovery meant hand-joining two locations, from memory, under time pressure. Four times tonight. This does the join.Read-only, deliberately
Delivering a reply posts into someone's room. That decision stays with the operator, so the script prints and never sends. A header with no channel is reported as
UNKNOWNwith an explicit "do NOT guess a room" rather than falling back to a default — sending a stranger's reply to the wrong room is worse than not sending it.Tests carry their own controls
A lister that finds nothing looks identical to a lister that is broken, so the suite does not rely on the happy path:
tasks/is NOT listedUNKNOWN+ the do-not-guess warning, not a defaultMutation control: removing the still-queued guard fails the negative control and nothing else. So the suite is not passing by finding nothing.
Checks
scripts/review-checks.shon the child-only diff: PASS.no replies awaiting deliveryat a moment when there genuinely were none.src/workspace_default.resolve_workspace, not a hardcoded path;--workspaceexists for tests.Scope
Operational tooling for an active incident — separate capability from #3382 (which makes the
task-queueprobe pool-aware). Both come out of the same night; neither depends on the other.