Skip to content

workflow: two run_workflow calls in the same millisecond share one task id, and the second run is silently dropped #2662

Closed

Description

@Birfy

Written-by: Chief

What happens

run_workflow generates a task id when the caller omits one, and it generates it from the millisecond timestamp only: plugins/workflow/src/service.js:1957

const taskId = cleanLine(body.task || body.taskId, 80) || `task-${now().toString(36)}`

Two calls that land in the same millisecond therefore get the same id, and the second one is silently discarded:

  • service.js:1174 — enqueue asks queuedOrRunning(item.id, task.id) first, and that compares run.task?.id === taskId (service.js:1020-1023). The first run already carries that id, so the second call returns { ...present(), already: true } and no second run is created.
  • Worse, before that check the second call has already written state.briefs[task.id] (service.js:1493), so the first run's stored brief is replaced by the second task's brief. The dropped task is not even recoverable from the cache.

The user sees a workflow that was "started twice" run once, and the second brief's text may be what the single run executes.

Reproduction

The service takes an injected now, so this is deterministic:

const svc = createWorkflowService({ persist: false, now: () => FIXED, bots: () => roster, spawn: async () => 'eph-1', release: async () => {}, dispatch: async () => ({ ok: true }), setTimer: () => 0, clearTimer: () => {} })
await svc.runNamed({ workflow: id, brief: 'Issue #1: fix the crash' })
await svc.runNamed({ workflow: id, brief: 'Issue #2: fix the timeout' })

Ran on tip d8a896c4a70a9aab9e7c5d46f004f70c5c31211f (repo copy in /data/dsh/bot/workspace/tmp/idau1108/tip, probe /data/dsh/bot/workspace/tmp/idau1108/repro/runsame2.mjs):

--- same millisecond
  runNamed #1 -> {"ok":true,"error":null}  task: task-loyw3v28
  runNamed #2 -> {"ok":true,"error":null,"already":true}
  runs in lane: task-loyw3v28 "Issue #1: fix the crash"
  activeCount: 1
--- 1 ms apart
  runNamed #1 -> {"ok":true,"error":null}  task: task-loyw3v28,task-loyw3v2a
  runNamed #2 -> {"ok":true,"error":null}
  runs in lane: task-loyw3v28 "Issue #1: fix the crash"  ||  task-loyw3v2a "Issue #2: fix the timeout"
  activeCount: 2

One millisecond of separation is the whole difference between two runs and one deliberately de-duplicated call.

Reachable from

run_workflow is a tool every Bot can call, and its id is generated when the call omits task (plugins/workflow/index.js, parameter description "Omitted ids are generated"). Two Bots handed the same workflow in the same group round, or one Bot issuing two calls in one step, is enough. Nothing about the call says "this may be deduplicated".

Fix

Use the helper that is already in the file and already imported (service.js:5 imports randomBytes; service.js:64-66 defines idOf(prefix) and has no caller today):

const taskId = cleanLine(body.task || body.taskId, 80) || idOf('task')

A random id makes two calls unique without touching queuedOrRunning, which must keep de-duplicating a repeated dispatch of the same card (that is #2468/#2475 behaviour and the queue contract).

Test to add (red first)

In plugins/workflow/test/, drive createWorkflowService with now: () => 1_700_000_000_000 (constant, so both calls share the millisecond), call runNamed twice with two different briefs, and assert two runs with two different task ids and each run's own title. On the current tip this fails with one run and already: true; after the fix it passes. Do not weaken queuedOrRunning: add a third assertion that a third call with an explicitly repeated task is still answered already: true.

Scope

  • plugins/workflow/src/service.js (line 1957, plus the test file)
  • changelog.d/<this-issue>-<slug>.fixed.md

Not in scope: other id generators (idOf call sites), the queue contract, already: true semantics.

Activity

  1. self-assigned this
    on Nov 8, 2026
  2. added
    bugSomething isn't working
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    area:automationScheduled tasks and automation
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Nov 8, 2026
  3. Birfy commented on Nov 8, 2026

    @Birfy
    CollaboratorAuthor

    Written-by: Chief

    已派实现线(flybot/workflow/assign,workflow wf-67229151,task i2662-impl)。这条 agent-working 只是让下一拍节拍不要重复派活,分支名与红先证据以 build/ship 节点的交付为准。

  4. removed
    agent-readyMaintainer-approved spec: an agent may take it. Only maintainers set this; never in a template
    agent-workingAn agent has claimed this issue (the claim comment names the branch)
    on Nov 8, 2026
  5. added 2 commits that reference this issue on Nov 9, 2026
    eeaab43
    af9a30b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:automationScheduled tasks and automationbugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions