Skip to content

Agent work-claim coordination: agents announce and reserve the scope they are working on #15946

Open
7 of 16 issues completed

Description

@mrveiss

Problem

Several AutoBot agents can work the same project at the same time. Nothing today tells
agent B that agent A is already editing a file, rewriting a KB collection, or driving a
device. a2a/task_manager.py tracks tasks, agents/agent_orchestration/coordinator.py
routes requests, and neither records which resource an agent is currently holding.
Two agents given overlapping work silently overwrite each other; the loser's work is lost
with no error anywhere.

Goal

A coordination channel with three properties:

  1. Announce — an agent declares the scope it is about to touch, with its identity,
    its task, and a human-readable intent.
  2. Reserve — an overlapping declaration is refused, and the refusal names the holder
    so the second agent can wait, queue, ask for a handoff, or pick different work.
  3. Expire — a crashed agent never blocks the project. Claims are TTL'd and renewed by
    heartbeat; a dead holder's scope frees itself.

Design

Substrate. autobot_shared/coordination/shared_runtime_bag.py already gives Redis-backed
cross-worker state with CAS and pub/sub (#6630). The claim registry is a sibling in the same
package, not a new store and not a second bus.

Scope grammar. <kind>:<segment>/<segment>/... — kinds file, dir, kb, device,
project, config. Two scopes overlap when the kinds match and one path is a
segment-aligned prefix of the other, so dir:autobot-backend/llc conflicts with
file:autobot-backend/llc/budget.py.

Modes. EXCLUSIVE (write intent) and SHARED (read intent). SHARED×SHARED coexists;
everything else conflicts. Re-acquiring a scope you already hold, as the same
(agent_id, task_id), is an idempotent renew — an agent must never deadlock against itself.

Authority. Redis is the system of record for agent_work_claims, declared in
autobot_shared/store_authority.py. Claims are ephemeral by design: the durable record of
work is the a2a Task, and a claim is rebuilt by the agent re-acquiring on its next step.

Event layer. docs/developer/EVENT_STATE_DOCTRINE.md lists agent-to-agent coordination
semantics as an anti-goal of the event layer — so the semantics live in
autobot_shared/coordination/, and only the user-visible projection ("agent X holds
scope Y") is published, on the existing bus, over the existing agent:{id} and global
channels. No new WebSocket route, and a REST snapshot so a reconnecting client recovers
state it missed.

Anti-goals

  • Not a distributed transaction manager, and not a substitute for DB constraints.
  • Not a new event bus, and not a new WebSocket endpoint.
  • Not a lock that can outlive its holder — every claim has a TTL.

Tasks

Wave 1

Wave 2 (both depend on #15947)

Wave 3 (depends on #15947, #15948)

Wave 4 (depends on #15949)

Activity

  1. mrveiss commented on Sep 7, 2026

    @mrveiss
    OwnerAuthor

    Grammar correction from implementing wave 1 — the umbrella's Design section says kinds file, dir, kb, device, project, config with overlap requiring the same kind, and then gives a cross-kind example (dir:…/llc conflicting with file:…/llc/budget.py). Those contradict. Resolved in #15958 by dropping dir: kinds are path, kb, device, project, config, and a claim covers its subtree, so the worked example holds within one kind. Rationale and the preserved false-positive case are on #15947; #15948-#15951 should read path: wherever they say file: or dir:.

  2. 8 remaining items

  3. mrveiss commented on Sep 8, 2026

    @mrveiss
    OwnerAuthor

    Live motivation from tonight: three collisions in one session, all from one root cause

    Recording these as observed events rather than as argument, because the umbrella has been reasoned about more than it has been evidenced.

    The shared mrveiss identity made one session misread ownership twice in twenty minutes, in opposite directions.

    1. Duplicate diagnosis and duplicate fix. Two sessions independently diagnosed the same red on PR fix(redis): name the Stack unit everywhere it is operated (#16071) #16072 — check_git_toplevel_env_scrubbed_test.py failing on a bare git ls-files — and both wrote the same fix, minutes apart. Neither had any signal the other was working it. One discovered the collision only by tripping over the other's pushed commit.

    2. A guard claimed by the wrong session. In the same exchange, that session described repo_tests/redis_unit_is_the_stack_unit_16071_test.py as "my new redis naming: five more sites name a unit the fleet does not have (#16060 follow-up) #16071 guard". It was not theirs — it was written hours earlier by a different session under redis naming: five more sites name a unit the fleet does not have (#16060 follow-up) #16071. They had read author=mrveiss on the branch and concluded it was their own work.

    3. An issue claimed by the wrong session. Within the same window, the same two sessions both reported PR fix(hooks): a non-deny permissionDecision must exit 0 to be read (#15956) #15996 as their remaining red, with identical failure lists. It had been held all session by one of them, who wrote the guard, fixed both halves of the deny()/stderr gap and carried the local commits. Resolving it required a coordinator ruling on history, because nothing in the repository distinguished them: author=mrveiss, and the branch had no worktree.

    Why the second one is the serious one

    The duplicated effort in (1) is recoverable — two correct fixes, one wasted hour, no damage. The other two are not the same kind of event. Being confidently wrong about who owns a file is the precondition for editing it, and the only reason (2) cost nothing is that the misattribution was to a file the session had merely diagnosed rather than one it went on to change.

    All three share a single mechanism: the branch name is the only ownership signal, and it is not attached to anything. author=mrveiss is shared by every session; a worktree is transient and was absent in case (3); and a branch with no live tree carries no owner at all.

    That is the state #15987's branch-keyed lock is designed to make legible, and these are three concrete instances of it failing within one session rather than a hypothetical.

    One adjacent finding worth carrying into the design

    A session with a finished worktree held it through several exchanges without noticing, while two peers were cap-blocked. The obvious check argued against releasing it: git merge-base --is-ancestor <branch> origin/Dev_new_gui reports NO for a squash-merged branch by construction, so a landed branch reads as unfinished.

    Any reaping or expiry logic built on this umbrella must therefore key on the merge commit's ancestry plus file presence on base, never on branch ancestry — otherwise the mechanism intended to free scarce slots will systematically conclude that finished work is still in progress.

  4. added this to the v0.9.0 milestone on Sep 12, 2026
  5. mrveiss commented on Sep 13, 2026

    @mrveiss
    OwnerAuthor

    Owner direction, 2026-09-13: session and agent coordination on by default. Mapped against origin/main. The primitives mostly exist; what's missing is wiring and a few extensions. New children, each extending existing machinery:

    #15950 Enforce work claims: task executor and base agent acquire declared sco
    #15951 Agent presence panel: who is working where
    #16269 Declared work scopes are held only on the agent_client path: workflow_
    #16646 feat(coordination): claim events never fire — hold_scopes calls try_ac
    #16647 feat(coordination): the conflict protocol is built but unused — wire w
    #16648 feat(coordination): scoped holds/freezes with a reason and an expiry —
    #16649 feat(coordination): one queryable view of who is doing what, in which
    #16650 feat(events): subscribe to events on the scopes I hold — channels are
    #16651 bug(llc): work-item checkout never expires — a crashed run keeps an it
    #16652 feat(coordination): one writer per external source — a poller lease vi
    #16653 test(coordination): no guard stops a third claim registry — pin 'one c

    Closed with every AC unticked: #15948, #15949 and #15987 each shipped their primitive with tests, but none has a production caller today. Their ACs are library-level, so the wiring is tracked in the new children, not by reopening them. A closure audit should still verify and tick their ACs with evidence.

    Reference design: our own multi-session development tooling now runs this exact pattern: claims with TTL, verdicts tied to a head, scoped holds, a filtered event stream, and a single poller per source.

  6. modified the milestones: v0.9.0, v0.10.0 on Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions