Skip to content

feat(cues): per-fire send_at scheduling on POST /v1/cues/{id}/fire (parity port of cueapi/cueapi#618)#45

Closed
mikemolinet wants to merge 3 commits into
mainfrom
port/618-cue-fire-send-at
Closed

feat(cues): per-fire send_at scheduling on POST /v1/cues/{id}/fire (parity port of cueapi/cueapi#618)#45
mikemolinet wants to merge 3 commits into
mainfrom
port/618-cue-fire-send-at

Conversation

@mikemolinet
Copy link
Copy Markdown
Collaborator

Summary

Parity port of cueapi/cueapi#618 (Phase 12.1.7 / roadmap §13). Optional send_at timestamp on the fire body delays dispatch until the time elapses.

The dispatch loop in worker/poller.py:dispatch_outbox already gates on DispatchOutbox.scheduled_at (added in slice 3b for messages, ported in 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 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.

Files changed

  • app/schemas/cue.py — new FireRequest model with send_at field
  • app/routers/cues.pyfire_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

Tests

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

  • cueapi-core ✓ (this PR)
  • cueapi-python — TODO: client.cues.fire() needs send_at kwarg (Backlog row)
  • cueapi-cli — TODO: cueapi fire <id> --send-at flag (Backlog row)
  • cueapi-mcp — TODO: cue-mac-app's lane (Backlog row)
  • cueapi-worker — N/A (worker doesn't fire cues)
  • cueapi-action — N/A
  • homebrew-tap — N/A
  • cuechain — N/A

🤖 Generated with Claude Code

…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>
@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/routers/cues.py
  • app/schemas/cue.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.

mikemolinet added a commit that referenced this pull request May 7, 2026
…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
Copy link
Copy Markdown
Collaborator Author

Closing as stale — branch diverged from main by ~8878 deletions across 59 files (created pre-messaging-primitive cleanup). Rebase infeasible.

Feature is still needed: send_at is absent from cueapi-core's app/routers/cues.py + app/schemas/cue.py. Parity port of cueapi/cueapi#618 has not landed.

Next step: re-port as fresh small PR against current main HEAD (f9ec4ea). The cueapi-cli + cueapi-mcp + cueapi-action + cueapi-python sides all shipped earlier (sessions 2026-05-10); only the cueapi-core server-side port remains.

Branch retained for reference; can re-open or open fresh.

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
…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.
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