Skip to content

fix: prefer newer server progress over stale local EPUB locator - #277

Open
bruno-b-martins wants to merge 10 commits into
van-geaux:mainfrom
bruno-b-martins:fix/issue-276-server-progress-resume
Open

bruno-b-martins wants to merge 10 commits into
van-geaux:mainfrom
bruno-b-martins:fix/issue-276-server-progress-resume

Conversation

@bruno-b-martins

@bruno-b-martins bruno-b-martins commented Oct 9, 2026 •

Copy link
Copy Markdown

Closes #276

Problem

Opening an EPUB always used the locator saved on the device, so progress made on another device was ignored and the book reopened at the old position, even though the book card showed the right percentage.

Fix

The reader now follows the fresh BookOrbit percent only when it is:

  • more than 2 points ahead of the saved locator (smaller gaps are renderer drift, Progress sync out of sync after update to 1.5.0 #96; progress that moved backwards is never followed), and
  • not this device's own write, meaning it is not within 1 point of the percent this device last reported.

Otherwise the exact local locator is kept. When the server wins, the reader positions from the server percent alone, starts the reading session from where it opens, and drops the old narration position unless narration is still playing.

The server percent is set only by a progress fetch that matched the file. Locators without a total progression (narration, annotations) are compared using the book's own positions.

Verification

Known limitations

  • Progress that moved backwards on another device is not followed.
  • Relaunching straight into an active reader (cold start or process death) keeps the local locator. Downloaded EPUBs restore through a local-only startup pass, so covering this means changing the startup flow.
  • Own writes are recognized only if the server returns the reported percent within 1 point. How BookOrbit rounds is not documented and was not checked.
  • Another client's write that lands ahead of the locator (for example marking a book as read) is followed.
  • Offline opens, and PDF and comic readers, are unchanged. Komga already passes the server locator.

🤖 Generated with Claude Code

bruno-b-martins and others added 8 commits October 9, 2026 18:27
…geaux#276)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…in (van-geaux#276)

Compare the fresh BookOrbit progress timestamp with the time the local
locator was saved, and require a material percent difference, so going
back on one device is no longer overridden. Ignore stale local page data
when the server position wins, and keep locators that cannot be compared
fairly.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…an-geaux#276)

Remember the BookOrbit progress update time this device last saw and
treat the server as newer only when it changed and points somewhere
materially different. Take the server percent and time only from a
matched progress entry. Read the saved locator's overall progress from
the book's positions when it lacks a total progression.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…an-geaux#276)

Compare the fresh server percent with what this device last reported
instead of tracking the server update time, so its own narration and
reader writes keep the exact locator. Start the reading session and drop
the stored narration position when the server position wins, clear stale
server fields before each fetch, and resolve the start position in one
place.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…an-geaux#276)

Follow server progress only when it is ahead of the saved locator, widen
the own-write tolerance for server rounding, drop the stored narration
position once the reader has moved, start the reading session at the
opened locator, and read the server percent from the book alone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…-geaux#276)

Start the reading session with the same percent formula the reader
reports later, clear stale server progress inside the repository fetch,
and leave the saved narration position alone while narration is active.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…geaux#276)

Fetch server progress and the last reported percent when restoring an
active EPUB reader, use the server value only once per launch, keep
narration of this book alive while it plays, share one percent
calculation for the session start, and reuse the guarded position search.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…anup until bound (van-geaux#276)

Use one total-progression lookup for the comparison, the session start
and the reported percent, wait for the narration service connection
before deciding whether narration is active, and drop the restore-path
fetch, which never runs for downloaded EPUBs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@van-geaux

Copy link
Copy Markdown
Owner

Thanks for the thorough investigation and implementation work on this.

I had not anticipated Lagrange being used across multiple devices with the same server configuration, so this uncovered an important gap in the EPUB resume behavior. Your work provides a strong basis for addressing it.

During review, I identified one remaining edge case around narration cleanup. When server progress wins, stale TTS position cleanup currently occurs from updateLocation() only if the TTS service is already bound. If the initial location update happens before the asynchronous service connection completes, cleanup can be skipped. An old narration position could then be reused later even though the reader resumed from newer server progress.

We are happy to take ownership of that remaining part. We will add a helper that can complete the cleanup from both updateLocation() and the TTS service onConnected callback, while preserving an active narration session, and add regression coverage for the service-binding race.

Version 2.0.0 is currently being prepared, so this work will be scheduled for a post-2.0.0 release. We will approve the completed implementation after the maintainer follow-up and validation are finished.

Thank you again for identifying and addressing this previously unanticipated multi-device use case.

…nects (van-geaux#276)

When server progress wins, the stored narration position was only
dropped from updateLocation, and only if the narration service was
already bound. If the first location update arrived before the
asynchronous service connection completed, the cleanup was skipped and
an old narration position could be reused later.

Track the cleanup in a small StaleNarrationPositionCleanup and complete
it from both updateLocation and the service onConnected callback,
whichever happens last, still keeping the position while narration of
this book is running. Add regression tests for both orderings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bruno-b-martins

Copy link
Copy Markdown
Author

Thanks for the review and for catching the binding race! To save you the follow-up, I've pushed 699ccb7: the cleanup now lives in a small StaleNarrationPositionCleanup and is completed from both updateLocation() and the TTS service onConnected callback, whichever comes last, still preserving an active narration session. It includes unit tests for both orderings. Happy for you to adjust or replace it however suits the post-2.0.0 plan.

🤖 Generated with Claude Code

…eaux#276)

The server progress extra is consumed when it wins, so a reader
recreated by rotation or process death before the cleanup completed
never dropped the narration position it replaced, and restored it from
saved state instead. Save whether the cleanup is still pending and
resume it in the recreated reader.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bruno-b-martins

Copy link
Copy Markdown
Author

One more follow-up: 0b3bca9 keeps a pending narration cleanup across reader recreation (rotation or process death). The server progress is consumed when it wins, so a reader recreated before the cleanup completed would otherwise restore the old narration position. The pending flag is now saved with the instance state and the cleanup resumes in the recreated reader, with unit tests covering it.

🤖 Generated with Claude Code

@van-geaux

Copy link
Copy Markdown
Owner

Thanks for the detailed implementation and for working through the TTS and recreation edge cases. I checked the current BookOrbit API and found that it does expose an updatedAt value in the book progress response. The underlying progress record also has more specific fields such as lastReadAt, textUpdatedAt, and narrationUpdatedAt.

There are some important caveats, though. BookOrbit’s server code does not treat updatedAt as a universal “last read” timestamp. Its meaning varies by writer, and the KOReader path deliberately freezes it. The more precise timestamp fields are available from the file-level progress endpoint, but not from the book-level endpoint that Lagrange currently uses.

Because of that, I’m not sure we can safely replace the current percentage-based rule with a simple timestamp comparison yet. Would you be open to discussing whether Lagrange should consume the file-level progress data, whether BookOrbit should expose a provider-neutral progress revision or authoritative text-position timestamp, or whether the current percentage fallback is the right short-term approach?

If we keep the percentage logic for now, I think we should document its thresholds and limitations clearly, then consider a server revision or text-position timestamp as a future improvement.

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.

EPUB resumes from stale local position instead of newer BookOrbit progress from another device

2 participants