fix(session): preserve explicit sessions with consumed drafts

This commit is contained in:
can1357
2026-07-05 12:16:33 +02:00
parent ecded0afe5
commit d4283d242c
2 changed files with 62 additions and 4 deletions
@@ -73,6 +73,7 @@ import {
import { type SessionTitleUpdate, serializeTitleSlot } from "./session-title-slot";
const JSONL_SUFFIX_LENGTH = ".jsonl".length;
const DRAFT_ONLY_SESSION_MARKER = ".draft-only-session";
const SUPERSEDED_COMPACTION_SUMMARY = "[Superseded compaction summary elided after a newer compaction]";
const SUPERSEDED_COMPACTION_SHORT_SUMMARY = "Superseded compaction elided";
@@ -827,6 +828,32 @@ export class SessionManager {
return artifactsDir ? path.join(artifactsDir, "draft.txt") : null;
}
#draftOnlySessionMarkerPath(): string | null {
const artifactsDir = this.getArtifactsDir();
return artifactsDir ? path.join(artifactsDir, DRAFT_ONLY_SESSION_MARKER) : null;
}
#hasDraftOnlySessionMarker(): boolean {
const markerPath = this.#draftOnlySessionMarkerPath();
return markerPath !== null && this.#storage.existsSync(markerPath);
}
async #writeDraftOnlySessionMarker(): Promise<void> {
const markerPath = this.#draftOnlySessionMarkerPath();
if (!markerPath) return;
await this.#storage.writeText(markerPath, "");
}
async #clearDraftOnlySessionMarker(): Promise<void> {
const markerPath = this.#draftOnlySessionMarkerPath();
if (!markerPath) return;
try {
await this.#storage.unlink(markerPath);
} catch (err) {
if (!isEnoent(err)) throw err;
}
}
#artifactManagerForSession(): ArtifactManager | null {
if (this.#adoptedArtifactManager) return this.#adoptedArtifactManager;
@@ -1173,10 +1200,17 @@ export class SessionManager {
async #dropIfEmptyAndNoDraft(): Promise<void> {
if (!this.#draftOnlySessionCleanupArmed) return;
const sessionFile = this.#sessionFile;
if (!sessionFile || !this.#storage.existsSync(sessionFile)) return;
if (!this.#entries.every(isDraftOnlyMetadataEntry)) return;
if (!sessionFile || !this.#storage.existsSync(sessionFile)) {
this.#draftOnlySessionCleanupArmed = false;
return;
}
const draftPath = this.#draftPath();
if (draftPath && this.#storage.existsSync(draftPath)) return;
if (!this.#entries.every(isDraftOnlyMetadataEntry)) {
await this.#clearDraftOnlySessionMarker();
this.#draftOnlySessionCleanupArmed = false;
return;
}
try {
await this.#storage.deleteSessionWithArtifacts(sessionFile);
this.#fileIsCurrent = false;
@@ -1299,7 +1333,10 @@ export class SessionManager {
this.#entries.every(isDraftOnlyMetadataEntry);
// Force the header onto disk so resume can find the file this draft attaches to.
await this.ensureOnDisk();
if (draftWillMaterializeMetadataOnlyFile) this.#draftOnlySessionCleanupArmed = true;
if (draftWillMaterializeMetadataOnlyFile) {
await this.#writeDraftOnlySessionMarker();
this.#draftOnlySessionCleanupArmed = true;
}
await this.#storage.writeText(draftPath, text);
}
@@ -1320,7 +1357,8 @@ export class SessionManager {
} catch (err) {
if (!isEnoent(err)) throw err;
}
if (this.#entries.every(isDraftOnlyMetadataEntry)) this.#draftOnlySessionCleanupArmed = true;
if (this.#entries.every(isDraftOnlyMetadataEntry) && this.#hasDraftOnlySessionMarker())
this.#draftOnlySessionCleanupArmed = true;
return draft;
}
@@ -126,6 +126,26 @@ describe("SessionManager close() drops empty metadata-only sessions", () => {
expect(await fileExists(sessionFile)).toBe(true);
});
it("keeps an explicitly ensured empty session after its draft is consumed on resume", async () => {
using tempDir = TempDir.createSync("@pi-session-close-keep-explicit-resumed-draft-");
const firstRun = SessionManager.create(tempDir.path(), tempDir.path());
await firstRun.ensureOnDisk();
await firstRun.saveDraft("resume me");
const sessionFile = firstRun.getSessionFile();
if (!sessionFile) throw new Error("Expected persistent session file");
await firstRun.close();
expect(await fileExists(sessionFile)).toBe(true);
const resumed = SessionManager.create(tempDir.path(), tempDir.path());
await resumed.setSessionFile(sessionFile);
expect(await resumed.consumeDraft()).toBe("resume me");
await resumed.saveDraft("");
await resumed.close();
expect(await fileExists(sessionFile)).toBe(true);
});
it("keeps a handoff custom message even before the next user turn", async () => {
using tempDir = TempDir.createSync("@pi-session-close-keep-handoff-");
const session = SessionManager.create(tempDir.path(), tempDir.path());