Skip to content

fix(workflow): copying one preset three times yields three identical names, and a run by name silently takes the first #1446

Closed as not planned

Description

@HugoXK

Written-by: FlyBot Product

plugins/workflow/** belongs to the workflow module, not to us: this is a defect report. We do not implement it and will not open a PR for it.

What happens

Copying one factory preset three times produces three workflows with the identical name (发布交付流水线 副本). Names are the address used by run_workflow, by a board workflow:<name> token and by assignee: <name>; every one of those paths resolves with a first-match lookup, so a run started by name silently lands on the first copy and the other two can never be started by name at all. A user cannot tell the three rows apart in Settings either (same name, same preview), and cannot tell which one a run will use.

The same collision is reachable without the preset path: create never enforces a unique name, and rename does not either, so two hand-written workflows may share a name — same silent first-match routing.

Reproduction (in-process service, no host)

First run on b20524fe; re-checked on live main 5daf756b (2026-10-27T11:57:54Z), where plugins/workflow/src/service.js and match.js are unchanged and the probe gives the same shape (ids differ per run; the 5daf756 run: wf-96c7edaf / wf-b953ae5e / wf-e5cd96bd, only the first reachable by name).

const svc = createWorkflowService({ persist: false, now: () => 1_700_000_000_000, bots: () => roster })
const tpl = templateCatalog()[0]
for (let i = 0; i < 3; i += 1) svc.create({ template: tpl.id })
const copies = svc.state().workflows.filter(w => w.name === `${tpl.name} 副本`)
const ran = svc.runNamed({ workflow: `${tpl.name} 副本`, brief: 'Ship it', task: '#1' })
const started = ran.workflows.filter(w => (w.activeCount ?? 0) > 0).map(w => w.id)

Output:

copies = [ 'wf-906f069e', 'wf-e5d84bcb', 'wf-d904162f' ]
copies named "发布交付流水线 副本" = 3
runNamed("发布交付流水线 副本") started = [ 'wf-906f069e' ]  | first copy = wf-906f069e
copies 2 and 3 reachable by name = false false

A second probe with two hand-written workflows both named 发布 (space-free name, so the token path is not truncated):

ids = [ 'wf-052433b9', 'wf-66e5b2a4' ]
runNamed("发布") started = [ 'wf-052433b9' ]  | second copy started = false
board token "workflow:发布" routes to = wf-052433b9  | second = wf-66e5b2a4
assignee "发布" routes to = wf-052433b9

Root cause

  • plugins/workflow/src/service.js:672 — the only dedupe for a template copy is state.workflows.some(item => item.name === base) ? cleanLine(${base} 副本, 80) : base. It compares the base name once and never compares the candidate base 副本 against the store, so copy FlyBot plugin layer (ctx.flybot) with social and tree plugins #2 and copy FlyBot: plugin architecture (umbrella) #3 both compute the same base 副本.
  • create (:665-713) has no unique-name check of any kind (a non-template create takes cleanLine(body.name, 80) as-is).
  • rename (:624-639) also has none: it checks readability and non-empty only, then assigns item.name = name.
  • findWorkflow (:142-146) is state.workflows.find(item => item.id.toLowerCase() === wanted || item.name.toLowerCase() === wanted) — first match wins, no ambiguity signal.
  • plugins/workflow/src/match.js:28-31 is a second copy of the same first-match lookup, and :53-60 uses it for the workflow:<name> token, task.workflowId/task.workflow, and assignee.
  • plugins/workflow/index.js:111-118 — the run_workflow tool takes workflow: 'Workflow id or exact name' and calls service.runNamed, so a model or a user passing the name gets the first match.
  • Client side: core/src/client/workflows.js:371-386 useTemplate posts workflow/create with only { template }, so two clicks produce the first pair of identical names; core/src/client/workflows.js:392-394 confirmRename only rejects an empty name and then posts workflow/rename, so a rename onto an existing name is accepted.

Expected

  1. Names are unique inside the store. A template copy picks the next free name (X, X 2, X 3, or 副本 2, 副本 3), not the same X 副本 every time.
  2. create and rename refuse a name that is already taken and answer with an explicit error (A workflow named "X" already exists), instead of storing a duplicate.
  3. If a legacy store already carries two workflows with the same name, runNamed must not silently pick the first: it returns { ok: false, error } naming both candidate ids (or returns the candidates), so the caller can retry by id.
  4. The Settings rename dialog surfaces that error text. The rename/create error pass-through in core/src/client/workflows.js is ours to make; the request issue is not split for it — whoever implements the workflow-module side should state in the PR which half the host did.

Acceptance (deterministic red today)

One test that covers all three cases:

  • three create({ template }) calls in a row produce three distinct names (today: three identical 发布交付流水线 副本);
  • rename onto an existing name answers ok === false and leaves the store unchanged (today: ok === true);
  • with a store that already holds two same-named workflows, runNamed(name) answers ok === false (or lists the candidate ids) instead of starting the first (today: starts wf-… Import the DS Bot plugin codebase from DeepSeek-Bot #1 silently);
  • after the fix, each of the three copies can still be started by id, and a start by name resolves to exactly one workflow or is refused.

Reachable

Not visible to users yet: the instance checkout is e37f1f8, whose build predates the Workflows surface; the defect arrives with the next milestone update. It is live on main (5daf756b, 2026-10-27T11:57:54Z) and reproducible in-process as above — no host, no browser, no credentials needed.

Dedupe (all states, open + closed)

  • workflow duplicate name → 46 results, no match (top hits: #1414 ephemeral map lost, #1416 denied step counted as handed, #1272 engine meta).
  • workflow rename same name → 23, no match (top hits: #1445, #1440, #1434 closed rename+delete, #1409).
  • workflow name collision → 2, neither about workflows (#820 header seats, #1328 board unify).
  • two workflows same name → 92, no match (#1378/#1432 read-only flowchart, #1425 reap test wall-clock).
  • copy workflow template → 10, no match (#1378, #1374 closed page rewrite, #1291 closed engine step 1).
  • 副本 → 12, no match (#1396 preset loses its "Use this" button once it has a live copy, #1287 copy a scheduled task, both different).

Closest existing issues and why they are not this one: #1434/#1435 (rename and delete exist; no uniqueness), #1396 (a preset with a live copy loses its button — about the template row, not about two user workflows sharing a name), #1414 (a run's ephemeral map is lost — a lane/reap concern), #1441 (delete reads the emptied state.runs — a different function and a different symptom).

Activity

  1. HugoXK commented on Oct 27, 2026

    @HugoXK
    ContributorAuthor

    Closing: this was filed under the superseded "report only, we do not implement plugins/workflow/**" policy (that line is still in the body). No implementation was started and there is no PR, so under the new rule the issue comes down rather than sitting as a claim without a PR. Will re-file with the reproduction and the PR together when a Dev picks it up.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions