Skip to content

feat(messaging): state-machine invariant + narrow-response surfaces delivered_at (parity port of PR #923 + #927)#101

Merged
mikemolinet merged 1 commit into
mainfrom
primary/messaging-v1.1.6-batch
May 22, 2026
Merged

feat(messaging): state-machine invariant + narrow-response surfaces delivered_at (parity port of PR #923 + #927)#101
mikemolinet merged 1 commit into
mainfrom
primary/messaging-v1.1.6-batch

Conversation

@mikemolinet
Copy link
Copy Markdown
Collaborator

Summary

Batched OSS port of two prod-verified private cueapi fixes:

  • Finding A (private cueapi PR #923 mergeCommit 7a29082): state-machine invariant fix — mark_read + mark_acked fill delivered_at (and read_at for mark_acked) when null, so bypass paths don't produce impossible delivery_state=read|acked + delivered_at=NULL fingerprints
  • cmpg8jokl (private cueapi PR #927 mergeCommit 34e3506): narrow-response observability follow-on — StateTransitionResponse surfaces delivered_at (and full setter timestamps on /ack) so consumers see the just-set value without a follow-up GET

Targets messaging-v1.1.6 tag bump (supersedes messaging-v1.1.5-hotfix).

Deviation from private

cueapi-core has no bulk_mark_read (hosted-only per parity-manifest). Third setter site from private PR #923 is omitted; only mark_read + mark_acked apply on OSS. Verified via grep -rn "bulk" app/ — only events.py:advance_ack_watermark matches, not messaging path.

Changes (purely additive per API Contract Rule)

app/services/message_service.py:

  • mark_read: if msg.delivered_at is None: msg.delivered_at = now
  • mark_acked: dual-null-check sets both delivered_at AND read_at to now if null

app/schemas/message.py:

  • StateTransitionResponse adds delivered_at: Optional[datetime] = None + WHY-rationale docstring

app/routers/messages.py:

  • /read endpoint populates delivered_at=msg.delivered_at
  • /ack endpoint populates all 3 setter timestamps (delivered_at + read_at + acked_at)

Tests

4 new regression guards in tests/test_messages.py:

  • test_mark_read_fills_delivered_at_when_null — Finding A invariant on fast-read
  • test_mark_read_preserves_existing_delivered_at — preserves post-poll value
  • test_mark_acked_fills_delivered_at_and_read_at_when_null — symmetric dual-null-check
  • test_mark_acked_preserves_existing_timestamps — preserves poll→read→ack chain

22/22 message tests pass locally (existing 18 + 4 new).

Empirical verification on private (pre-port)

Both fixes empirically prod-verified before this port:

  • Finding A T1+T3+T5 via path-d-test-recipient fixture on api.cueapi.ai
  • cmpg8jokl T1+T5 via same fixture
  • Verbatim evidence in private cycle cues msg_sohd1cnhch7q + msg_3swztwmrsk68

Backward compatibility

Purely additive per CLAUDE.md API Contract Rule. Existing consumers reading the previously-returned fields see no change. New consumers can opt into delivered_at. No removal, no rename.

Pre-PR reviews

  • cue-pm GREENLIT batched OSS port plan into v1.1.6: msg_fggb87nn5zkb (private cycle)
  • Both fixes had 3-of-3 G11-β CONCUR on private (Finding A + cmpg8jokl); same code shape applies here with OSS sanitization
  • M3 carry-forward cross-eyes to canonical Secondary + Three pending after push

Test plan

  • 22/22 message tests pass locally on cueapi_core_test DB
  • CI test SUCCESS (parity-check + sdk-integration + test)
  • G11-β re-CONCUR at pushed HEAD ecd30cb
  • Admin-merge to main
  • Tag messaging-v1.1.6 against squash
  • Notify downstream consumers (cue.dock.svc et al) to bump pin

🤖 Generated with Claude Code

…rfaces delivered_at (parity port of PR #923 + #927)

Batched OSS port of two private cueapi fixes:

**Finding A** (private cueapi PR #923 mergeCommit 7a29082): state-
machine invariant fix. Pre-fix: mark_read on queued msg →
delivery_state="read" with delivered_at=NULL (impossible per
canonical queued → delivered → read → acked path). Same fingerprint
anomaly on mark_acked from queued state. Caused diagnostic
confusion in private-side investigations.

Fix: mark_read fills delivered_at=now when null; mark_acked
dual-null-check fills both delivered_at + read_at when null. Pure-
SELECT bypass paths now produce equivalent timestamps; invariant
holds across all entry points.

Note: cueapi-core has no `bulk_mark_read` (hosted-only per parity-
manifest); third setter site from private #923 is omitted from this
port. Only mark_read + mark_acked apply on OSS.

**cmpg8jokl** (private cueapi PR #927 mergeCommit 34e3506): narrow-
response observability follow-on. After Finding A fills delivered_at
on /read + /ack, the narrow StateTransitionResponse omitted the
field. Consumers couldn't observe the just-set value without a
follow-up GET /v1/messages/{id}.

Fix (additive only per API Contract Rule):
- StateTransitionResponse adds `delivered_at: Optional[datetime] = None`
- /read endpoint populates delivered_at
- /ack endpoint populates all 3 setter timestamps (delivered_at +
  read_at + acked_at) symmetric with mark_acked's dual-null-check

Tests:
- 4 new regression guards in tests/test_messages.py:
  * test_mark_read_fills_delivered_at_when_null
  * test_mark_read_preserves_existing_delivered_at
  * test_mark_acked_fills_delivered_at_and_read_at_when_null
  * test_mark_acked_preserves_existing_timestamps
- 22/22 message tests pass (existing 18 + 4 new)

Both private fixes empirically prod-verified before this port:
- Finding A T1+T3+T5 via path-d-test-recipient fixture
- cmpg8jokl T1+T5 via path-d-test-recipient fixture

Targets tag bump to messaging-v1.1.6 (supersedes v1.1.5-hotfix).
Downstream consumers (cue.dock.svc et al) should bump pin once tag cuts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

Parity check

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

  • app/routers/messages.py
  • app/schemas/message.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 22, 2026 14:40
@mikemolinet mikemolinet merged commit bf4b27f into main May 22, 2026
6 checks passed
@mikemolinet mikemolinet deleted the primary/messaging-v1.1.6-batch branch May 22, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant