Skip to content

[agora-fake-strategist] Track targeted event status per recipient #8

Description

@kelos-bot

Latest verdict

  • Status: VALID

  • Checked at: 2026-10-01T12:02:34Z

  • Evidence: main is still at 2f3ddab. Nothing has merged since #7, and no PRs are open. This issue has no comments or assignees. The only other open issue, #5, covers browser token auth and does not overlap. Every code anchor below still points at the cited code, and make build verify passes. A freshly built agora-server with AGORA_TOKEN set reproduced every scenario:

    • For an instruction targeting ["agent-a","agent-b"], agent A's agora status <id> acknowledged made B's open inbox show acknowledged. After A's done, both open inboxes and GET /api/events?agent=agent-b&open=true returned [], and B's agora inbox printed inbox empty.
    • After agent A marked an ["all"] instruction done, B's agora inbox --open printed inbox empty.
    • After a restart, B's open inbox held only a newer instruction, although all three JSONL status_change records have actor: agent-a.
    • POST /api/agents/agent-b/inbox/<id>/status returns 404.
    • Agent A then posted twenty --target all comments. B's default agora inbox returned 20 items, an older open instruction plus 19 broadcasts, and omitted a newer instruction targeted at B. A's own default inbox showed 20 of its own broadcasts.

    This run also weighed three alternatives:

    • An unauthenticated /api/healthz still returns 401 once AGORA_TOKEN is set, so Kubernetes probes fail for token-protected deployments. That is a smaller deployment fix.
    • handleInbox returns immediately, so a blocked agent cannot wait on a reply without polling. That makes agora inbox long-polling an optimization, not a correctness issue.
    • agora post --reply-to does not target the parent's actor. A CLI reply therefore reaches the asker only through an explicit --target, while the browser fills that target in automatically. That is a smaller routing gap.

    None of these outranks recipient-scoped status. Long-polling and reply routing would also feed more events into the same shared-status inbox.

Area

New API, UI, or Deployment Capabilities

Candidate

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

Why this should be the next strategic priority

Agora already supports fan-out. An event can target several agents, all is a wildcard target, and the reporting skill tells each agent to acknowledge and complete actionable inbox items. But an event's lifecycle state is a single value.

As a result, every multi-target instruction acts like a single-consumer queue, even though nobody asked for one. The first recipient to mark it done, resolved, or rejected changes the event for everyone, so it disappears from every recipient's actionable inbox. A coordinator also cannot tell whether one recipient or all of them handled it.

This blocks several high-value workflows at their foundation:

  • Parallel coding agents cannot each accept and finish the same review, validation, or rollout instruction.
  • A human cannot broadcast a decision to all and keep it in the inbox of each agent that has not acknowledged it yet.
  • CI, reviewer, and deployer handoffs cannot show which consumers have acted.
  • Teams must duplicate events per recipient, which fragments the shared timeline and defeats the point of multi-target events.
  • Agents cannot clear all broadcasts for themselves. They either close a broadcast for every other agent, or leave it posted, where it fills their default inbox and hides newer targeted work.

Integrations would inherit this ambiguity. Fixing recipient ownership first makes Agora's existing event and inbox model safe to extend.

Current behavior

  • Event has multiple Targets but only one Status.
  • UpdateStatus records the actor, then overwrites the single status without considering recipients.
  • JSONL replay applies each status_change to the whole event and keeps no per-actor handling state.
  • Inbox filtering checks the event-wide status for every target. GET /api/events?agent= uses the same path through filterFromQuery.
  • ListEvents returns the oldest matches first and stops at limit. The CLI inbox defaults to 20, so unhandled broadcasts can hide newer targeted work.
  • The server exposes only the event-wide status endpoint, and the CLI always calls it.
  • The CLI's filterInbox filters the server response again, on the event-wide status. renderEvent prints that same status.
  • The browser actions also change the event-wide status, recording the selected actor.
  • The reporting skill requires every recipient to acknowledge and complete actionable inbox items. Today that is unsafe for multi-target and all events.

One instruction is enough to reproduce the current behavior:

POST /api/events
Content-Type: application/json

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

At first, both open inboxes contain the event. Agent A then calls the current status API:

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

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

The result is:

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:  []
agent-b after restart:     []

The JSONL log records actor: "agent-a" on the status_change, but replay applies it to the whole event. targets: ["all"] fails the same way.

The broadcast starvation case reproduces with the CLI:

agent-a: 20 x agora post --type comment --target all --title "note N"   (each stays posted)
human:   agora post --type instruction --target agent-b --title "Stop and rebase"
agent-b: agora inbox   -> the 20 broadcasts only; the instruction is not shown

Proposed design

Keep global lifecycle and add recipient handling

Keep the current endpoint and Event.status semantics for deliberate transitions that apply to the whole event:

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

Add a recipient-scoped operation:

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

{"actor":"agent-a","status":"done"}
  • The event must target the recipient directly or through all.
  • Reuse the existing status vocabulary instead of adding a second lifecycle.
  • Store both recipient and actor. They are usually equal, but keeping both preserves the audit trail when an operator acts on behalf of an agent.
  • Treat recipient state as coordination metadata, not authentication. Local, shared-token, and Kubernetes deployments keep their current bearer-token behavior.

For an agent inbox, derive the effective status in this order:

  1. A terminal global status (done, resolved, or rejected) closes the event for everyone.
  2. Otherwise, use the recipient's latest status, if there is one.
  3. Otherwise, fall back to the global event status.

Apply effective status wherever EventFilter.Agent is set, which covers both /api/agents/<agent>/inbox and GET /api/events?agent=. Evaluate open=true and repeated status filters against effective status before applying limit.

Responses keep the event shape and gain only optional fields:

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

An inbox response needs only recipient_status for the recipient it names. Timeline and detail responses may include recipient_statuses, so a coordinator can see partial completion. status stays the canonical global lifecycle, and nothing closes an event globally on its own.

Persist receipts additively

Append a new JSONL record kind:

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

Replay these records into an index from event ID to recipient to latest status. Existing event and status_change records keep their meaning, so existing JSONL files need no migration. For all, create recipient entries only when an agent acts, so Agora needs no agent registry.

Update first-party consumers

  • Add agora status --recipient agent-a <event-id> done. Keep unscoped agora status global, for compatibility and for coordinators.
  • In agora inbox, filter and render on recipient_status when it is present, and fall back to status. Otherwise the client-side filterInbox would drop, for example, a posted event the recipient has acknowledged under --open.
  • Change skills/agora-reporting to use recipient-scoped status for work taken from the agent's own inbox.
  • When the browser's selected actor is a direct or all target, scope the acknowledge and done actions to that recipient and show the actor's effective status. Keep closing an event for everyone as an explicit coordinator action.
  • Do not add environment variables, per-agent tokens, or Kubernetes resources.

Compatibility

  • Existing API clients and the global status endpoint keep their behavior.
  • Existing event fields, status values, target matching, and forward polling remain valid.
  • Added response fields are optional, and old clients can ignore them.
  • Existing JSONL logs replay unchanged. When an event has no recipient state, its global status applies.
  • AGORA_URL, AGORA_AGENT, AGORA_THREAD, and AGORA_TOKEN keep their meanings.
  • A global terminal update is still available when a coordinator wants to close the event for everyone.
  • Rollback compatibility is not promised. Older binaries reject the new recipient_status_change record kind at replay.

Acceptance criteria

  • An open instruction targeted at agent-a and agent-b appears in both open inboxes.
  • After agent A acknowledges and completes it through the recipient endpoint, it leaves agent A's actionable inbox but stays actionable for agent B.
  • Agent B can acknowledge and complete the same event on its own.
  • The same isolation works for all, with recipient records created only when an agent acts.
  • A terminal global update still removes the event from every recipient's open inbox.
  • Inbox open and repeated status filters use recipient-effective status before applying limit. This also applies to GET /api/events?agent=.
  • An agent that marks twenty posted all broadcasts done for itself sees a newer instruction targeted at it in its default agora inbox. Other agents still see those broadcasts as actionable.
  • agora inbox --open keeps a posted event that the recipient has acknowledged, and shows the recipient's status.
  • Recipient changes survive a restart, and a fixture that contains only existing JSONL record kinds replays identically.
  • Updating a recipient the event does not target returns a focused 400 response.
  • The CLI and browser use recipient-scoped updates when handling targeted inbox work.
  • In the reporting skill, one agent's acknowledge-and-complete loop cannot clear another recipient's item.
  • Tests cover two explicit recipients, all, global closure, broadcast starvation, filtering before limit, persistence and replay, and unchanged behavior for existing clients.
  • make verify passes.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions