fix(coding-agent): prevent duplicate session headers when forking from pre-assistant entry

createBranchedSession() wrote the file and set flushed=true even when the
branched path had no assistant message. The next _persist() call saw no
assistant, reset flushed=false, and the subsequent flush appended all
in-memory entries to the already-populated file, duplicating the header
and entries.

Fix: defer file creation when the branched path has no assistant message,
matching the newSession() contract. _persist() creates the file on the
first assistant response.

closes #1672
This commit is contained in:
Mario Zechner 2026-02-27 22:18:26 +01:00
parent 9825c13f5f
commit 2f64df1e52
3 changed files with 90 additions and 7 deletions

View file

@ -1187,11 +1187,7 @@ export class SessionManager {
}
if (this.persist) {
appendFileSync(newSessionFile, `${JSON.stringify(header)}\n`);
for (const entry of pathWithoutLabels) {
appendFileSync(newSessionFile, `${JSON.stringify(entry)}\n`);
}
// Write fresh label entries at the end
// Build label entries
const lastEntryId = pathWithoutLabels[pathWithoutLabels.length - 1]?.id || null;
let parentId = lastEntryId;
const labelEntries: LabelEntry[] = [];
@ -1204,16 +1200,29 @@ export class SessionManager {
targetId,
label,
};
appendFileSync(newSessionFile, `${JSON.stringify(labelEntry)}\n`);
pathEntryIds.add(labelEntry.id);
labelEntries.push(labelEntry);
parentId = labelEntry.id;
}
this.fileEntries = [header, ...pathWithoutLabels, ...labelEntries];
this.sessionId = newSessionId;
this.sessionFile = newSessionFile;
this.flushed = true;
this._buildIndex();
// Only write the file now if it contains an assistant message.
// Otherwise defer to _persist(), which creates the file on the
// first assistant response, matching the newSession() contract
// and avoiding the duplicate-header bug when _persist()'s
// no-assistant guard later resets flushed to false.
const hasAssistant = this.fileEntries.some((e) => e.type === "message" && e.message.role === "assistant");
if (hasAssistant) {
this._rewriteFile();
this.flushed = true;
} else {
this.flushed = false;
}
return newSessionFile;
}