feat(agents): roster endpoint + last_seen_at + ETag (parity port of cueapi/cueapi#630)#47
feat(agents): roster endpoint + last_seen_at + ETag (parity port of cueapi/cueapi#630)#47mikemolinet wants to merge 1 commit into
Conversation
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. |
|
Option 1: equivalent change merged to private monorepo as cueapi/cueapi#630 (commit landed 2026-05-05 20:37 UTC). This is a parity port of that PR. |
…ueapi/cueapi#630) Mirrors private PR #630 (Phase A of Agent Directory PRD §Surface 1 + §Surface 5). Adds an implicit "online" derivation so callers don't have to PATCH agent status explicitly — the server bumps ``agents.last_seen_at`` on hot-path activity (message send by sender, inbox poll by recipient) and derives ``online`` from recency. What this PR ships: * **Migration 024** — adds ``agents.last_seen_at TIMESTAMPTZ NULL``. Nullable, no backfill (existing rows stay caller-asserted until first hot-path write lands). NOTE: collides with open OSS PR #46 (also targets 024_message_send_at) — whichever lands first, the other rebases + renumbers to 025. * **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 * **Hot-path hooks** write ``last_seen_at = now()``: - ``create_message`` — sender's agent (in same tx as message insert) - ``list_inbox`` — recipient's agent on every poll * **Online derivation** (server-computed): - ``last_seen_at`` ≤ 5 min → ``online`` - ≤ 30 min → ``away`` - older or NULL → ``offline`` - Caller override wins: PATCH status to ``away`` or ``offline`` sticks regardless of activity. Files: - ``alembic/versions/024_agents_last_seen_at.py`` — new migration - ``app/models/agent.py`` — ``last_seen_at`` column - ``app/schemas/agent.py`` — ``AgentRosterEntry`` + ``AgentRosterResponse`` - ``app/services/agent_service.py`` — ``list_roster`` + 7 pure helpers - ``app/services/inbox_service.py`` — ``_bump_last_seen_stmt`` + integration - ``app/services/message_service.py`` — sender's ``last_seen_at`` hook - ``app/routers/agents.py`` — ``GET /v1/agents/roster`` + ``_etag_matches`` - ``tests/test_agent_roster.py`` — 21 new tests (15 pure unit + 6 integration) - ``parity-manifest.json`` — bump 6 entries to 2026-05-05 Tests: 21 new. 657 passed total (excluding 7 pre-existing SDK test failures unrelated to this PR). Out of scope (separate ports / future PRs): * MCP tool ``cueapi_agents_list`` / ``cueapi_agents_describe`` — cue-mac-app's lane * CLI ``agents list --online-only`` / ``agents describe`` — cueapi-secondary's lane * Desktop UI Directory tab — cue-mac-app's lane * Per-agent ``kind`` / ``capabilities`` fields — §1 capability registry territory Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4e25e80 to
f3b5a62
Compare
|
Closing as stale — branch diverged from main by ~8876 deletions across 63 files. CI is FAILURE on this branch (test + sdk-integration). Rebase infeasible. Feature is still needed: The cueapi-mcp side shipped via PR #30 yesterday (2026-05-10) with the 4 read-only directory tools (cueapi_list_agents / get / presence / roster). The server-side port is still needed for cueapi-core deployments. Next step: re-port as fresh PR against current main HEAD ( Branch retained for reference. |
Pull request was closed
…ueapi/cueapi#630) (#80) 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.
Summary
Parity port of cueapi/cueapi#630 (Phase A of Agent Directory PRD §Surface 1 + §Surface 5). Adds implicit "online" derivation so callers don't have to PATCH agent status explicitly — server bumps `agents.last_seen_at` on hot-path activity and derives `online` from recency.
What this PR ships
Migration collision warning
This PR's migration is numbered `024` against current OSS HEAD (023). Open OSS PR #46 (port of cueapi/cueapi#623 message send_at) ALSO targets `024`. Whichever lands first, the other rebases + renumbers to 025.
Tests
21 new (15 pure unit + 6 integration). 657 OSS regression passing.
Parity Impact
🤖 Generated with Claude Code