fix(api): add trailing-edge DB flush for streamed interaction writes - #2848
Merged
Conversation
The streaming DB write is throttled to dbWriteInterval (5s) leading-edge with no trailing flush, so when an agent pauses mid-turn (e.g. before a tool call) the persisted interaction sits up to 5s behind the live stream. The frontend publish path already had a trailing flushTimer; the DB path did not. Any consumer reading the DB during that window — the useLiveInteraction 3s-poll fallback, a page-reload snapshot, or any other reader — saw stale/truncated text. Mirror the publish flushTimer with a dbFlushTimer (500ms): a throttled streaming update now schedules a trailing DB flush that persists the latest content shortly after the last chunk. Continuous streaming keeps resetting the timer, so writes still happen at the 5s leading cadence (bounding TOAST churn); only pauses and burst tails trigger a catch-up write. Extract the write into flushStreamingFieldsToDB (column-scoped, shared by both paths) and stop the timer at the existing teardown points. Verified live in the inner Helix: DB catch-up on a mid-turn pause drops from up to 5s to ~500ms; the live view was already current and is unchanged. Adds regression test TestMessageAdded_TrailingDBFlush. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Spec-Ref: helix-specs@e9bbf4f02:002212_read-design2026-07-03
…ec-task-detail Spec-Ref: helix-specs@e9bbf4f02:002212_read-design2026-07-03
…ec-task-detail Spec-Ref: helix-specs@a779e18cb:002212_read-design2026-07-03
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the spec-task detail live-message truncation/lag investigation
(
design/2026-07-03-spectask-live-message-truncation.md).The task named two candidate root causes. Instrumenting both ends of the live
path in the inner Helix and running the repro (a text turn, a text→tool_call turn
with a 25s pause, and a long prose turn) showed:
currentResponses(src=LIVE), never the DB fallback. Cause Website #1's frontendsymptom did not occur.
TRAILING-FLUSHpublishesfire ~50ms after each burst and the frontend tracked them to completion. The
live-view lag the doc describes was already fixed by the publish
flushTimer(commit
6fdef74a4, 2026-04-25), so the reported text truncation no longerreproduces on
main.interactions.response_messageduring a streaming turn showed it advancing in~5s steps: the streaming DB write is throttled to
dbWriteInterval(5s)leading-edge with no trailing flush, while the live stream is current to
~50ms. The publish path had a trailing flush; the DB path did not. So any
consumer that reads the DB mid-stream — the
useLiveInteraction3s-pollfallback (cause Website #1's mechanism), a page-reload snapshot, or any other reader —
saw up to 5s-stale/truncated text.
This PR closes that asymmetry by mirroring the publish
flushTimerwith atrailing-edge DB flush.
Changes
streamingContext.dbFlushTimeranddbTrailingFlushInterval = 500ms.flushStreamingFieldsToDB(sctx)(callerholds
sctx.mu; column-scoped so it never clobbers state/completed/error),shared by the leading-edge write and the new trailing flush.
pending
dbFlushTimer; the newelsebranch schedules/reschedules a trailingflush 500ms after the last chunk. Continuous streaming keeps resetting the
timer, so writes still happen at the 5s leading cadence (bounding TOAST churn);
only pauses and burst tails trigger a catch-up write.
dbFlushTimerat the same teardown points asflushTimer(interactiontransition reset +
flushAndClearStreamingContext); the AfterFunc re-checks!dirtyand nil accumulator/interaction, matching the publish flushTimer'srace handling.
TestMessageAdded_TrailingDBFlush.Verification
localhost:8080): DB catch-up on a mid-turn pausedrops from up to 5s to ~500ms; continuous streaming keeps the 5s cadence
(no extra churn); the live view was current every turn and is unchanged.
go build ./pkg/server/passes;TestWebSocketSyncSuite(incl. the new test)green.
is fully removed (no net frontend change).
Notes
The frontend LIVE path is intentionally left untouched — it already renders the
in-progress message correctly, and its completion/flicker/cross-session logic is
delicate. This change hardens only the DB/fallback path.
🔗 Open in Helix
📋 Spec:
🚀 Built with Helix