mirror of
https://github.com/wu736139669/hapi.git
synced 2026-11-07 01:21:37 +00:00
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).
93 lines
4.7 KiB
Markdown
93 lines
4.7 KiB
Markdown
# 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.
|