Summary
When two or more Maestro agents are registered against the same project directory, they each independently load the project's .maestro/cue.yaml and each spin up their own copy of every subscription. The result: every trigger fires N times (once per agent sharing the workspace), every scheduled time runs N agents in parallel, every task.pending poll fans out N ways. There is currently no way to declare which agent in a shared workspace should "own" the Cue config.
Concrete repro from my setup
I have an Obsidian vault registered as two agents (different model/profile, same workspace):
Obsidian claude-code $HOME/.../V1 id 160dff0f-...
Obsidian(Sonnet) claude-code $HOME/.../V1 id fe7c6b37-...
$HOME/.../V1/.maestro/cue.yaml contains a time.scheduled subscription at 18:00 and a task.pending subscription on research.md. As soon as the engine registers both agents, both fire every subscription — the 18:00 summary runs twice, the research-task pipeline kicks off two parallel investigations clobbering each other's outputs in the same Research/<date>-<slug>/ folder.
This is a legitimate (and increasingly common) workflow: keep one workspace, switch between Opus and Sonnet agents against it depending on the task. Today, doing so multiplies the cost and chaos of every Cue trigger.
Why it's a scheduler concern, not a YAML-author concern
A subscription already supports agent_id for fan-out targeting, but that doesn't help here — there's no way to say "only the agent with id X should ever evaluate this whole config". A YAML author who wants single-fire semantics today has to:
- duplicate the config per agent and gate each one with
agent_id filters, OR
- pick one agent in the UI and remember never to use the other one for unattended work
Both are footguns. The engine knows which agents share a project root; it should be the one to dedupe.
Proposed fix
Add an optional owner_agent_id (or similarly named) field to the top-level settings: block of cue.yaml:
settings:
owner_agent_id: 160dff0f-db39-4245-9756-39662cd75499 # Obsidian — Opus
timeout_minutes: 30
…
subscriptions:
- name: "research queue"
event: task.pending
watch: "research.md"
prompt_file: .maestro/prompts/cue-research.md
Behavior:
- If
owner_agent_id is set, only the agent whose id matches loads/runs the subscriptions in this file. All other agents whose projectRoot equals this project see the config, log a one-line [CUE] "Obsidian(Sonnet)" skipping cue.yaml — owner_agent_id targets "Obsidian", and register no triggers.
- If
owner_agent_id is absent, fall back to a sensible default — see "Default policy" below.
- Per-subscription
agent_id continues to work for fan-out within the owner.
Default policy when owner_agent_id is unset
This is the trickier half. Two reasonable options:
- First-registered wins — the first agent to call
initSession for a given projectRoot "claims" the config; subsequent agents on the same root skip with a warning. Deterministic on a sorted session list, easy to reason about, but order-dependent if the user reshuffles agents.
- Warn loud, run anyway — preserve today's behavior, but on detection of multiple agents sharing a project root with
cue.yaml, surface a prominent banner in the Cue modal: "2 agents share /path/to/V1 — every trigger will fire twice. Set settings.owner_agent_id to disambiguate."
I'd argue for (1) plus the banner from (2) — it's safer and the banner explains how to make it deterministic.
Suggested fix bundle
- Add
owner_agent_id?: string to the settings schema.
- In
cue-session-runtime-service.ts's initSession, after loadCueConfigDetailed returns ok, check if config.settings.owner_agent_id is present and != session.id — if so, log + return early.
- If
owner_agent_id is unset, group sessions by projectRoot, pick the first by stable order, and treat it as implicit owner. Log the implicit choice so users can see what claimed the config.
- Surface a banner in the Cue modal when multiple sessions share a project root, regardless of whether
owner_agent_id is set.
- Docs: add a "Sharing a workspace across agents" section to
maestro-cue-configuration.md.
Related
(Found together while wiring up a research pipeline against an Obsidian vault that I happened to have registered as two agents.)
Summary
When two or more Maestro agents are registered against the same project directory, they each independently load the project's
.maestro/cue.yamland each spin up their own copy of every subscription. The result: every trigger fires N times (once per agent sharing the workspace), every scheduled time runs N agents in parallel, everytask.pendingpoll fans out N ways. There is currently no way to declare which agent in a shared workspace should "own" the Cue config.Concrete repro from my setup
I have an Obsidian vault registered as two agents (different model/profile, same workspace):
$HOME/.../V1/.maestro/cue.yamlcontains atime.scheduledsubscription at 18:00 and atask.pendingsubscription onresearch.md. As soon as the engine registers both agents, both fire every subscription — the 18:00 summary runs twice, the research-task pipeline kicks off two parallel investigations clobbering each other's outputs in the sameResearch/<date>-<slug>/folder.This is a legitimate (and increasingly common) workflow: keep one workspace, switch between Opus and Sonnet agents against it depending on the task. Today, doing so multiplies the cost and chaos of every Cue trigger.
Why it's a scheduler concern, not a YAML-author concern
A subscription already supports
agent_idfor fan-out targeting, but that doesn't help here — there's no way to say "only the agent with id X should ever evaluate this whole config". A YAML author who wants single-fire semantics today has to:agent_idfilters, ORBoth are footguns. The engine knows which agents share a project root; it should be the one to dedupe.
Proposed fix
Add an optional
owner_agent_id(or similarly named) field to the top-levelsettings:block ofcue.yaml:Behavior:
owner_agent_idis set, only the agent whose id matches loads/runs the subscriptions in this file. All other agents whoseprojectRootequals this project see the config, log a one-line[CUE] "Obsidian(Sonnet)" skipping cue.yaml — owner_agent_id targets "Obsidian", and register no triggers.owner_agent_idis absent, fall back to a sensible default — see "Default policy" below.agent_idcontinues to work for fan-out within the owner.Default policy when
owner_agent_idis unsetThis is the trickier half. Two reasonable options:
initSessionfor a givenprojectRoot"claims" the config; subsequent agents on the same root skip with a warning. Deterministic on a sorted session list, easy to reason about, but order-dependent if the user reshuffles agents.cue.yaml, surface a prominent banner in the Cue modal: "2 agents share/path/to/V1— every trigger will fire twice. Setsettings.owner_agent_idto disambiguate."I'd argue for (1) plus the banner from (2) — it's safer and the banner explains how to make it deterministic.
Suggested fix bundle
owner_agent_id?: stringto thesettingsschema.cue-session-runtime-service.ts'sinitSession, afterloadCueConfigDetailedreturns ok, check ifconfig.settings.owner_agent_idis present and !=session.id— if so, log + return early.owner_agent_idis unset, group sessions byprojectRoot, pick the first by stable order, and treat it as implicit owner. Log the implicit choice so users can see what claimed the config.owner_agent_idis set.maestro-cue-configuration.md.Related
task.pendingfirst-scan seeding(Found together while wiring up a research pipeline against an Obsidian vault that I happened to have registered as two agents.)