Skip to content

Dead-letters section and Requeue button (vikunja#557) - #9

Merged
TadMSTR merged 2 commits into
mainfrom
feat/dead-letters-view
Aug 29, 2026
Merged

TadMSTR merged 2 commits into
mainfrom
feat/dead-letters-view

Conversation

@TadMSTR

@TadMSTR TadMSTR commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Build agent-workflow-interop-2026-08 Phase 1. Pairs with task-queue-mcp#18 (v0.10.0), which owns the queue-side half and the operator-only POST /tasks/{id}/requeue route this calls.

Why

~/.claude/task-queue/dead-letters/ is written by task-dispatcher when a task exhausts its routing retries. Nothing could show it: the MCP's get_task answered not found, and this plugin globbed the queue root only.

Seventeen tasks sat there between 2026-05-29 and 2026-07-25 — every one a security audit request, all seventeen carrying the identical failed_reason — with no interface anywhere in the fleet able to display the number.

This does not fix the bug that produced them (vikunja#63/#169).

What

  • GET /dead-letters — read-only, reading dead-letters/*.yml directly like every other read here. No new manifest permission, no new env var.
  • panels/dead-letters.ts — the section.
  • dead-letters.ts — record shaping and reason-grouping as pure, unit-tested functions.
  • Requeue button → new requeue control action → the MCP's operator-only route.

Three decisions

  • Collapsed, and a section rather than a tab. The healthy count is zero, so a tab would be permanently empty furniture and an always-open block a permanent scroll cost. What it must never be is absent: the heading renders whatever the count is, including none, and turns red the moment it is not.
  • Grouped by failed_reason. Seventeen records with one identical reason are one bug that fired seventeen times. Seventeen sibling rows read as seventeen unrelated problems — roughly how they were treated for three months.
  • The count loads on every refresh, not on expand. A number that only appears once the operator opens the section is a number nobody sees, which is the exact failure this surface exists to end.

Requeue is confirmed before firing, and the confirmation says the part that matters: it does not fix why the task was dropped, so if the cause is live it comes back.

A drift gate the invariant was missing

AGENTS.md says the ControlAction union, server.ts's route regex, and the MCP's custom routes are "three copies of one contract" — and nothing checked it. Adding an action to the union alone compiles; the failure mode is a button that 404s against the plugin's own backend. The two copies in this repo are now pinned to each other by a source-level test. Verified to fail in both directions (drop requeue from the regex → red; drop it from the union → red).

Handled edge cases

Case Behaviour Why
record has no id skipped, not rendered a row that cannot be addressed is a row whose Requeue button could not work
unparseable failure timestamp renders — ago() maps an invalid date to "just now", which would claim a three-month-old drop happened seconds ago
created is a Date serialised, not string-coerced js-yaml parses every real record's unquoted timestamp into a Date
task lands in dead-letters/ no WS event fs.watch is not recursive on Linux; deliberate, and documented — the dispatcher's unlink from the queue root does fire

Verification

npm run build (tsc gate) clean; 85 tests pass. Ran dist/server.js against the live queue:

GET /dead-letters → 17 records
reasons: {"Invalid or missing build_name in payload: 'unknown'": 17}
agents:  {"security": 17}

Not yet deployed — needs ./deploy.sh && pm2 restart cloudcli after merge, and the paired task-queue-mcp container rebuild for the Requeue button's route to exist.

🤖 Generated with Claude Code

developer-agent added 2 commits August 29, 2026 07:28
~/.claude/task-queue/dead-letters/ is written by task-dispatcher when a task
exhausts its routing retries, and nothing could show it — the MCP's get_task
answered `not found` and this plugin globbed the queue root only. Seventeen
tasks accumulated there between 2026-05-29 and 2026-07-25, every one a security
audit request, all seventeen carrying the identical failed_reason, and the only
notice any got was one Matrix message at the moment it was dropped.

Pairs with task-queue-mcp v0.10.0. Does not fix the bug that produced the
seventeen — that is vikunja#63/#169.

- GET /dead-letters, read-only, reading the YAML directly like every other read.
- panels/dead-letters.ts — collapsed, not a tab: the healthy count is zero, so a
  tab would be permanently empty furniture. The heading renders the count
  whatever it is, including `none`, and turns red when it is not. The count
  loads on every refresh rather than on expand; a number that appears only after
  the operator opens the section is a number nobody sees.
- dead-letters.ts — shaping and reason-grouping as pure, tested functions.
  Grouped by failed_reason: 17 records with one identical reason are ONE bug
  that fired 17 times, and 17 sibling rows read as 17 unrelated problems.
- Requeue button → the new `requeue` control action → POST /tasks/:id/requeue.
  Confirmed before firing, and the confirmation says the part that matters:
  requeueing does not fix why the task was dropped.
- A source-level drift gate pinning the ControlAction union to server.ts's
  mutation route regex. AGENTS.md called these "three copies of one contract"
  and nothing checked it; adding an action to the union alone compiles, and the
  failure mode is a button that 404s against the plugin's own backend. Verified
  to fail in both directions.

Verified against the live queue: GET /dead-letters returns all 17, one reason
group, all targeting security.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

agent-id: developer
Audit 2026-08-29 (agent-workflow-interop-2026-08-phase1): no findings in this
repo. Worth recording what the auditor verified structurally rather than by
test — that toDeadLetter() builds its response from an explicit field allowlist
and never references `payload`, making GET /dead-letters strictly narrower than
the existing GET /tasks, and that renderRow() escapes every interpolated value
while routing summary and reason through textContent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

agent-id: developer
@TadMSTR
TadMSTR merged commit 08150bd into main Aug 29, 2026
2 checks passed
@TadMSTR
TadMSTR deleted the feat/dead-letters-view branch August 29, 2026 11:46
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.

1 participant