From 901205420f63195bf4de1f7e2a436ff6a9c18e79 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 18 Jul 2026 23:24:51 -0400 Subject: [PATCH] feat(kanban): talk to a running worker without a restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A running worker now polls its comment thread and folds new operator notes into the live turn via the OUT-OF-BAND steer channel (list_comments_after + a heartbeat-driven bridge, watermarked so history isn't re-injected and the worker's own notes are skipped). No block→comment→unblock dance. Desktop's composer sends notes live ("delivered within a few seconds") with "Requeue with note" as the restart option and a help tooltip. --- apps/desktop/src/plugins/kanban/drawer.tsx | 130 ++++++++++++++---- hermes_cli/kanban_db.py | 27 ++++ run_agent.py | 8 +- .../hermes_cli/test_kanban_comment_queries.py | 54 ++++++++ tests/tools/test_kanban_comment_injection.py | 124 +++++++++++++++++ tools/kanban_tools.py | 79 +++++++++++ 6 files changed, 393 insertions(+), 29 deletions(-) create mode 100644 tests/hermes_cli/test_kanban_comment_queries.py create mode 100644 tests/tools/test_kanban_comment_injection.py diff --git a/apps/desktop/src/plugins/kanban/drawer.tsx b/apps/desktop/src/plugins/kanban/drawer.tsx index a59a24105e6..a5b3909850e 100644 --- a/apps/desktop/src/plugins/kanban/drawer.tsx +++ b/apps/desktop/src/plugins/kanban/drawer.tsx @@ -279,7 +279,23 @@ function AssigneeMenu({ // Mirrors the review pane's commit-message field: one row tall to start // (button-height), CSS field-sizing grows it with content, button hugs the // bottom edge as it grows. -function CommentComposer({ onSubmit, pending }: { onSubmit: (body: string) => void; pending: boolean }) { +// +// On a RUNNING task the worker polls its comment thread and folds new notes +// into the live turn (OUT-OF-BAND steer), so a plain note reaches the agent +// mid-run within a few seconds — no block/unblock dance. `onRequeue` is the +// heavier option: post the note AND reclaim so the task restarts from scratch +// with the note in context (use when the current run has gone off the rails). +function CommentComposer({ + onRequeue, + onSubmit, + pending, + running +}: { + onRequeue?: (body: string) => void + onSubmit: (body: string) => void + pending: boolean + running?: boolean +}) { const [body, setBody] = useState('') const submit = () => { @@ -291,31 +307,53 @@ function CommentComposer({ onSubmit, pending }: { onSubmit: (body: string) => vo } } + const requeue = () => { + const trimmed = body.trim() + + if (trimmed && !pending && onRequeue) { + onRequeue(trimmed) + setBody('') + } + } + return ( -
-