* Re-append cached session metadata to the end of the transcript file. * This ensures metadata stays within the tail window that readLiteMetadata * reads during progressive loading. * * Called from two contexts with different file-ordering implications: * - During compaction (compact.
(skipTitleRefresh = false)
| 733 | * external-writer concern — their caches are authoritative. |
| 734 | */ |
| 735 | reAppendSessionMetadata(skipTitleRefresh = false): void { |
| 736 | if (!this.sessionFile) return |
| 737 | const sessionId = getSessionId() as UUID |
| 738 | if (!sessionId) return |
| 739 | |
| 740 | // One sync tail read to refresh SDK-mutable fields. Same |
| 741 | // LITE_READ_BUF_SIZE window readLiteMetadata uses. Empty string on |
| 742 | // failure → extract returns null → cache is the only source of truth. |
| 743 | const tail = readFileTailSync(this.sessionFile) |
| 744 | |
| 745 | // Absorb any fresher SDK-written title/tag into our cache. If the SDK |
| 746 | // wrote while we had the session open, our cache is stale — the tail |
| 747 | // value is authoritative. If the tail has nothing (evicted or never |
| 748 | // written externally), the cache stands. |
| 749 | // |
| 750 | // Filter with startsWith to match only top-level JSONL entries (col 0) |
| 751 | // and not "type":"tag" appearing inside a nested tool_use input that |
| 752 | // happens to be JSON-serialized into a message. |
| 753 | const tailLines = tail.split('\n') |
| 754 | if (!skipTitleRefresh) { |
| 755 | const titleLine = tailLines.findLast(l => |
| 756 | l.startsWith('{"type":"custom-title"'), |
| 757 | ) |
| 758 | if (titleLine) { |
| 759 | const tailTitle = extractLastJsonStringField(titleLine, 'customTitle') |
| 760 | // `!== undefined` distinguishes no-match from empty-string match. |
| 761 | // renameSession rejects empty titles, but the CLI is defensive: an |
| 762 | // external writer with customTitle:"" should clear the cache so the |
| 763 | // re-append below skips it (instead of resurrecting a stale title). |
| 764 | if (tailTitle !== undefined) { |
| 765 | this.currentSessionTitle = tailTitle || undefined |
| 766 | } |
| 767 | } |
| 768 | } |
| 769 | const tagLine = tailLines.findLast(l => l.startsWith('{"type":"tag"')) |
| 770 | if (tagLine) { |
| 771 | const tailTag = extractLastJsonStringField(tagLine, 'tag') |
| 772 | // Same: tagSession(id, null) writes `tag:""` to clear. |
| 773 | if (tailTag !== undefined) { |
| 774 | this.currentSessionTag = tailTag || undefined |
| 775 | } |
| 776 | } |
| 777 | |
| 778 | // lastPrompt is re-appended so readLiteMetadata can show what the |
| 779 | // user was most recently doing. Written first so customTitle/tag/etc |
| 780 | // land closer to EOF (they're the more critical fields for tail reads). |
| 781 | if (this.currentSessionLastPrompt) { |
| 782 | appendEntryToFile(this.sessionFile, { |
| 783 | type: 'last-prompt', |
| 784 | lastPrompt: this.currentSessionLastPrompt, |
| 785 | sessionId, |
| 786 | }) |
| 787 | } |
| 788 | // Unconditional: cache was refreshed from tail above; re-append keeps |
| 789 | // the entry at EOF so compaction-pushed content doesn't evict it. |
| 790 | if (this.currentSessionTitle) { |
| 791 | appendEntryToFile(this.sessionFile, { |
| 792 | type: 'custom-title', |
no test coverage detected