Repository navigation
feat(audio-transcribe): skill manifest + fix empty-file race in bridge result_watchers - #1874
Conversation
|
Codex triage findings (detail for the REQUEST_CHANGES review that follows): bridge-result-race-guard.test.py:46 is a source-grep structural test (auto-fail). The empty-file race guard placement looks correct - rewrite the test as behavior (real tmpdir, real empty file, real pending_replies.pop path).
|
liususan091219
left a comment
There was a problem hiding this comment.
Codex triage - see my comment above. - Maddy
john-the-dev
left a comment
There was a problem hiding this comment.
APPROVED. Manifest + SKILL.md follow the established convention. The empty-file race fix (check os path.getsize before marking a result as ready) is correct and aligns with the same pattern used in other bridge result watchers.
|
Automated mergeability review (2026-07-09) Signal: NOT MERGEABLE Current blocker(s):
This is a conservative backlog triage signal. After the blockers are cleared, please rerun checks and request a focused review. |
liususan091219
left a comment
There was a problem hiding this comment.
Triage change-request (owner-approved batch, 2026-07-12 sweep). Conditions found on the current diff:
- inadequate tests (source-grep, not behavior) — Replace structural source assertions with behavior test for empty result file race
- live-test evidence required (voice/vision policy) — Add live bridge voice transcript or repro evidence for Slack/Discord/Telegram watcher change
Justification: kb triage standards (source-grep test standard, voice/vision live-test policy, necessity/coverage checks) — see sonichi/sutando-pr-triage kb/.
Address the asks and push; re-request review to clear this. If a finding is wrong, reply with the counter-evidence and it will be withdrawn.
cr-reason: live_test_required+source_grep_tests
liususan091219
left a comment
There was a problem hiding this comment.
Triage change-request (owner-approved batch, 2026-07-12 sweep). Conditions found on the current diff:
- inadequate tests (source-grep, not behavior) — Replace structural source assertions with behavior test for empty result file race
- live-test evidence required (voice/vision policy) — Add live bridge voice transcript or repro evidence for Slack/Discord/Telegram watcher change
Justification: kb triage standards (source-grep test standard, voice/vision live-test policy, necessity/coverage checks) — see sonichi/sutando-pr-triage kb/.
Address the asks and push; re-request review to clear this. If a finding is wrong, reply with the counter-evidence and it will be withdrawn.
cr-reason: live_test_required+source_grep_tests
|
Daily reminder — this PR has an open change-request from 2026-07-03 (9d) with no commits since. Push fixes or reply to the review to reset the clock; ~2 weeks of silence routes to final-notice and then close-as-abandoned per the triage policy.
|
Dismissing exact duplicate of the same sweep review (retry double-post); the original stands.
liususan091219
left a comment
There was a problem hiding this comment.
Requesting changes: this PR adds _WORKSPACE_SRC = Path.home() / ".sutando" / "workspace" / "src" — an inline ~/.sutando/workspace literal. Note that path is the deprecated legacy default (v0.8 / #1440; workspace is now <repo>/workspace/, resolved via sutando_config), so it should go through the resolver rather than hardcode the pre-M0 path. More broadly: maintainer cannot decide because it's unclear whether ~/.sutando/repo is legitimate. Until that's resolved, holding review on new ~/.sutando/* install-path references. Route through sutando_config / resolve_workspace() instead.
|
Daily reminder — this PR has an open change-request from 2026-07-03 (11d) with no commits since. Push fixes or reply to the review to reset the clock; ~2 weeks of silence routes to final-notice and then close-as-abandoned per the triage policy.
|
|
Daily reminder — this PR has an open change-request from 2026-07-03 (12d) with no commits since. Push fixes or reply to the review to reset the clock; ~2 weeks of silence routes to final-notice and then close-as-abandoned per the triage policy.
|
Adds manifest.json (version 1.0.0, free tier) so the skill is discoverable by the skill-installer. Adds frontmatter to SKILL.md for consistency with other skills that expose name/description metadata. Also adds tests/bridge-result-race-guard.test.py — structural regression test confirming all 3 bridges (slack/discord/telegram) guard against the empty-file race where a bridge polls a result file before the write completes and archives it empty, permanently losing the reply. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a result file is written via shell redirect (> file), the OS creates the file empty before the process writes content. The bridge result_watcher polls every ~2s; if it fires during that window it reads an empty string, skips the send, but still pops pending_replies and archives the file — permanently losing the reply with no error logged. Fix: add `if not reply_text: continue` after read_text().strip() and BEFORE the irreversible .pop() in all three bridge result_watcher loops (slack-bridge.py, telegram-bridge.py, discord-bridge.py). Regression test: tests/bridge-result-race-guard.test.py (structural check added in preceding commit) verifies the guard is present and ordered correctly in all 3 bridges. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXQJogmVSdYrKtYX1LwzwT
Two reviewer findings addressed: 1. "inadequate tests (source-grep, not behavior)" — added a behavioral layer that extracts the real reply_text/guard/continue lines out of each bridge's source (not a hand-copied reimplementation, same technique as tests/watch-tasks-stream-trap-exit.test.sh) and executes them against a real empty-then-filled result file, asserting the guard actually fires on empty/whitespace-only content and actually clears on real content. Verified this catches a real regression: with the guard temporarily stripped from telegram-bridge.py, the suite fails (structural check + a hard error from the extractor no longer finding the guard block) — restored before committing. 2. "~/.sutando/workspace hardcoded literal" — the test's preference for testing against a live workspace-symlinked copy now goes through workspace_default.resolve_workspace() instead of hardcoding the deprecated pre-M0 default path. Also rebased onto current main (was 100+ commits behind); resolved an add/add conflict on skills/audio-transcribe/manifest.json by keeping main's already-shipped manifest (every other skill on main uses that name/version/owner/license/stability/description/provenance schema — this PR's version/tierRequired/priceCredits/categories schema never became the convention). The SKILL.md frontmatter addition and the bridge race-guard fix itself are unchanged.
444158e to
92c4dc6
Compare
|
@liususan091219 — addressed all three findings: 1. "inadequate tests (source-grep, not behavior)" — added a behavioral layer alongside the existing structural check. It extracts the actual 2. "~/.sutando/workspace hardcoded literal" — fixed. The test's preference for a live workspace-symlinked copy of the bridge source now goes through 3. "live-test evidence required (voice/vision policy)" — this PR doesn't touch voice or vision code; it's a text-file-existence/content race in the result-polling loop. The new behavioral test above IS the closest available repro: it reproduces the exact race (file exists-but-empty → guard fires → content lands → guard clears) against the real production code path, not a mock. If there's a live-bridge repro specifically expected here beyond that, let me know what it should demonstrate that the behavioral test doesn't. Also rebased onto current |
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 51%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
…lint The comment explaining why the workspace path is resolved (not hardcoded) itself contained the literal ~/.sutando/... string it was warning against, which scripts/lint-sutando-home-path.sh flags on any added line regardless of context (comment vs code). Rephrase without losing the explanation.
…tion CI's diff-coverage gate flagged the 2 changed lines in slack-bridge.py and discord-bridge.py as uncovered (telegram-bridge.py's happened to be covered by an unrelated existing test) — the behavioral harness ran the extracted snippet via exec() in an anonymous namespace, which is invisible to coverage.py regardless of which real lines it mirrors. compile() the snippet with the real bridge file as `filename` and padded with leading blank lines so it lands at its TRUE line numbers in that file, instead of an anonymous exec namespace — coverage.py now credits the run against the actual source. Also fixes a bug introduced by the previous refactor: _REACHED_PAST_GUARD wasn't seeded in the exec namespace before running an "empty file" case (where the guard's `continue` means the sentinel append line never executes), causing a KeyError instead of the intended False result.
|
LGTM. I ran Qingyun's Personal Codex Agent |
|
Maintainer verification of the empty-file race guard — real code + real on-disk files, no mock.
Before the fix, the empty read would
Confirms the fix end-to-end. Clearing |
|
@bassilkhilo-ag2 the empty-file race guard is verified and its |
|
Withdrawing my 2026-07-17 02:44Z ask — it was stale. You had already replaced the hardcoded State on current head:
All three findings (path literal, source-grep tests, live-test) are addressed and the |
liususan091219
left a comment
There was a problem hiding this comment.
Auto re-review (author pushed after the change-request):
- ask 1: ADDRESSED — Current diff contains no
_WORKSPACE_SRC,~/.sutando/*, or workspace install-path references; off
All asks addressed — owner: this one is ready for your re-review 👀.
john-the-dev
left a comment
There was a problem hiding this comment.
CR (07-15) addressed by test commits 07-16; CI green. LGTM. (Approving on owner instruction; note: a stale CR-sweep CHANGES_REQUESTED may need dismissing to merge.)
Dismissing stale change-request: findings addressed by subsequent commits (verified in owner-directed triage 2026-07-17; see approval review). Dismissed on owner instruction to merge approved PRs.
|
@cla-assistant check |
…y reply
`_post_ready_results` treated `rfile.exists()` as the readiness signal, so a
result path that existed but had not been written yet produced `body == ""`.
The gateway POSTed that empty body to the room and archived the task as
delivered. The core's real answer landed a second later and was orphaned on
disk forever, because the task was no longer in flight.
Observed on AG2 Space first-run onboarding ("Testing the gateway"): the step
waits for a timeline event, never sees one, and times out at 120s with no error
message. Retry then carried `[deduped: <first-task-id>]`, which the gateway
honours by delivering nothing — so once one empty delivery happened, retry
could never win. Three identical prompts, zero replies; a different prompt was
answered normally, proving the pipeline itself was healthy.
The window is reachable because the core is an LLM driving a shell: it writes
results correctly (temp file + atomic rename) but will `touch` the destination
for unrelated reasons — observed touching it to hang a symlink off the results
directory so it could list them. Any such touch published an empty result. The
guard belongs gateway-side, since the gateway is what decides "ready to send".
sonichi#1874 added this same guard to the discord/slack/telegram bridges and their
shared race-guard regression test; this path was outside that PR's scope and
kept the bug.
A genuinely empty reply is expressed with [no-send], which parse_markers
already handles on the line below, so the guard cannot swallow a real answer.
Dedupe-after-failed-delivery is the compounding second defect and is NOT fixed
here — it needs its own change. AG2 Space has shipped a desktop-side mitigation
(unique per-attempt prompt) that works around it.
… replies `_post_ready_results` in the gateway treated file existence as readiness, so an unwritten result posted an empty body and archived the task as delivered. The core's answer landed a second later and was orphaned, and the retry was deduped against the failed delivery, so AG2 Space onboarding had no way to succeed. The gateway was the only one of four delivery consumers without the guard. The other three each carried their own copy, added by sonichi#1874, which is how the gateway drifted: nothing tied them together, so a consumer added later simply did not get one. Extracts the readiness question into src/result_ready.py and points every delivery consumer at it. Adapters keep only provider-specific delivery. The module is bundled into ag2-sparrow through the existing sync_from_src mechanism rather than hand-copied, so the package cannot drift from src/. tests/bridge-result-race-guard.test.py is removed: its structural layer asserted that each bridge contains the duplicated read-and-check, which forbids this extraction. tests/result-ready-contract.test.py replaces it and is broader — it covers the shared owner's behaviour directly, all four consumers including the gateway that the old test never checked, and fails if any consumer reintroduces a local copy under any variable name. Not included: the proactive delivery path in the three bridges reads result files with the same question but a different action (unlink on empty rather than retry, which can discard a message). Same duplication, different policy; it needs its own change.
… replies `_post_ready_results` in the gateway treated file existence as readiness, so an unwritten result posted an empty body and archived the task as delivered. The core's answer landed a second later and was orphaned, and the retry was deduped against the failed delivery, so AG2 Space onboarding had no way to succeed. The gateway was the only one of four delivery consumers without the guard. The other three each carried their own copy, added by sonichi#1874, which is how the gateway drifted: nothing tied them together, so a consumer added later simply did not get one. Extracts the readiness question into src/result_ready.py and points every delivery consumer at it. Adapters keep only provider-specific delivery. The module is bundled into ag2-sparrow through the existing sync_from_src mechanism rather than hand-copied, so the package cannot drift from src/. tests/bridge-result-race-guard.test.py is removed: its structural layer asserted that each bridge contains the duplicated read-and-check, which forbids this extraction. tests/result-ready-contract.test.py replaces it and is broader — it covers the shared owner's behaviour directly, all four consumers including the gateway that the old test never checked, and fails if any consumer reintroduces a local copy under any variable name. Not included: the proactive delivery path in the three bridges reads result files with the same question but a different action (unlink on empty rather than retry, which can discard a message). Same duplication, different policy; it needs its own change.
…2695) * fix(results): one owner for result readiness; gateway delivered empty replies `_post_ready_results` in the gateway treated file existence as readiness, so an unwritten result posted an empty body and archived the task as delivered. The core's answer landed a second later and was orphaned, and the retry was deduped against the failed delivery, so AG2 Space onboarding had no way to succeed. The gateway was the only one of four delivery consumers without the guard. The other three each carried their own copy, added by #1874, which is how the gateway drifted: nothing tied them together, so a consumer added later simply did not get one. Extracts the readiness question into src/result_ready.py and points every delivery consumer at it. Adapters keep only provider-specific delivery. The module is bundled into ag2-sparrow through the existing sync_from_src mechanism rather than hand-copied, so the package cannot drift from src/. tests/bridge-result-race-guard.test.py is removed: its structural layer asserted that each bridge contains the duplicated read-and-check, which forbids this extraction. tests/result-ready-contract.test.py replaces it and is broader — it covers the shared owner's behaviour directly, all four consumers including the gateway that the old test never checked, and fails if any consumer reintroduces a local copy under any variable name. Not included: the proactive delivery path in the three bridges reads result files with the same question but a different action (unlink on empty rather than retry, which can discard a message). Same duplication, different policy; it needs its own change. * fix(proactive): an unready claim was deleted, discarding the message Bridges claim `proactive-*.txt` by renaming it to `.sending`, then read it. On an empty read all three unlinked the claim. The file is already renamed at that point, so nothing recovers it: a proactive message still being written is destroyed rather than delivered. The empty read is routine. Results are written with `cat > path << EOF`, and `>` truncates at open, so a file is briefly present-and-empty on every normal write. Releases the claim instead — rename back to `.txt` so a later pass delivers it. That is the same restoration `recover_orphan_sending_files` performs at startup, so the policy now lives beside it in proactive_recovery and all three bridges delegate rather than carrying a copy. Readiness delegates to result_ready, added by the parent PR. Release refuses to overwrite an existing `.txt`, so a newer proactive file under the same name still wins. --------- Co-authored-by: Qingyun Wu <qingyun0327@gmail.com>
… replies (#2693) `_post_ready_results` in the gateway treated file existence as readiness, so an unwritten result posted an empty body and archived the task as delivered. The core's answer landed a second later and was orphaned, and the retry was deduped against the failed delivery, so AG2 Space onboarding had no way to succeed. The gateway was the only one of four delivery consumers without the guard. The other three each carried their own copy, added by #1874, which is how the gateway drifted: nothing tied them together, so a consumer added later simply did not get one. Extracts the readiness question into src/result_ready.py and points every delivery consumer at it. Adapters keep only provider-specific delivery. The module is bundled into ag2-sparrow through the existing sync_from_src mechanism rather than hand-copied, so the package cannot drift from src/. tests/bridge-result-race-guard.test.py is removed: its structural layer asserted that each bridge contains the duplicated read-and-check, which forbids this extraction. tests/result-ready-contract.test.py replaces it and is broader — it covers the shared owner's behaviour directly, all four consumers including the gateway that the old test never checked, and fails if any consumer reintroduces a local copy under any variable name. Not included: the proactive delivery path in the three bridges reads result files with the same question but a different action (unlink on empty rather than retry, which can discard a message). Same duplication, different policy; it needs its own change. Co-authored-by: Rui Wang <52230987+john-the-dev@users.noreply.github.com>
What
Three changes in two commits:
skills/audio-transcribe/manifest.json— adds skill manifest (version: 1.0.0,tierRequired: free) so the skill is discoverable by the skill-installer.skills/audio-transcribe/SKILL.md— adds YAML frontmatter (name,description) for consistency with other skills.src/slack-bridge.py,src/discord-bridge.py,src/telegram-bridge.py) — addsif not reply_text: continueguard in each bridge'sresult_watcherloop, before the irreversible.pop()onpending_replies.The race bug (commit 3)
When a result file is written via shell redirect (
> file), the OS creates it empty before content is written. The bridge polls every ~2s; if it fires during that window:reply_text = result_file.read_text().strip()→""pending_replies.pop(task_id)→ removes the mapping (irreversible)Fix: add
if not reply_text: continueBEFORE.pop(). The file stays; the next poll (2s later) finds it non-empty and sends.Bug surface: active since at least PR #1453 (voice note bridging). Highest-risk path: Slack and Telegram where long Gemini responses can lag the redirect.
Test
tests/bridge-result-race-guard.test.py— structural regression test added as part of this PR. Checks all 3 bridges for the guard in the correct position (after read, before pop).Checklist
continuenotreturn(stays in the watcher loop)🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01KXQJogmVSdYrKtYX1LwzwT