Background
#3656 fixes the read path for session JSONL files where two records were glued together as `}{` (an interrupted append lost its trailing `\n`). Two follow-ups were intentionally left out of that PR's scope.
Item 1: `countSessionMessages` consistency
`packages/core/src/services/sessionService.ts:314-341` is a separate reader path that does not go through `jsonl-utils`. It silently skips a malformed physical line in its catch block, so a glued line (which #3656 now recovers in `read()` / `readLines()`) is still dropped here:
```ts
for await (const line of rl) {
try {
const record = JSON.parse(trimmed) as ChatRecord;
...
} catch {
continue; // entire glued line dropped, including the N records inside
}
}
```
Effect: the list-view message count for a session that has any corruption is lower than the record count seen on resume.
Suggested fix: route this loop through `parseLineTolerant` (export it), or simply call `jsonl.read()` and dedupe by `uuid`.
Item 2: write-side durability
The corruption shape #3606 / #3656 address is fundamentally a write-side problem. `writeLine` / `writeLineSync` use `appendFile(Sync)` without `fsync` and without atomic-rename, so a process killed mid-append can still leave the file with a glued line.
#3656 deliberately stays on the read side (avoids accumulating empty separator lines on every resume). If corruption reports persist, options to consider:
- `fdatasync` after each append (cost: hot-path latency; needs benchmarking against auto-title + tool-call write rate)
- write-then-rename with a per-line tmp file (safer but heavier)
- accept current trade-off and rely on read-side recovery
Open for discussion — no immediate action required unless reports of corruption persist.
Background
#3656 fixes the read path for session JSONL files where two records were glued together as `}{` (an interrupted append lost its trailing `\n`). Two follow-ups were intentionally left out of that PR's scope.
Item 1: `countSessionMessages` consistency
`packages/core/src/services/sessionService.ts:314-341` is a separate reader path that does not go through `jsonl-utils`. It silently skips a malformed physical line in its catch block, so a glued line (which #3656 now recovers in `read()` / `readLines()`) is still dropped here:
```ts
for await (const line of rl) {
try {
const record = JSON.parse(trimmed) as ChatRecord;
...
} catch {
continue; // entire glued line dropped, including the N records inside
}
}
```
Effect: the list-view message count for a session that has any corruption is lower than the record count seen on resume.
Suggested fix: route this loop through `parseLineTolerant` (export it), or simply call `jsonl.read()` and dedupe by `uuid`.
Item 2: write-side durability
The corruption shape #3606 / #3656 address is fundamentally a write-side problem. `writeLine` / `writeLineSync` use `appendFile(Sync)` without `fsync` and without atomic-rename, so a process killed mid-append can still leave the file with a glued line.
#3656 deliberately stays on the read side (avoids accumulating empty separator lines on every resume). If corruption reports persist, options to consider:
Open for discussion — no immediate action required unless reports of corruption persist.