Skip to content

feat(agents): roster endpoint + last_seen_at + ETag (parity port of cueapi/cueapi#630)#47

Closed
mikemolinet wants to merge 1 commit into
mainfrom
port/630-agent-roster
Closed

feat(agents): roster endpoint + last_seen_at + ETag (parity port of cueapi/cueapi#630)#47
mikemolinet wants to merge 1 commit into
mainfrom
port/630-agent-roster

Conversation

@mikemolinet
Copy link
Copy Markdown
Collaborator

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 024 — `agents.last_seen_at TIMESTAMPTZ NULL`. Nullable, no backfill.
  • `GET /v1/agents/roster` — display-optimized snapshot for session-boot prompt injection. Drops opaque IDs / secrets / timestamps; adds derived `online` / `last_seen_relative` / `preferred_contact`. Weak ETag + `If-None-Match` → 304.
  • Hot-path hooks for `create_message` (sender) + `list_inbox` (recipient).
  • Online derivation: ≤5min online, ≤30min away, older offline. Caller override wins.

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

  • cueapi-core ✓ (this PR)
  • cueapi-python — Phase A consumer-side (deferred)
  • cueapi-cli — `agents list --online-only` / `agents describe` (Backlog row)
  • cueapi-mcp — cue-mac-app's lane
  • cueapi-worker — N/A
  • cueapi-action — N/A

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

Parity check

This PR modifies files tracked in parity-manifest.json:

  • app/models/agent.py
  • app/routers/agents.py
  • app/schemas/agent.py
  • app/services/agent_service.py
  • app/services/inbox_service.py
  • app/services/message_service.py

Please confirm one of the following in a reply or PR description update:

  1. The equivalent change has been applied to the private cueapi monorepo. Link the PR.
  2. This change is OSS-only and does not need porting. Briefly explain why (e.g. "fixes a bug that only exists in the OSS build").
  3. A follow-up issue has been filed to port the reverse direction. Link the issue.

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.

@govindkavaturi-art govindkavaturi-art enabled auto-merge (squash) May 5, 2026 20:51
@mikemolinet
Copy link
Copy Markdown
Collaborator Author

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>
@mikemolinet mikemolinet force-pushed the port/630-agent-roster branch from 4e25e80 to f3b5a62 Compare May 7, 2026 00:38
@mikemolinet
Copy link
Copy Markdown
Collaborator Author

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: last_seen_at column + /v1/agents/roster endpoint are absent from cueapi-core. Parity port of cueapi/cueapi#630 has not landed in SERVER side.

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 (f9ec4ea).

Branch retained for reference.

auto-merge was automatically disabled May 11, 2026 15:33

Pull request was closed

mikemolinet added a commit that referenced this pull request May 11, 2026
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant