Skip to content

feat(messages): per-message send_at on POST /v1/messages (parity port of cueapi/cueapi#623)#46

Closed
mikemolinet wants to merge 1 commit into
mainfrom
port/623-message-send-at
Closed

feat(messages): per-message send_at on POST /v1/messages (parity port of cueapi/cueapi#623)#46
mikemolinet wants to merge 1 commit into
mainfrom
port/623-message-send-at

Conversation

@mikemolinet
Copy link
Copy Markdown
Collaborator

Summary

Parity port of cueapi/cueapi#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 (PR #45 here / 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. Existing rows default to NULL = "send now" (full back-compat).
  • MessageCreate + MessageResponse schemas grow send_at: Optional[datetime].
  • 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 (treated as "send now").
  • list_inbox gates with Message.send_at IS NULL OR send_at <= now() on both the read query AND the queued→delivered transition UPDATE.
  • list_sent unchanged — sender SHOULD see their scheduled messages.

Files changed

  • alembic/versions/024_message_send_at.py — new migration (renumbered from private's 047)
  • app/models/message.pysend_at column
  • app/schemas/message.pysend_at on MessageCreate + MessageResponse
  • app/services/message_service.pysend_at parameter + plumbing
  • app/services/inbox_service.pysend_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 (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

  • cueapi-core ✓ (this PR)
  • cueapi-python — TODO: client.messages.create(send_at=...) (Backlog row)
  • cueapi-mcp — TODO: cue-mac-app's lane (Backlog row)
  • cueapi-cli — N/A (no message create CLI yet)
  • cueapi-worker — N/A
  • cueapi-action — N/A
  • homebrew-tap — N/A
  • cuechain — 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/message.py
  • app/routers/messages.py
  • app/schemas/message.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.

…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>
@mikemolinet mikemolinet force-pushed the port/623-message-send-at branch from 885e3d0 to fd4396e Compare May 7, 2026 00:34
mikemolinet added a commit that referenced this pull request May 7, 2026
…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
Copy link
Copy Markdown
Collaborator Author

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: send_at is absent from cueapi-core's app/schemas/message.py + app/routers/messages.py. Parity port of cueapi/cueapi#623 has not landed in the SERVER side (cli/mcp/python sides shipped 2026-05-10).

Next step: re-port as fresh small PR against current main HEAD (f9ec4ea).

Branch retained for reference.

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

Pull request was closed

mikemolinet added a commit that referenced this pull request May 11, 2026
…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).
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