Skip to content

Define a clock-free feed-head contract for first-run history #267

Open

Description

@bniladridas

Problem

Pepper CLI's claude-channel decides which feed messages are history by comparing each event's created_at with a local process timestamp minus 60 seconds (ChannelWorker.cutoff, _handle_event in src/pepper_cli/claude_channel.py). The client has no server timestamp that shares a clock with created_at. The HTTP Date header is generated at the HTTP layer and can come from an intermediary, so it cannot move the boundary safely in either direction: a Date ahead hides live messages, and a Date behind pushes retained history as live. #255 tracks the remaining limitation. #261 only rolls back an HTTP Date attempt and records the limitation; it does not resolve this issue.

This issue is filed in the Vycari/pepper-cli repository, where the client-side work is tracked. The separate Vycari/pepper repository contains the feed-server implementation relevant to the server contract described here. That repository is not reachable from the accounts currently available for this investigation.

Existing protocol and proposal

Pepper CLI already receives an opening cursor on a positionless start=head stream. The client records it as a resume position and treats its arrival as the live edge. A focused client change tracked under #255 (PR #268) uses that opening cursor as the first-run history boundary.

The client-side change uses this existing opening cursor: on a first start=head connection with no saved position, it treats event IDs above the opening cursor as live and bypasses the local clock cutoff for those events. Keep the existing cutoff for --start beginning, older servers that do not send an opening cursor, and replay from a saved resume position. That client change is tracked under #255 (PR #268) and does not wait for this server contract request.

This issue tracks the stronger server-side guarantees needed to rely on the opening cursor as a complete history boundary. The server should expose or formally guarantee the subscription-time feed head on each stream. The existing cursor frame may serve as that marker; a separate head frame is another option if the existing frame cannot provide the required semantics.

If a dedicated head frame is chosen, a JSON payload such as data: {"head": 123} is sufficient. It must not carry an SSE id: line (see Compatibility).

Contract requirements

  1. Atomicity. Capturing the head and starting delivery must not leave a gap. An event committed after the captured head must still reach the connection, either in replay or as live. It must not fall between the two.
  2. Ordering. The head marker must arrive before any replayed event on a new connection. Its value must come from the same monotonically increasing event-ID sequence used by all three feeds. There must be no per-room or per-topic sequence.
  3. Classification. On a first connection with no saved position, an event id at or below the head is history and an id above the head is live. The same rule must hold on all three endpoints:
    • /api/v1/groups/events/stream
    • /api/v1/groups/{group_id}/events/stream
    • /api/v1/groups/{group_id}/topics/{topic_id}/events/stream
  4. Resume. A connection with Last-Event-ID continues after that position as today. If a head marker is also sent, it must not cause events above the saved position to be reclassified as history. The existing cursor_ahead recovery behavior is unchanged.
  5. Compatibility. Existing clients ignore SSE frames they do not know only when the frame has no id: line. With an id:, Pepper CLI's claude-channel treats the unknown kind as an event, and because its payload carries no room scope, the channel currently stops with "the server sent an event from outside the requested scope". A dedicated head frame must therefore have no SSE id:. Its value belongs in the JSON payload. Frames without an id are ignored by claude-channel, rooms watch, and the TUI.

Acceptance criteria

  • No event is lost between capturing the head and subscribing to the feed.
  • Events at or below the head are classified as history on a first connection with no saved position.
  • Events above the head are classified as live on that connection.
  • The same boundary semantics apply to all three endpoints.
  • Resume from a saved Last-Event-ID retains its existing behavior and does not reuse the first-connection history boundary.

Client follow-up

The client-side first-run change is tracked separately under #255 (PR #268) and uses the existing opening cursor. This issue is limited to confirming and documenting the server-side atomicity, ordering, and cross-endpoint guarantees. The client change does not need to wait for this issue to be resolved.

Refs #255.

Verification note

The server-side requirements have not been verified against the implementation in Vycari/pepper. The endpoints and likely implementation points (src/pepper/api/groups.py::agent_events_stream, room_events_stream, topic_events_stream; src/pepper/gateway/channels/groups_service/agent_feed.py::scope_frame) come from MIRROR comments and the API contract snapshot in Vycari/pepper-cli, not from direct inspection of the feed-server repository. Attempts to access Vycari/pepper with the currently available accounts return 404.

Activity

  1. changed the title [-]Server contract: expose the feed head as a clock-free history boundary[/-] [+]Superseded: client-side first-run cutoff is tracked in #255[/+] on Nov 15, 2026
  2. changed the title [-]Superseded: client-side first-run cutoff is tracked in #255[/-] [+]Server contract: expose the feed head as a clock-free history boundary[/+] on Nov 15, 2026
  3. changed the title [-]Server contract: expose the feed head as a clock-free history boundary[/-] [+]Define a clock-free feed-head contract for first-run history[/+] on Nov 15, 2026
  4. bniladridas commented on Nov 15, 2026

    @bniladridas
    ContributorAuthor

    Requesting confirmation from the maintainers of the feed server in Vycari/pepper before #268 leaves draft. Direct inspection is still blocked because that repository returns 404 for the accounts available here, so confirmation on this issue is the remaining check. If the feed-server maintainers do not watch this repository, please route this comment to them.

    The client now uses the opening start=head cursor as the first-run boundary. Three properties need to be confirmed against the server implementation:

    1. The opening cursor value equals the actual feed head at subscription time.
    2. Capturing that head and starting delivery are atomic: an event committed after the captured head cannot fall between replay and live delivery.
    3. Every event committed after the captured head is delivered with an event ID greater than the head, using the same cross-feed event sequence on all three stream endpoints.

    If any property is not guaranteed, please describe the actual behavior and the smallest contract change needed. #268 will stay draft until this is confirmed here. Refs #268. Refs #255.

  5. bniladridas commented on Nov 15, 2026

    @bniladridas
    ContributorAuthor

    Per .github/CODEOWNERS, @allenhutchison owns this repository. @allenhutchison, can you confirm the three properties above, or route this to the feed-server maintainers for Vycari/pepper? #268 remains draft until the guarantees are confirmed on this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions