feat(cues): per-fire send_at scheduling on POST /v1/cues/{id}/fire (parity port of cueapi/cueapi#618)#45
feat(cues): per-fire send_at scheduling on POST /v1/cues/{id}/fire (parity port of cueapi/cueapi#618)#45mikemolinet wants to merge 3 commits into
Conversation
…arity port of cueapi/cueapi#618) Mirrors private PR #618 (Phase 12.1.7 / roadmap §13). Optional ``send_at`` timestamp on the fire body delays dispatch until the time elapses. Why this is small in OSS too: the dispatch loop in worker/poller.py:dispatch_outbox already gates on ``DispatchOutbox.scheduled_at`` (added in slice 3b for messages, ported in the messaging-primitive-port). This PR plumbs ``send_at`` from FireRequest through to ``Execution.scheduled_for`` and ``DispatchOutbox.scheduled_at``; no poller changes required. Semantics: - send_at omitted (or no body) → existing behavior: dispatch immediately, outbox.scheduled_at = NULL. - send_at in the future → execution.scheduled_for = send_at, outbox.scheduled_at = send_at. - send_at in the past → forgiving fallback to "fire now". No error. Worker-transport cues create no outbox row (existing invariant), but ``execution.scheduled_for`` still reflects send_at. Deviation from private: this PR does NOT include ``payload_override`` or ``merge_strategy`` on FireRequest. Those belong to private PR #575 (``require_payload_override`` enforcement) which is a separate parity port not yet ported. ``payload_override`` resolution can be added later without breaking this PR's API shape. Tests: 5 new pinning all 5 semantics paths (private has 6; the payload_override-compose case is dropped here per the deviation above). 32 regression tests green across cues/fire/outbox/poller suites. Files: - app/schemas/cue.py — new FireRequest model with send_at field - app/routers/cues.py — fire_cue accepts FireRequest body, plumbs send_at - tests/test_fire_send_at.py — 5 new tests - parity-manifest.json — bump cue.py + cues.py entries to 2026-05-05 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
|
Closing as stale — branch diverged from main by ~8878 deletions across 59 files (created pre-messaging-primitive cleanup). Rebase infeasible. Feature is still needed: Next step: re-port as fresh small PR against current main HEAD ( Branch retained for reference; can re-open or open fresh. |
Pull request was closed
…arity port of cueapi/cueapi#618) (#78) Re-port of closed [PR #45](#45) which was on a stale base ~8880 deletions behind main. Fresh against current main HEAD. Closes §13 / Phase 12.1.7 (cue side). Optional `send_at` timestamp on the fire body delays dispatch until the time elapses. ## Why this is small The dispatch loop in `worker/poller.py:dispatch_outbox` already gates on `DispatchOutbox.scheduled_at` (added in slice 3b for messages, just merged via PR #77 for messages send_at). This PR plumbs `send_at` from FireRequest through to `Execution.scheduled_for` and `DispatchOutbox.scheduled_at`. No poller changes required. ## What lands - **app/schemas/cue.py** — new `FireRequest` Pydantic model with optional `send_at: Optional[datetime]` field. - **app/routers/cues.py** — `fire_cue` endpoint accepts optional `body: Optional[FireRequest] = None`. Computes effective `scheduled_for` (future send_at → that timestamp; past or omitted → now). Sets `DispatchOutbox.scheduled_at` on the outbox row when scheduled. - **tests/test_fire_send_at.py** — 6 tests verbatim from private (5 active + 1 skipped). The skipped test relies on `payload_override` which is a separate parity port (cueapi/cueapi#589/#590 — not yet in cueapi-core); marked with a `@pytest.mark.skip` + reason pointer so it un-skips automatically when those ports land. ## Semantics (per private cueapi#618) - `send_at` omitted (or no body) → existing behavior: dispatch immediately, outbox.scheduled_at = NULL. - `send_at` in the future → execution.scheduled_for = send_at, outbox.scheduled_at = send_at, dispatcher gates until that time. - `send_at` in the past → forgiving fallback to "fire now". No error. Same shape as send_at omitted. Idempotent — caller doesn't have to worry about clock skew or being a few ms late. ## Tests 5 new tests pass (omitted, future-delays-dispatch, past-falls-back, invalid-timestamp-422, worker-transport-no-outbox). 1 skipped (composes-with-payload-override, depends on PR #589/#590 port). Full local suite: 834 passed + 18 xfailed (pre-existing) + 4 skipped (1 new, 3 pre-existing). Zero regressions. ## Re-port note Re-port of closed PR #45. Fresh against current main after PR #74 + #75 + #77 merged earlier in this session.
Summary
Parity port of cueapi/cueapi#618 (Phase 12.1.7 / roadmap §13). Optional
send_attimestamp on the fire body delays dispatch until the time elapses.The dispatch loop in
worker/poller.py:dispatch_outboxalready gates onDispatchOutbox.scheduled_at(added in slice 3b for messages, ported inmessaging-primitive-port). This PR plumbssend_atfrom FireRequest through toExecution.scheduled_forandDispatchOutbox.scheduled_at; no poller changes required.Semantics
send_atomitted (or no body) → existing behavior: dispatch immediately,outbox.scheduled_at = NULL.send_atin the future →execution.scheduled_for = send_at,outbox.scheduled_at = send_at.send_atin the past → forgiving fallback to "fire now". No error.execution.scheduled_forstill reflects send_at.Deviation from private
This PR does NOT include
payload_overrideormerge_strategyonFireRequest. Those belong to private PR #575 (require_payload_overrideenforcement) which is a separate parity port not yet ported.payload_overrideresolution can be added later without breaking this PR's API shape.Files changed
app/schemas/cue.py— newFireRequestmodel withsend_atfieldapp/routers/cues.py—fire_cueacceptsFireRequestbody, plumbssend_attests/test_fire_send_at.py— 5 new testsparity-manifest.json— bump cue.py + cues.py entries to 2026-05-05Tests
5 new (private has 6; the payload_override-compose case is dropped per the deviation above). 32 regression tests green across cues/fire/outbox/poller suites locally.
Parity Impact
client.cues.fire()needssend_atkwarg (Backlog row)cueapi fire <id> --send-atflag (Backlog row)🤖 Generated with Claude Code