acpmux: every Claude workflow of the daemon's chats in one list (_acpmux/workflows)

Merged
TE
12d ago
+451-1

Leo, 2026-10-24: "lawrence also sent some of this stuff with dynamic workflows claude "trajectories" ... we can use that to help think about how cmux-next/acpmux wants to expose/reveal/render dynamic workflows... we can look at claude code desktop and see how we can do better". Design: manaflow-ai/cmuxterm-hq#3720 and #3792.

It reads the runId, agents and usage that #24789 (landed) adds to a workflow's updates.

acpmux owner notice. plans/cmux-next/acpmux-audit.md names the acpmux owner as session cmux-4f (PL-acpmux). That session can't be reached from here, so this is the notice. The change adds a small index on the hub (hub/workflow_index.rs), fed from the tap's existing subagent annotation (hub/tap.rs), plus a list RPC and a Web refusal in remote_guard.

Risk: high by path (remote_guard, the tap). The tap gains one map update per subagent event, under a lock held only for that. The list is the app's and the unix socket's only, never the Web's.

What changed

A workflow is visible only inside the chat that started it. When several run at once, or the same one runs twice, nobody sees it. On one recent night with Claude Code workflows:

  • 6 to 8 were pending at once;
  • one run was resumed twice within 3 minutes;
  • three same-named rows differed only by their args.

The hub now keeps an index of every workflow on its chats (hub/workflow_index.rs).

  • Fed from what the tap already writes. The tap puts the subagent model into each live update: a spawn of kind: "workflow", its progress, and its end. The index folds those events, so nothing new is read. Session-load replays are skipped.
  • _acpmux/workflows {includeEnded?} → {workflows}.
    • Order: running workflows first, newest first, then the last 50 that ended.
    • Each row: sessionId, chatName, subagentSessionId, runId, name, task, args, state, reason (paused), agents {done, started, failed?, queued?}, tokens, toolUses, elapsedMs, action (current step), startedAt, updatedAt and endedAt.
    • Duplicates: two running workflows with the same name and the same known args, in any chat, list each other in duplicateOf. A run whose args aren't known is never a duplicate. acpmux doesn't send args yet; the resume-inputs follow-up sends them when they hold no secret.
    • A chat that is gone takes its workflows with it. A chat whose agent exits ends its running workflows as disconnected: they ran in that process and send no end of their own.
    • Bounds. At most 256 running rows are kept (the quietest go first). Ended rows are trimmed once every 50 ends, and the list shows the newest 50.
    • Locking. The index lock is held only to fold one update's events or to copy the rows. Duplicates are grouped in one pass over the copy.
  • Who may call it. The app and the unix socket. A Web client is refused (workflows.remote), since rows name chats and their workflows' args.
  • Capability workflows-index-v1. The protocol IR and the generated web method list include the method.

This is the daemon feed the gallery's workflow summaries (#24874) need for their duplicate and capacity warnings. It adds no UI.

Not in this PR. Peers' workflows (a fleet-wide list through each peer), and a change notification for live views. Rows will carry args once the resume-inputs follow-up sends them; that follow-up keeps args that may hold a secret out entirely.

No capture: a daemon change with no visible UI. Its only webview file is the generated protocol method list (protocol/generated/methods.ts), which adds one method name.

Merge review: Codex (xhigh, read-only, a different model) SHIP at c8702ea6cf59614f4343e88e679e21e975775803. The receipt is in the PR comments.

Review

An independent Codex review (xhigh, read-only) returned REVISE on three findings, all fixed in the second commit:

  • a workflow whose process died stayed "running" forever;
  • every update scanned all rows under the lock, and list's duplicate check was quadratic;
  • gone chats were forgotten once per row.

Codex confirmed the tap sees live updates only (replays excluded) and that the Web guard is right. Its merge review then blocked on duplicates by name alone while args are not sent yet; duplicates now need known args on both rows.

Testing

  • cargo test -p acpmux --lib -- hub::workflow_index hub::tap capabilities protocol at the head: 13 passed (at the rebased head, with hub::subagent_transcript and claude_stdio: 96) (protocol_ir_is_current included). Five tests are new:
    • a workflow is listed from its spawn to its end, and a plain subagent is not;
    • the same workflow running twice is a duplicate, until one ends, and never without known args;
    • only the latest ended workflows are kept, with running ones listed first;
    • a chat's exit ends its running workflows;
    • running rows are bounded too.
  • cargo test -p acpmux --test remote_guard: 13 passed. It now checks that the Web is refused the list and the app gets it.
  • cargo fmt --check is clean. cargo clippy -p acpmux --lib --tests -- -D warnings passes. The generated method list is current.

The change is daemon-only, so there's no UI capture.

Agent session: huge-blue

šŸ¤– Generated with Claude Code