Skip to content

Track targeted event status per recipient #8

Description

@kelos-bot

Area

New API, UI, or Deployment Capabilities

Candidate

Add recipient-scoped inbox status to targeted events, so each human or agent can acknowledge and complete a shared instruction independently without clearing it from every other recipient's inbox.

Why this should be the next strategic priority

Agora already models fan-out: an event has a targets array, all is a wildcard target, and the reporting skill tells every agent to acknowledge actionable inbox items and mark them terminal when handled. But lifecycle state is a single scalar on the event.

That turns every shared instruction into an accidental single-consumer queue. The first recipient to mark it done, resolved, or rejected changes the event globally, so it disappears from all other open/actionable inboxes. There is also no way for a coordinator to distinguish "builder handled this" from "every target handled this."

This fails on the first event sent to two recipients, not only after a deployment reaches a scale threshold. It blocks several of Agora's highest-value growth paths:

  • Parallel coding agents cannot independently accept and finish the same review, validation, or rollout instruction.
  • A human cannot broadcast a decision with targets: ["all"] and retain it for agents that have not acknowledged it yet.
  • CI, reviewer, and deployer handoffs cannot show which consumers have handled an event.
  • Larger teams must create duplicate events per recipient, fragmenting the shared timeline and making fan-out targeting misleading.

Evidence from the repository

  • internal/agora/event.go:83-97 gives an event multiple Targets but only one Status.
  • internal/agora/event.go:216-230 sends an event targeted to all to every agent and defines openness solely from that scalar status.
  • internal/agora/store.go:91-119 records the status-changing actor, then overwrites the one stored event status without considering its targets.
  • internal/agora/store.go:219-227 discards the actor when replaying a status_change; only the last global value survives in memory.
  • internal/agora/store.go:251-269 applies inbox status and open filters to event.Status before checking which agent is targeted.
  • internal/server/server.go:140-162 exposes only the event-wide status mutation endpoint.
  • internal/agoracli/cli.go:237-267 always calls that global endpoint, including when an agent is handling an inbox item.
  • internal/server/static/app.js:240-249 and internal/server/static/app.js:299-304 render one set of status actions and post a global update for the selected actor.
  • skills/agora-reporting/SKILL.md:48-52 requires each agent to acknowledge actionable items and then mark them done, resolved, or rejected, which is unsafe for multi-target and all events under the current model.
  • TestInboxMatchesTargetsAndMentions covers all delivery, while the status and inbox tests use only a single target; there is no test that one recipient's completion leaves work open for another.

Reproduction on current main

I posted one instruction targeted to two agents:

{
  "type": "instruction",
  "actor": "human",
  "thread": "release",
  "title": "Validate release",
  "targets": ["agent-a", "agent-b"]
}

Both open inboxes initially contained the event. Agent A then used the existing status API:

POST /api/events/<event-id>/status
Content-Type: application/json

{"actor":"agent-a","status":"done"}

The result was:

agent-a open inbox before: [<event-id>]
agent-b open inbox before: [<event-id>]
agent-a marks done:         event.status=done
agent-a open inbox after:  []
agent-b open inbox after:  []

The JSONL log contains actor: "agent-a" on the status_change, but replay applies that record globally. The same failure occurs for targets: ["all"].

Proposed design

Separate event lifecycle from per-recipient handling

Keep the current endpoint and Event.status semantics unchanged for intentional event-wide transitions:

POST /api/events/<event-id>/status
{"actor":"human","status":"resolved"}

Add an explicit recipient-scoped status operation, for example:

POST /api/agents/agent-a/inbox/<event-id>/status
Content-Type: application/json

{"actor":"agent-a","status":"done"}
  • Validate that the event targets the named recipient directly or through all.
  • Reuse the current status vocabulary and normalization rather than adding a second lifecycle.
  • Store both recipient and actor; they are usually equal, but retaining both permits an operator to act on behalf of an agent and preserves an audit label.
  • Treat recipient state as coordination metadata, not authentication. Local, shared-token, and Kubernetes deployments keep their current bearer-token behavior.

For an agent inbox, compute an effective status as follows:

  1. A terminal event-wide status (done, resolved, or rejected) closes the event for everyone.
  2. Otherwise, use that agent's latest recipient status when one exists.
  3. Otherwise, fall back to the event-wide status.

Apply open=true and repeated status filters to this effective status. This makes recipient filtering correct before limit is applied.

Inbox responses should remain event-shaped but add an optional field such as recipient_status; timeline responses can add an optional recipient_statuses map when receipts exist:

{
  "id": "evt_...",
  "status": "open",
  "targets": ["agent-a", "agent-b"],
  "recipient_status": "done",
  "recipient_statuses": {
    "agent-a": "done",
    "agent-b": "acknowledged"
  }
}

The canonical status remains event-wide. The additional fields let agents reason about their own inbox and let a human see partial completion without changing existing response fields.

Persist receipts additively

Append a new JSONL record kind instead of rewriting event records:

{
  "kind": "recipient_status_change",
  "recipient_status": {
    "event_id": "evt_...",
    "recipient": "agent-a",
    "actor": "agent-a",
    "status": "done",
    "created_at": "..."
  }
}

Replay it into an in-memory event ID -> recipient -> latest status index. Existing event and status_change records retain their exact meaning, so existing JSONL files load without migration. For an all target, create recipient entries lazily as agents acknowledge or complete the item; Agora does not need an agent registry.

Make first-party consumers safe

  • Add a CLI form such as agora status --recipient agent-a <event-id> done that uses the recipient endpoint; keep an unscoped agora status global for compatibility and operator use.
  • Update skills/agora-reporting to use recipient-scoped status for items obtained from the agent's inbox.
  • When the browser's selected actor is a direct or all target, make its acknowledge/done actions recipient-scoped and render that actor's effective status. Event-wide closure should remain an explicit coordinator action.
  • Do not require new environment variables, agent-specific tokens, or Kubernetes resources.

Compatibility

  • Existing API clients and the current global status endpoint retain their behavior.
  • Existing event JSON fields, status values, target matching, and forward polling remain valid.
  • Added response fields are optional; old clients can ignore them.
  • Existing JSONL logs replay unchanged; missing recipient state falls back to the global event status.
  • AGORA_URL, AGORA_AGENT, AGORA_THREAD, and AGORA_TOKEN keep their current meanings.
  • A global terminal update remains available when a coordinator intentionally wants to close the event for everyone.

Acceptance criteria

  • An open instruction targeted to agent-a and agent-b appears in both open inboxes.
  • After agent A acknowledges and completes it through the recipient-scoped path, it is absent from agent A's open/actionable inbox but remains open in agent B's inbox.
  • Agent B can independently acknowledge and complete the same event.
  • The same isolation works for an event targeted to all, with recipient records created lazily.
  • A terminal event-wide update still removes the event from every recipient's open inbox.
  • Inbox open and repeated status filters use recipient-effective state before applying limit.
  • Recipient changes survive server restart, while a fixture containing only the existing JSONL record kinds still replays identically.
  • Attempts to update a recipient not targeted by the event return a focused 400 response.
  • The CLI and browser use recipient-scoped updates when handling targeted inbox work.
  • The reporting skill's acknowledge/complete loop cannot clear another agent's inbox item.
  • Tests cover two explicit recipients, the all wildcard, global closure, filtering-before-limit, persistence/replay, and existing-client behavior.
  • make verify passes.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions