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:
- A terminal global status (
done, resolved, or rejected) closes the event for everyone.
- Otherwise, use the recipient's latest status, if there is one.
- 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.
Latest verdict
Status:
VALIDChecked at:
2026-10-01T12:02:34ZEvidence:
mainis 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, andmake build verifypasses. A freshly builtagora-serverwithAGORA_TOKENset reproduced every scenario:["agent-a","agent-b"], agent A'sagora status <id> acknowledgedmade B's open inbox showacknowledged. After A'sdone, both open inboxes andGET /api/events?agent=agent-b&open=truereturned[], and B'sagora inboxprintedinbox empty.["all"]instructiondone, B'sagora inbox --openprintedinbox empty.status_changerecords haveactor: agent-a.POST /api/agents/agent-b/inbox/<id>/statusreturns404.--target allcomments. B's defaultagora inboxreturned 20 items, an older open instruction plus 19 broadcasts, and omitted a newerinstructiontargeted at B. A's own default inbox showed 20 of its own broadcasts.This run also weighed three alternatives:
/api/healthzstill returns401onceAGORA_TOKENis set, so Kubernetes probes fail for token-protected deployments. That is a smaller deployment fix.handleInboxreturns immediately, so a blocked agent cannot wait on a reply without polling. That makesagora inboxlong-polling an optimization, not a correctness issue.agora post --reply-todoes 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,
allis 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, orrejectedchanges 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:
alland keep it in the inbox of each agent that has not acknowledged it yet.allbroadcasts for themselves. They either close a broadcast for every other agent, or leave itposted, 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
Eventhas multipleTargetsbut only oneStatus.UpdateStatusrecords the actor, then overwrites the single status without considering recipients.status_changeto the whole event and keeps no per-actor handling state.GET /api/events?agent=uses the same path throughfilterFromQuery.ListEventsreturns the oldest matches first and stops atlimit. The CLI inbox defaults to 20, so unhandled broadcasts can hide newer targeted work.filterInboxfilters the server response again, on the event-widestatus.renderEventprints that same status.allevents.One instruction is enough to reproduce the current behavior:
At first, both open inboxes contain the event. Agent A then calls the current status API:
The result is:
The JSONL log records
actor: "agent-a"on thestatus_change, but replay applies it to the whole event.targets: ["all"]fails the same way.The broadcast starvation case reproduces with the CLI:
Proposed design
Keep global lifecycle and add recipient handling
Keep the current endpoint and
Event.statussemantics for deliberate transitions that apply to the whole event:Add a recipient-scoped operation:
all.recipientandactor. They are usually equal, but keeping both preserves the audit trail when an operator acts on behalf of an agent.For an agent inbox, derive the effective status in this order:
done,resolved, orrejected) closes the event for everyone.Apply effective status wherever
EventFilter.Agentis set, which covers both/api/agents/<agent>/inboxandGET /api/events?agent=. Evaluateopen=trueand repeatedstatusfilters against effective status before applyinglimit.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_statusfor the recipient it names. Timeline and detail responses may includerecipient_statuses, so a coordinator can see partial completion.statusstays 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
eventandstatus_changerecords keep their meaning, so existing JSONL files need no migration. Forall, create recipient entries only when an agent acts, so Agora needs no agent registry.Update first-party consumers
agora status --recipient agent-a <event-id> done. Keep unscopedagora statusglobal, for compatibility and for coordinators.agora inbox, filter and render onrecipient_statuswhen it is present, and fall back tostatus. Otherwise the client-sidefilterInboxwould drop, for example, apostedevent the recipient hasacknowledgedunder--open.skills/agora-reportingto use recipient-scoped status for work taken from the agent's own inbox.alltarget, 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.Compatibility
AGORA_URL,AGORA_AGENT,AGORA_THREAD, andAGORA_TOKENkeep their meanings.recipient_status_changerecord kind at replay.Acceptance criteria
agent-aandagent-bappears in both open inboxes.all, with recipient records created only when an agent acts.openand repeatedstatusfilters use recipient-effective status before applyinglimit. This also applies toGET /api/events?agent=.postedallbroadcastsdonefor itself sees a newer instruction targeted at it in its defaultagora inbox. Other agents still see those broadcasts as actionable.agora inbox --openkeeps apostedevent that the recipient hasacknowledged, and shows the recipient's status.400response.all, global closure, broadcast starvation, filtering before limit, persistence and replay, and unchanged behavior for existing clients.make verifypasses.