Skip to content

PATCH /v1/agents/:name returns one presence value and broadcasts a different one, from the same request #313

Description

@khaliqgant

Summary

PATCH /v1/agents/:name with {"status":"active"} on a stale agent returns "offline" in the response body and simultaneously broadcasts agent.status.active to realtime subscribers. Same request, same instant, two different answers about the same agent.

Originally surfaced by cubic on #306 and confirmed independently while reviewing that PR.

Mechanism

updateAgent writes the status column but never renews last_seen:

// packages/engine/src/engine/agent.ts
const setClause: Record<string, unknown> = {};
if (updates.status !== undefined) setClause.status = updates.status;
...
const [updated] = await db.update(agents).set(setClause).where(eq(agents.id, agent.id)).returning();

return {
  ...
  status: effectiveAgentStatus(updated),

effectiveAgentStatus derives from last_seen, so for a row whose last_seen is older than AGENT_LIVENESS_TTL_MS it returns offline no matter what was just written to the column.

The realtime fanout on the same request reads the requested value instead:

// packages/engine/src/routes/agent.ts
if (body.status !== undefined) {
  await fanoutAgentStatus(c, updated, body.status);

So the HTTP caller is told offline while every subscriber is told active, and the stored column now says active while the derived view says offline.

Why it is worth a separate issue

This is the same "two fields, one name" hazard that made the presence area expensive to reason about (#312, and the identity guard in #306). It is not, however, the same severity: after #306 the reclaim guard reads last_seen, so updateAgent no longer moves an identity boundary. Before that change, this endpoint was a write path into the reclaim guard — setting status: 'active' re-tightened a row while proving nothing about liveness. That part is now closed; the reporting divergence is not.

Options, not a recommendation

Worth deciding rather than patching the symptom:

  • have the fanout use effectiveAgentStatus(updated) so both surfaces agree — cheapest, keeps the write meaningless for a stale row
  • have an explicit status: 'active' write renew last_seen, on the grounds that an operator asserting liveness is an activity signal — but this makes an unauthenticated-of-the-agent PATCH able to assert an agent's liveness, which is exactly the coupling fix(engine): restore agent presence lifecycle #306 removed. Probably wrong for that reason.
  • reject status writes that contradict observed activity, and return 409
  • drop caller-supplied status from this endpoint entirely, on the grounds that presence is server-observed and not something a caller should be able to assert

Not urgent

No identity or delivery consequence after #306. It is a correctness and observability defect: monitoring and any consumer reacting to agent.status.* will disagree with the API for up to one TTL window.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions