Files
hapi/docs/api/client-contract/read-state.md
T
weishu 6ed83eda3c fix: show unread for running sessions
Running sessions previously suppressed the ordinary unread dot, hiding
answers the user had not seen. Unread is now
manualUnread || contentRevision > readRevision on web, iOS and Android.

Web rows draw the running/background spinner beside the unread dot, and
unread outranks background status. Because session patches arrive before
the message they count, a chat following its visible tail hides its own
ordinary dot until the rendered acknowledgement lands (display-only; web
and iPad split view).
2026-11-01 15:39:46 +08:00

93 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Session read state
Sources: `shared/src/sessionReadState.ts`, `shared/src/messages.ts`,
`hub/src/store/sessionReadState.ts`, `hub/src/sync/messageService.ts`.
Read state is shared by Web, iOS and Android within one Hub and JWT namespace.
CLI terminal activity does not acknowledge reading. `updatedAt`, message `seq`,
permission state and transport heartbeat are independent of unread status.
## Revisions and unread
Session, SessionSummary and session SSE patches carry:
```ts
contentRevision: number
readState: { readRevision: number; manualUnread: boolean; version: number }
```
The hub increments `contentRevision` atomically with a new readable assistant
message: answer text, rendered media or an error. User prompts, reasoning, tool
traffic, usage, metadata and heartbeats do not advance it. Imported/copied
history starts at zero; live delayed output counts. A replay with the same
`localId` keeps its original revision. Meaningful message rows carry their
`contentRevision` in REST and SSE.
Unread is `manualUnread || contentRevision > readRevision`, including while the
session is running; clients show running activity beside the unread dot. Input
and permission attention remain independent. Selecting a session does not make it read.
Session patches can arrive before the message they count, so a client may hide the
ordinary dot of a session whose visible chat follows its tail (the same conditions as
an automatic read below) until that output renders and is acknowledged. This is
display-only and never sends a read; manual unread stays visible.
Clients merge revisions monotonically and use the highest read-state `version`
for the manual flag, including when REST and SSE arrive out of order.
## Read acknowledgement
`PUT /api/sessions/:id/read-state` accepts one of:
```json
{"type":"read","readRevision":12,"expectedVersion":4}
```
```json
{"type":"unread","expectedVersion":4}
```
The response includes `{status, contentRevision, readState}`. Counters must be
nonnegative safe integers. An invalid/future revision returns 400. Manual unread
uses compare-and-set: a mismatched version returns 409 with authoritative state.
Read requests advance by maximum; they clear manual unread only when
`expectedVersion` matches. Thus an older automatic read cannot erase a newer
manual choice, and an acknowledgement of revision 12 cannot consume revision 13.
An old session ID resolving to a replacement returns 409: revisions cannot be
replayed against a different session. Namespace access is checked before mutation.
Read writes emit `session-updated` patches without advancing `updatedAt`.
Automatic reads require a visible foreground chat, successful message loading,
and the latest tail laid out on screen. Background, history browsing, hidden
inspectors/terminals, failed loads and list navigation cannot acknowledge reading.
A read-state-only update is not a new viewing action. Explicit “mark all read”
uses the content revisions observed in that list action.
Latest message pages carry `page.readThroughRevision`. A completed forward
catch-up carries it only if its snapshot still reaches the current head. Before
pages and partial/bounded catch-ups cannot certify a later head. Clients retain
the receipt with the corresponding message window; they can advance it with
rendered incoming message revisions after initial tail synchronization. Never
substitute the summary’s head revision for a rendered window receipt.
## Offline and lifecycle
Persist the exact observed revision and expected version in a durable outbox
before presenting a successful local read. Retry transient failures, including
after restart/foregrounding. Each operation has an ID so an old response cannot
remove a newer queued operation. Authoritative 400/403/404/409 rejection rolls
back the optimistic mark; do not blindly rebase onto a newer manual choice.
Storage keys include Hub origin and JWT namespace; never derive namespace by
splitting the pairing access token. Switching Hub must finish pending disk writes.
## Upgrade and history changes
Database v30 adds persistent read fields with zero defaults. Existing history
becomes a read baseline once; old client timestamp/manual-marker files are ignored.
This clears old unread dots on upgrade, while pending approvals/input remain.
Update Hub and clients together; clients do not fall back to timestamp-based reads.
Fork/import copies are baseline history. A session merge translates unread source
revisions above the destination head, transfers manual unread, and retires the
source state. Client windows seeded across IDs discard old message revisions and
fetch a fresh tail. Rewind never reuses revisions; if all unread output is removed,
it retires that output while preserving an explicit manual-unread choice.