Repository navigation
fix(task-bridge): dedup work() submissions with identical text in 2-min window - #29
VasiliyRad wants to merge 1 commit into
Conversation
…in window Port upstream PR sonichi#820 (sonichi/sutando) into this fork. AG2Platform/stando never received the fix; the merge that brought the surrounding code did not include the dedup branch, leaving voice over-delegation uncontained: Gemini's tendency to retry-call work() with the same task text 3-5 times in sub-second succession was spawning that many parallel task files, each running independently and shipping its own result DM (the "cascade of duplicate result DMs" symptom called out in sonichi#820's commit message). Fix structure mirrors upstream: - `PendingTask` grows a `taskText: string` field so the in-memory map can detect duplicates without re-reading task files from disk. - `DEDUP_WINDOW_MS = 2 * 60 * 1000` module constant. - `normalizeTask(t)` = lowercase + collapse whitespace + trim + cap at 150 chars. Catches the rewording-noise that triggers the over- delegation pattern (Gemini retrying with extra spaces, different casing, trailing punctuation past the cap). - `workTool.execute` scans `_pendingTasks` for a normalized-match entry submitted within DEDUP_WINDOW_MS. On hit: return `{ status: "duplicate", taskId, message }` instead of writing a new task file. The message explicitly tells the agent NOT to retry. Tests: `tests/task-bridge-work-dedup.test.ts` (3 cases) — true integration via `workTool.execute(...)` against the real `SUTANDO_WORKSPACE/tasks/` dir. Uses the same task-file leak-check harness as `tests/task-bridge-format.test.ts` so a future bug that writes orphan files is caught. - Second identical call → duplicate response with first's taskId. - Whitespace + case variation still dedupes (normalizer pin). - Distinct task texts each produce fresh taskIds (no false positive). Local test run: 3/3 pass + 12/12 existing task-bridge tests (`tests/task-bridge-format.test.ts`, `tests/task-bridge-isvoice.test.ts`) still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Clean port. A few observations:
Logic: The dedup scan is O(n) over _pendingTasks. In practice this map stays small (tasks expire within their timeout window, typically ≤10 min), so no concern. One micro-nit: normalizeTask(pending.taskText) is recomputed on every loop iteration. Could cache as pending.normalizedText at insertion time, but the savings are negligible here.
Return value: The message field explicitly tells the model not to retry — good anti-footgun. The caller at workTool.execute returns { status: 'duplicate', taskId, message } while a fresh task returns just { taskId, ... } (no status field). Make sure all callers that pattern-match on status === 'duplicate' are covered — the test checks this correctly.
Test isolation gap (non-blocking): afterEach cleans up task files but doesn't reset _pendingTasks (module-level). Each test's first submission stays in the map for subsequent tests. The three cases here use distinct texts so no collision, but future test authors who reuse a prior case's text will see unexpected dedup. Worth adding _pendingTasks.clear() in afterEach — requires exporting the map or a _resetForTest() helper from task-bridge.ts.
Tests: Core regression case, normalization variant, and non-collision case all covered. Leak harness borrowed from the existing format-test pattern — good consistency.
|
Code review: LGTM. Clean port of upstream PR sonichi#820 — Not merged — PR is CONFLICTING with |
|
Closing — current 's |
Summary
Bug-by-omission. Upstream
sonichi/sutandoshipped PR sonichi#820 (fix(task-bridge): dedup work() submissions with identical text within 2-minute window) on 2026-05-18; the merge that pulled the surrounding code into AG2Platform/stando didn't include this branch, leaving voice over-delegation uncontained here.Symptom: Gemini retrying
work()3-5 times with identical task text in sub-second succession spawns N parallel task files, each runs independently, each ships its own result DM — the "cascade of duplicate result DMs" the upstream commit calls out.Fix
Mirrors upstream structure:
PendingTaskgrows ataskTextfield so dedup can match against in-memory state without re-reading task files.DEDUP_WINDOW_MS = 2 * 60 * 1000module constant.normalizeTask(t)= lowercase + collapse whitespace + trim + cap at 150 chars. Catches the rewording-noise that triggers the over-delegation pattern (Gemini retrying with extra spaces, casing changes, trailing punctuation past the cap).workTool.executescans_pendingTasksfor a normalized-match entry submitted withinDEDUP_WINDOW_MS. On hit, returns{ status: "duplicate", taskId, message }and skips the task-file write. The message explicitly tells the agent NOT to retry.Tests
tests/task-bridge-work-dedup.test.ts— 3 integration cases. Invokes the realworkTool.execute(...)against the realSUTANDO_WORKSPACE/tasks/dir; uses the same task-file leak-check harness astests/task-bridge-format.test.tsso any future regression that writes orphan files is caught.second identical call within window returns duplicate with first taskIddedup ignores whitespace and case differencesnormalizeTaskpin — Gemini's retry rewording variants must still collide.different task texts each produce a fresh taskId3/3 pass locally; existing 12 task-bridge tests (
*-format,*-isvoice) still pass.Bug-finding calibration
1 real bug (port-gap) fixed across 3 tests. Total this round: 7 bugs / ~90 tests ≈ 1:13.
Test plan
npx tsx --test tests/task-bridge-work-dedup.test.ts— 3/3 passnpx tsx --test tests/task-bridge-format.test.ts tests/task-bridge-isvoice.test.ts— 12/12 pass (no regressions)npm run typecheck— cleanworkTool.executerapidly with identical text; verify only ONE task file lands intasks/🤖 Generated with Claude Code