Repository navigation
fix: keep AI transcript capture honest across write failures and pauses - #441
Merged
Merged
Conversation
The per-message tailer (OpenCode's layout) appended each file's name to its sidecar index before processing it. A write that failed, or a file read before its writer finished and so failing to parse, was then marked handled and never read again: a message missing from the record while the source looked healthy. A turn whose write throws is now counted on the session, processUnit reports whether the unit fully landed, and the name is indexed only when it did. A failed unit gets a bounded retry, since nothing may touch the directory again. A paused file is still indexed unread, so resume does not record the pause. Handled names also join the in-memory set now. Before, every pass re-read and re-parsed every message file the session had, with only the turn uuid dedup keeping the events single. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
During a pause the JSONL tailer wrote no events but went on appending the transcript's raw bytes to its sidecar, so a private conversation was still kept in the project. Simply not appending would move the next read back to the pause, since the sidecar's size is the read position. Paused bytes are now replaced by newlines of the same length: nothing of the content is kept, the size still marks how far the source was read, and a restart's line alignment ends on a boundary. A line that began during the pause and finishes after it is dropped and filled the same way. The capture_gap description no longer says the raw data was kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The JSONL tailer reads from where its sidecar ends and appends what it read after processing. emitTurn reported a failed write only to capture health, so the sidecar advanced past the turn and it was never read again, with the source still looking healthy. When any turn in a pass fails, the read state is put back and the same bytes are read again on a bounded retry; turns that did land are skipped by uuid then, and the counters a re-read would double are restored. After the last retry the bytes are consumed and a capture_gap with reason write_failed records how many turns are missing, so one turn the store keeps refusing cannot hold back the rest of the session. The pause gap is now recorded after the pass succeeds, so a rewind cannot record it twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An independent review reproduced six defects in the three fixes before this one, and one claim that did not hold: - A child turn queued for its parent was lost when its flush failed: nothing was left to re-read. Turns whose write throws now wait in a per-session queue, retried first on every pass that is not paused. - A compaction after a paused partial line kept pauseTail set and dropped the first line of the new content. The reset clears it. - File events spent the retry budget in milliseconds. Only the retry timer's own passes count now, a pending retry keeps its schedule, a timer that fires while paused waits without spending an attempt, and re-arming clears the timer it replaces. - A turn that failed before a pause was swallowed by the pause. The queue keeps it until recording resumes. - A restart while a paused line was unfinished kept the rest of that line in the sidecar. pauseTail is now a marker beside the sidecar, ignored and removed if retention pruned the sidecar, and left out of evidence bundles. - A failed logged-tier insert (thinking turns) returned null, which read as deliberately not recorded. It throws now, as a chained insert does. - OpenCode returned an empty result for a message file still being written, so the host marked it handled. parseUnit throws instead. Retry decisions now live in catchUpSession for both paths, and when the retries are spent both give up the same way: consume, and write a capture_gap with reason write_failed naming the failed turns and unreadable units. A re-read no longer queues the same child twice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
This was referenced Oct 24, 2026
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.
問題
AI transcript tailer(
tailer-host.ts)有三條路徑,會讓已經觀測到的內容沒進到紀錄,或把暫停期間的內容留了下來:emitTurn()只把錯誤回報給 capture health,sidecar 照樣前進,失敗的 turn 不會再被讀。三種情況下,來源看起來都很正常。
變更
capture_gap(reason: write_failed),記下失敗的 turn 數和無法解析的檔案數。.pause-tail標記檔,重啟後仍有效。sidecar 被 retention 清掉時不採信這個標記檔;證據包也不打包它。審查
這個 PR 經過一次獨立的對抗式審查。前三個 commit 被找出 6 個缺陷,還有一個不成立的宣稱(OpenCode 寫到一半的檔案)。第 4 個 commit
116c1813修正了全部問題,reviewer 的重現測試已改寫成正式的回歸測試。驗證
opencode-adapter.test.ts加了 1 項。最初的新測試在修正前的程式上大多會失敗。catchUpSession()找不到 session 時本來就會直接返回。npm test:16 個 shell/PTY 測試檔在本機失敗,失敗清單和修改前的基準完全相同,而且這些測試都沒有用到tailer-host。以 CI 結果為準。已知限制(修改前就存在)
合併注意
#437 也改過
catchUpJsonl和catchUpPerMessageDir,把main合進 #437 時預期會有衝突。🤖 Generated with Claude Code