Skip to content

fix(agent-api): reject non-task- ids in /task-done to stop Task list pollution#1875

Open
bassilkhilo-ag2 wants to merge 2 commits into
sonichi:mainfrom
bassilkhilo-ag2:fix/agent-api-task-done-id-guard
Open

fix(agent-api): reject non-task- ids in /task-done to stop Task list pollution#1875
bassilkhilo-ag2 wants to merge 2 commits into
sonichi:mainfrom
bassilkhilo-ag2:fix/agent-api-task-done-id-guard

Conversation

@bassilkhilo-ag2

@bassilkhilo-ag2 bassilkhilo-ag2 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Closes #1786.

Root cause

task-bridge.ts _shouldFallthrough (L187) matches task-, voice-, and proactive- result files — all three pass to the /task-done POST handler. Since voice-* and proactive-* files carry per-fire timestamp ids, each re-fire creates a new task_history entry, accumulating duplicate rows in the web Task list (one per reminder fire, per proactive ping, etc.).

Note: #1788 (merged) already closes this at the source — task-bridge.ts:750 gates the /task-done POST itself behind if (taskId.startsWith('task-')), so voice-*/proactive-* no longer reach this endpoint from the primary caller. This PR is the server-side belt-and-suspenders half: it makes /task-done refuse non-task-* ids regardless of caller, so a second caller (or a future regression in the TS-side gate) can't reintroduce the pollution.

Fix

One early-return guard in the /task-done handler, before any task_history write:

if not tid.startswith("task-"):
    self.send_json(200, {"ok": True})
    return

The caller (task-bridge.ts) receives 200 and is satisfied. The guard prevents voice-* and proactive-* ids from reaching task_history. Genuine task-* ids are unaffected.

Why here (not at task-bridge.ts)

The issue description (Reproduce C) traced the write to /task-done do_POST (~L697) — this is the correct layer. Fixing at the ingestion point is the smallest targeted change with no side-effects on the task-bridge caller, and provides defense-in-depth independent of the TS-side gate in #1788.

Test

tests/agent-api-task-done-id-guard.test.py — structural check:

  1. Guard exists: if not tid.startswith("task-"):
  2. Guard returns 200 + returns before task_history write
  3. task_history[tid] write follows the guard (correct ordering)
  4. Guard is inside the /task-done handler
python3 tests/agent-api-task-done-id-guard.test.py
PASSED: /task-done id guard present and correctly ordered

🤖 Generated with Claude Code

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01KXQJogmVSdYrKtYX1LwzwT

…st pollution (closes sonichi#1786)

task-bridge.ts feeds voice-* and proactive-* result files through the
/task-done endpoint (they all pass _shouldFallthrough's match set). Since
each notification file carries a per-fire timestamp id, every re-fire adds
a NEW row to the in-memory task_history dict — duplicate rows pile up in
the web Task list with each pending-question reminder or proactive ping.

Fix: add an early-return guard before the task_history write in the
/task-done handler:

    if not tid.startswith("task-"):
        self.send_json(200, {"ok": True})
        return

The 200 response keeps the caller happy; the guard prevents voice-* and
proactive-* ids from ever entering task_history. Genuine task-* ids are
unaffected.

Structural regression test added: tests/agent-api-task-done-id-guard.test.py
verifies the guard exists, returns 200, and precedes the task_history write.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXQJogmVSdYrKtYX1LwzwT
@qingyun-wu

Copy link
Copy Markdown
Collaborator

Cold-review — LGTM, one context note. Server-side validation at /task-done is the right instinct (don't trust the caller), the guard is correct (200 + early-return before any task_history write), and it closes #1786.

Context worth noting: #1788 (merged) already gates the /task-done POST on the caller side — task-bridge.ts:750 wraps the fetch in if (taskId.startsWith('task-')), so voice-*/proactive-* no longer reach this endpoint from the primary caller. That makes this PR defense-in-depth rather than the primary fix. That's still valuable (a second caller or a future regression in the TS gate can't pollute the Task list), but the root-cause section describes the pre-#1788 flow — worth a line clarifying it's the belt-and-suspenders server-side half so reviewers don't think the TS gate is missing.

Nit (non-blocking): the test is structural (regex-greps agent-api.py for the guard text + ordering). It'll go red if someone refactors tid.startswith("task-") into a helper even when behavior is unchanged. A behavioral test (POST a proactive-<ts> id, assert no new task_history row) would be more robust — though the structural style does match the repo's existing "refuse new X" checks, so not a blocker.

Also: mergeState is BEHIND — needs an update-branch before merge. Net: sound, mergeable after a rebase.

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

@cla-assistant check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pending-question reminders duplicate in the Task list — task-bridge registers proactive-/voice- notification files as tasks

2 participants