* feat(orchestrator): carry sender_task metadata through user message paths
Add WithSenderTask builder to UserMessageMeta and a metadata-aware
QueueMessageWithMetadata variant on the message queue. The drain path
in event_handlers_agent merges queued metadata into the recorded user
message so attribution survives the queue → DB transition.
* feat(mcp): attribute message_task_kandev calls to the sending agent
The MCP server inside agentctl now injects sender_task_id and
sender_session_id from its own server context. The backend handler
validates the sender (rejecting missing IDs, self-messages, and unknown
tasks), looks up the sender title, wraps the prompt in a <kandev-system>
attribution block, and propagates structured metadata across all three
delivery paths (queue, created-session, record+prompt). The wrapped
content lets the receiving agent identify the peer both live and on ACP
session resume; the existing sysprompt strip pipeline hides the wrapper
from the UI.
* feat(web): render sender-task badge on cross-task agent messages
User messages whose metadata carries sender_task_id now render a
clickable purple badge above the bubble, linking to the source task.
The badge live-resolves the sender's title from the kanban store and
falls back to a greyed snapshot when the task isn't loaded
(cross-workspace, deleted, etc.). Adds a non-subscribing useTaskById
selector so badges don't fan out WS subscriptions per message.
* test(e2e): cover cross-task agent message attribution
Eight scenarios driven through the mock-agent's MCP script directive:
queue path, prompt path, self-message rejection, unknown-target error,
badge link target, live title re-resolution after rename, kandev-system
wrapper preservation in raw_content, and prompt bodies containing their
own kandev-system tags. Adds an updateTaskTitle helper to ApiClient.
* fix: address CodeRabbit + Greptile P1 findings on PR #819
- requeueMessage now forwards queuedMsg.Metadata via QueueMessageWithMetadata
so sender attribution survives transient delivery failures and retries.
- CREATED-session branch in handleMessageTask records the user message AFTER
StartCreatedSession succeeds, so a launch failure no longer leaves an
orphaned attributed chat row.
- SenderTaskBadge: collapse identical displayTitle/fullTitle locals.
- E2E: extract waitForTargetIdle helper; replace the 1.5s raw sleep in the
collision-check test with the same poll the rest of the suite uses.
* fix: address Claude review suggestions on PR #819
- wrapAgentMessage strips </kandev-system> from the embedded sender title
before formatting. Without this, a title containing the closing tag
would short-circuit the non-greedy strip regex and leak the attribution
tail into the visible UI bubble. Metadata snapshot keeps the original
title so the badge still displays it.
- Add a regression test covering the malicious-title case.
- Document the deliberate cross-workspace messaging policy on the sender
lookup so future readers don't add a workspace-scope check (Q13 of the
feature plan locked this as intentional).
* fix(web): exclude permission_request from turn-complete safety net
The session.turn.completed handler iterated over messages and forced
metadata.status="complete" on anything carrying tool_call_id. Permission
prompts also carry tool_call_id (for FE pairing with their tool_call),
so this loop overwrote the user's "approved" decision on every prompt
in the turn — and the FE component re-rendered the Approve/Deny buttons.
* fix(backend): protect permission_request rows from tool_call sweeps
Three permission_request invariants the prior code violated, mirroring
the FE bug fix:
- CompletePendingToolCallsForTurn: SQL UPDATE filtered by tool_call_id
presence, also catching permission_request rows. Add type filter so
the safety net only touches actual tool messages.
- GetMessageByToolCallID: returned the oldest row carrying that
tool_call_id, which races to the permission_request when timing
shifts. Exclude permission_request type so a tool_update can never
land on the user's approve/reject row.
- applyToolCallMessageUpdate: defensive guard that refuses to overwrite
a permission_request, making the invariant explicit at the layer that
does the writing.
Adds a regression test for CompletePendingToolCallsForTurn covering
approved/rejected/pending permission rows.
* test(e2e): cover approval-reappear regression with mock-agent scenario
Adds a multi-permission mock-agent scenario (three sequential bash
tools, each requiring approval) and an E2E test that approves all
three and asserts no permission action row remains visible after
the agent's turn ends, and that the resolved state survives a reload.
Supporting test infrastructure:
- data-testid hooks on PermissionActionRow.
- permissionActionRows() / permissionApproveButtons() on SessionPage.
- backend fixture force-disables AGENTCTL_AUTO_APPROVE_PERMISSIONS=false
so a developer's local AUTO_APPROVE=true env doesn't silently bypass
the permission UI in tests that exercise the approve/reject flow.
* test(backend): cover GetMessageByToolCallID permission_request exclusion
Addresses claude-review feedback on the approvals fix:
- Adds a regression test that seeds both a permission_request and a
tool_call with the same tool_call_id (permission_request created
first, so an ORDER BY created_at ASC sweep without the type filter
would race to it) and asserts the lookup returns the tool_call row.
- Bumps the applyToolCallMessageUpdate invariant guard from Warn to
Error: reaching that branch means a caller bug, not a transient
condition, and Warn would silently swallow the violation.
* fix(e2e): scope auto-approve override to permission-approval test only
The previous fixture force-disabled AGENTCTL_AUTO_APPROVE_PERMISSIONS
globally, which broke session-isolation.spec.ts and other tests that
use scenarios with permission-required tools (e.g. /e2e:read-and-edit)
and rely on auto-approve to let the agent finish without UI clicks.
Default the env to "true" in the fixture; let permission-approval.spec
flip it to "false" via a backend.restart() override in beforeAll, then
restore in afterAll. Adds an envOverrides parameter to backend.restart.
* feat(quick-chat): eager-init agent on profile selection
Switch quick-chat backend from `IntentPrepare` to `IntentStart` so the
agent process boots immediately when the user picks a profile, instead
of waiting for the first prompt. The ACP `session/new` handshake fires
before the user types anything, so the slash menu, mode dropdown, and
model selector are populated up-front. Fixes the empty-slash-menu UX
in quick chat for ACP agents (OpenCode, Claude, Codex, etc.).
Frontend picker shows a per-card spinner + disables sibling cards
during boot. Rapid profile switches (and other picker-bypassing
actions like opening a new tab, switching tabs, or closing the modal)
supersede the in-flight start via a monotonic request id; the
resolved task is cleaned up via `deleteQuickChatTask` so we never
orphan an ephemeral task or its agent process.
Backend rollback uses a fresh background context so `DeleteTask`
runs even when the request context has already been cancelled by
the client.
`mock-agent` now emits `available_commands_update` from `NewSession`
and `LoadSession` (matching real OpenCode/Claude) so the eager-init
pipeline is exercised in e2e. Existing prompt-time emit kept as an
idempotent fallback.
* test+cleanup(quick-chat): unit tests for useAgentSelection; minor cleanups
- Add 5 unit tests for useAgentSelection covering happy path, rapid-pick
supersession, reset() during in-flight, and error toast vs silent-on-superseded.
Exercises the orphan-task cleanup that the eager-init flow depends on.
(claude bot review)
- Drop redundant isDisabled alias in AgentPickerView. (claude bot review)
- Drop unused ctx parameter from emitAvailableCommandsAfterDelay — it was
always called with context.Background() so the Done() branch was dead
code. (claude bot review)
- Re-format with prettier after the rebase.
* fix(quick-chat): use constants.TaskDeleteTimeout for rollback ctx
Match the other DeleteTask call sites in task_http_handlers.go (line 851)
instead of an ad-hoc 10s timeout. (claude bot review)