feat(agents): roster endpoint + last_seen_at + ETag (parity port of cueapi/cueapi#630)#80
Merged
Conversation
…ueapi/cueapi#630) Re-port of closed [PR #47](#47) which was on a stale base ~8880 deletions behind main. Fresh against current main HEAD. Phase A of the Agent Directory productization. Eliminates the failure mode where agents had to remember 6+ fields per recipient AND had no way to discover the live roster. ## What lands - **GET /v1/agents/roster** — display-optimized snapshot for prompt- injection at session-boot. Distinct from the existing management surface (GET /v1/agents): - Always-full list (no pagination) - Drops opaque IDs / secrets / timestamps / tenancy metadata - Adds derived ``online``, ``last_seen_relative``, ``preferred_contact`` - Always excludes soft-deleted agents - Weak ETag + ``If-None-Match`` → 304 Not Modified for poll efficiency - ETag bucketed to 5-min windows so quiet periods produce stable hashes - ``Cache-Control: private, max-age=300`` matches derivation buckets - **Migration 031** (renumbered from private's 048) — adds ``agents.last_seen_at TIMESTAMPTZ NULL``. Nullable, no backfill. - **Hot-path hooks** write ``last_seen_at = now()``: - ``create_message`` — sender's agent (in same tx via touch_last_seen) - ``list_inbox`` — recipient's agent, on EVERY poll (via _bump_last_seen_stmt). Even when no queued messages exist, the poll proves activity. - **Online derivation** (server-computed in ``list_roster``): - within 5 min → ``online`` - within 30 min → ``away`` - older or NULL → ``offline`` - Caller override wins: PATCHed status=away/offline keeps that override regardless of recent activity ## Pure helpers (for unit-testability — pytest-cov + ASGI issue) - ``_build_roster_entry(agent, now)`` in agent_service.py: ORM Agent → (entry_dict, etag_part_string) - ``_compute_roster_etag(parts)`` in agent_service.py: list → weak ETag - ``_derive_online_state(now, last_seen_at, asserted_status)`` → (online_bool, derived_status) - ``_format_relative(now, last_seen_at)`` → "active now" / "5m ago" / ... - ``_bucketed_seen(last_seen_at)`` → string for ETag stability - ``_bump_last_seen_stmt(agent_id, now)`` in inbox_service.py: SQLAlchemy UPDATE statement - ``_etag_matches(if_none_match, server_etag)`` in agents router: conditional GET predicate ## Tests 27 new tests in tests/test_agent_roster.py (verbatim from private): shape verification, hot-path hooks (sender + recipient), derivation correctness across all 3 buckets, caller-asserted status override, soft-delete exclusion, preferred_contact derivation, last_seen_relative formatting, ETag 304 handling, ETag changes when roster mutates, pure-helper unit tests. 27/27 pass locally. Full local suite: 890 passed + 18 xfailed (pre-existing) + 4 skipped. Zero regressions. ## Re-port note Re-port of closed PR #47. Fresh against current main after PR #74 + #75 + #76 + #77 + #78 + #79 merged earlier in this session.
Parity checkThis PR modifies files tracked in
Please confirm one of the following in a reply or PR description update:
This is a soft check — it does not block merge. The goal is visibility, not friction. See HOSTED_ONLY.md for the open-core policy. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Re-port of closed PR #47 which was on a stale base ~8880 deletions behind main. Fresh against current main HEAD.
Phase A of the Agent Directory productization (PRD: trydock.ai/mike/agent-directory-productization-prd). Eliminates the failure mode where agents had to remember 6+ fields per recipient AND had no way to discover the live roster.
What lands
New endpoint
GET /v1/agents/roster— display-optimized snapshot for prompt-injection at session-boot. Distinct fromGET /v1/agents(management surface):online,last_seen_relative,preferred_contactIf-None-Match→ 304 Not Modified for poll efficiencyCache-Control: private, max-age=300matches derivation bucketsMigration 031 (renumbered from private's 048)
Adds
agents.last_seen_at TIMESTAMPTZ NULL. Nullable, no backfill.Hot-path hooks
last_seen_at = now()writes:create_message— sender's agent (viatouch_last_seenin agent_service)list_inbox— recipient's agent on EVERY poll (via_bump_last_seen_stmtin inbox_service). Even when no queued messages exist, the poll proves activity.Online derivation (server-computed in
list_roster)onlineawayofflinestatus=away/offlinekeeps that overridePure helpers (for unit-testability)
pytest-cov + ASGI in-process dispatch on Python 3.12 doesn't always trace HTTP-routed paths cleanly. Pure helpers sidestep that.
_build_roster_entry(agent, now)in agent_service.py_compute_roster_etag(parts)in agent_service.py_derive_online_state(now, last_seen_at, asserted_status)_format_relative(now, last_seen_at)_bucketed_seen(last_seen_at)_bump_last_seen_stmt(agent_id, now)in inbox_service.py_etag_matches(if_none_match, server_etag)in agents routerTests
27 new tests in
tests/test_agent_roster.py(verbatim from private):preferred_contactderivationlast_seen_relativeformattingFull local suite: 890 passed + 18 xfailed (pre-existing) + 4 skipped. Zero regressions.
Re-port note
Re-port of closed PR #47. Fresh against current main after PR #74 + #75 + #76 + #77 + #78 + #79 merged earlier in this session.
Test plan
🤖 Generated with Claude Code