| `error` | `session_hold_failed` | `{op, sessionId}` — an open session could not be held on disk; see [below](#a-session-the-app-never-saw-end-ended-unseen) |
### A session the app never saw end (`ended: unseen`)
Added 2026-09-13 (amber-app, first in the build after 1.3.0+38). Until then the
summary and the amber-api health row were written only when the player closed, and
a box whose television is switched off with the film on screen never closes it.
Measured on the kids' box `Xiaomi MIBOX4 · ff3bcf`: **13 of 13 films in 14 days
left no summary and no health row.** At the end of every one the 5 s position
save ran at full rate up to the last 45 s `api_usage` tick and then stopped, and
no row from the hide/pause flush arrived, then or at any later launch (0 of 3572
`api_usage` rows were delivered late). That box gives the app no callback, so a
write on `AppLifecycleState.paused` would have recovered none of them.
Now both players hold the open session on disk every 10 s
(`<appSupport>/open_sessions/<sessionId>.json`). A normal close writes as before
and deletes the copy once the summary is in the telemetry spool. The next launch
writes whatever is still held, through the same single writer, with:
| field | meaning |
|---|---|
| `ended` | `closed`: the player closed and wrote it. `unseen`: a later launch wrote it from the held copy. Absent on older builds. |
| `lastSeenAt` | `unseen` only. When the session was last held, device clock, UTC. The row's own `ts` and `created` are the later launch, often the next day. |
| `playedOn` | `unseen` only, and only when it differs: the `appVersion gitSha` that played it, because a row is stamped with the build that sends it. |
Reading an `unseen` row:
- The counters stop at `lastSeenAt`, at most 10 s before the end, and
`promptOutcome` is `notShown`.
- It does not say how the session ended (TV switched off, power cut, crash, killed
in the background), only that the app did not see it.
- It arrives when the box is next started. A complaint made before that start has
no row yet.
- Its amber-api health row is sent at the same moment, once. A session that was
held and then closed normally sends one health row, at the close.
- A session killed within its first 10 s was never held and still leaves nothing.
`kind: error`, `event: session_hold_failed`, `meta: {op, sessionId}` records a
hold that could not be written (`put`), let go of (`release`), read back
(`unreadable`, the copy is deleted) or listed (`recover`). One row per session per