fix(daemon): reset the console offset to the watermark on restart

When the daemon was restarted, the output-parse offset was reset to 0.
The daemon's output buffer is retained across runs, so the next poll
re-read the whole retained tail of the previous run and fed it to the
busy tracker as if it were fresh. A witness rebuild or rescan line left
over from the old run could then be mistaken for activity in the new
one.

- On restart the offset is now reset to the absolute end of the output
  at that moment, captured on the lifecycle thread after the stop and
  before the start, so the UI resumes parsing only the new run's output
  rather than replaying the old tail.
- The capture is handed to the UI thread through a pending flag and
  applied there before the next read, both on the normal poll and when
  skipping the backlog, so the offset is only ever moved on the thread
  that owns it.

Tests cover reading from the captured watermark (only the new run is
seen) versus the old reset-to-0 behaviour (the previous run replays).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 06:26:13 -05:00
co-authored by Claude Opus 4.8
parent e9042f091e
commit a6b7b966b0
6 changed files with 43 additions and 1 deletions
+12 -1
View File
@@ -333,7 +333,10 @@ public:
bool startDaemon() override { return app_.startEmbeddedDaemon(); }
void resetOutputOffset() override
{
app_.daemon_output_offset_ = 0;
// Capture the absolute end NOW (lifecycle thread, after stop / before start) so the UI resumes
// parsing only this run's output, not the retained tail of the previous run.
app_.daemon_output_reset_to_.store(app_.daemon_controller_ ? app_.daemon_controller_->outputEnd() : 0);
app_.daemon_output_reset_pending_.store(true);
app_.resetDaemonBusy();
}
@@ -1280,6 +1283,7 @@ void App::update()
// Check daemon output for rescan progress (offloaded to worker)
if (daemon_controller_ && daemon_controller_->isRunning()) {
applyPendingDaemonOutputReset();
std::string newOutput = daemon_controller_->outputSince(daemon_output_offset_);
if (!newOutput.empty()) {
feedDaemonBusy(newOutput); // witness rebuild / startup replay, for the quit guard + stop loop
@@ -5741,10 +5745,17 @@ void App::skipDaemonOutputBacklog()
if (daemon_controller_ && daemon_controller_->isRunning()) {
// Advances daemon_output_offset_ to the end. The busy tracker still sees it: whether the node is
// mid-rebuild must not depend on whether the window was minimized while the rebuild started.
applyPendingDaemonOutputReset();
feedDaemonBusy(daemon_controller_->outputSince(daemon_output_offset_));
}
}
void App::applyPendingDaemonOutputReset()
{
if (daemon_output_reset_pending_.exchange(false))
daemon_output_offset_ = daemon_output_reset_to_.load();
}
void App::handlePaymentURI(const std::string& uri)
{
auto payment = util::parsePaymentURI(uri);