feat(messages): per-message send_at on POST /v1/messages (parity port of cueapi/cueapi#623)#46
feat(messages): per-message send_at on POST /v1/messages (parity port of cueapi/cueapi#623)#46mikemolinet 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. |
…parity port of cueapi/cueapi#623) Mirrors private PR #623 (Phase 12.1.7 / roadmap §13). Optional ``send_at`` timestamp on MessageCreate delays delivery until the time elapses. Same shape as cue-fire send_at (port #45 / cueapi/cueapi#618). Implementation surfaces: * **Migration 024** (renumbered from private's 047 to fit OSS sequence after existing 023_messaging_primitive_multi_shell.py) adds ``messages.send_at TIMESTAMPTZ NULL`` plus a partial index ``ix_messages_send_at`` (WHERE send_at IS NOT NULL) built CONCURRENTLY to avoid ACCESS EXCLUSIVE lock on a potentially large messages table. Existing rows default to NULL = "send now" (full back-compat). * **MessageCreate** + **MessageResponse** schemas grow ``send_at: Optional[datetime]``. ``extra="forbid"`` already in place. * **create_message** plumbs ``send_at`` into both ``Message.send_at`` and ``DispatchOutbox.scheduled_at`` so push-delivery dispatch is also gated. Past timestamps are forgiving fallback ("send now") — caller doesn't have to worry about clock skew. * **list_inbox** gates with ``Message.send_at IS NULL OR send_at <= now()`` on both the read query AND the queued→delivered transition UPDATE. Recipients can't see scheduled messages until their time; the atomic poll-fetch transition skips them too. * **list_sent** unchanged — sender SHOULD see their scheduled messages (they queued them deliberately). Files: - alembic/versions/024_message_send_at.py — new migration - app/models/message.py — send_at column - app/schemas/message.py — send_at on MessageCreate + MessageResponse - app/services/message_service.py — send_at parameter + plumbing - app/services/inbox_service.py — send_at gate on inbox query + transition UPDATE - app/routers/messages.py — pass send_at from body to service - tests/test_message_send_at.py — 7 new tests - parity-manifest.json — bump 5 entries to 2026-05-05 Tests: 7 new. 660 passed total (excluding 7 pre-existing SDK test failures). Phase 12.1.7 OSS port complete: cue-fire send_at (port/618 / PR #45) + message send_at (this PR). §17 BCC light (private PR #619) is a separate parity port not yet ported here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
885e3d0 to
fd4396e
Compare
…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>
|
Closing as stale — branch diverged from main by ~8901 deletions across 59 files. CI is FAILURE on this branch (test + sdk-integration). Rebase infeasible given the drift. Feature is still needed: Next step: re-port as fresh small PR against current main HEAD ( Branch retained for reference. |
Pull request was closed
…parity port of cueapi/cueapi#623) (#77) Re-port of closed [PR #46](#46) which was on a stale base ~8900 deletions behind main. Fresh against current main HEAD. Closes §13 / Phase 12.1.7 — messages-side complement to the cue-fire send_at shipped in PR #618 (which still needs its own re-port). ## What lands - **alembic/versions/030_message_send_at.py** (renumbered from private's 047) Adds `messages.send_at TIMESTAMPTZ NULL` + partial index `ix_messages_send_at` (WHERE send_at IS NOT NULL) built CONCURRENTLY so the index creation doesn't take an ACCESS EXCLUSIVE lock on the messages table during deploy. Existing rows default to NULL = "send now". - **app/models/message.py** — adds `send_at` Column. - **app/schemas/message.py** — `send_at: Optional[datetime]` on MessageCreate + MessageResponse. - **app/routers/messages.py** — passes `send_at=body.send_at` into `create_message`. - **app/services/inbox_service.py** — gates inbox query AND queued→delivered transition UPDATE with `send_at IS NULL OR send_at <= now()`. Scheduled messages are invisible until their time; the atomic poll-fetch transition skips them too. - **app/services/message_service.py** — `create_message` plumbs `send_at` into both `Message.send_at` and `DispatchOutbox.scheduled_at`. Past timestamps are forgiving fallback (treated as send-now). `to_response_dict` surfaces the persisted value. - **tests/test_message_send_at.py** — 7 new tests verbatim from private cueapi covering all 7 semantic paths (omitted, future-invisible, outbox-scheduled, past-fallback, becomes-visible- after-pass, sender-view-shows-it, invalid-timestamp). - **parity-manifest.json** — new entry for migration 030 under `message-send-at-port (private #623)`. ## Wire format `send_at` flows in the BODY of `POST /v1/messages` (server contract: `MessageCreate.send_at`). Same shape as cue-fire `send_at` (PR #618). NULL = send now (default). Future timestamp = inbox-gate + DispatchOutbox.scheduled_at. Past timestamp = forgiving send-now. ## Tests 7/7 new tests pass locally. Full local suite: 836 passed + 18 xfailed (pre-existing) + 3 skipped. Zero regressions. ## Sibling ports - cli, mcp, python, action sides shipped via session 2 (2026-05-10): cueapi-cli #48, cueapi-mcp #33, cueapi-python (private), cueapi-action #12. This is the cueapi-core (OSS server) side that was still missing. ## Re-port note Re-port of closed PR #46. That branch was ~8900 deletions behind main; fresh port against current main HEAD (after PR #74 + #75 merged earlier in this session).
Summary
Parity port of cueapi/cueapi#623 (Phase 12.1.7 / roadmap §13). Optional
send_attimestamp on MessageCreate delays delivery until the time elapses. Same shape as cue-fire send_at (PR #45 here / cueapi/cueapi#618).Implementation surfaces
023_messaging_primitive_multi_shell.py) addsmessages.send_at TIMESTAMPTZ NULLplus a partial indexix_messages_send_at(WHERE send_at IS NOT NULL) built CONCURRENTLY. Existing rows default to NULL = "send now" (full back-compat).send_at: Optional[datetime].send_atinto bothMessage.send_atandDispatchOutbox.scheduled_atso push-delivery dispatch is also gated. Past timestamps are forgiving fallback (treated as "send now").Message.send_at IS NULL OR send_at <= now()on both the read query AND the queued→delivered transition UPDATE.Files changed
alembic/versions/024_message_send_at.py— new migration (renumbered from private's 047)app/models/message.py—send_atcolumnapp/schemas/message.py—send_aton MessageCreate + MessageResponseapp/services/message_service.py—send_atparameter + plumbingapp/services/inbox_service.py—send_atgate on inbox query + transition UPDATEapp/routers/messages.py— passsend_atfrom body to servicetests/test_message_send_at.py— 7 new testsparity-manifest.json— bump 5 entries to 2026-05-05Tests
7 new (matching private). 660 passed locally (excluding 7 pre-existing SDK test failures unrelated to this PR).
Phase 12.1.7 OSS port progress
Parity Impact
client.messages.create(send_at=...)(Backlog row)🤖 Generated with Claude Code