* 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)
| 800 | * external-writer concern — their caches are authoritative. |
| 801 | */ |
| 802 | reAppendSessionMetadata(skipTitleRefresh = false): void { |
| 803 | if (!this.sessionFile) return |
| 804 | const sessionId = getSessionId() as UUID |
| 805 | if (!sessionId) return |
| 806 | |
| 807 | // One sync tail read to refresh SDK-mutable fields. Same |
| 808 | // LITE_READ_BUF_SIZE window readLiteMetadata uses. Empty string on |
| 809 | // failure → extract returns null → cache is the only source of truth. |
| 810 | const tail = readFileTailSync(this.sessionFile) |
| 811 | |
| 812 | // Absorb any fresher SDK-written title/tag into our cache. If the SDK |
| 813 | // wrote while we had the session open, our cache is stale — the tail |
| 814 | // value is authoritative. If the tail has nothing (evicted or never |
| 815 | // written externally), the cache stands. |
| 816 | // |
| 817 | // Filter with startsWith to match only top-level JSONL entries (col 0) |
| 818 | // and not "type":"tag" appearing inside a nested tool_use input that |
| 819 | // happens to be JSON-serialized into a message. |
| 820 | const tailLines = tail.split('\n') |
| 821 | if (!skipTitleRefresh) { |
| 822 | const titleLine = tailLines.findLast(l => |
| 823 | l.startsWith('{"type":"custom-title"'), |
| 824 | ) |
| 825 | if (titleLine) { |
| 826 | const tailTitle = extractLastJsonStringField(titleLine, 'customTitle') |
| 827 | // `!== undefined` distinguishes no-match from empty-string match. |
| 828 | // renameSession rejects empty titles, but the CLI is defensive: an |
| 829 | // external writer with customTitle:"" should clear the cache so the |
| 830 | // re-append below skips it (instead of resurrecting a stale title). |
| 831 | if (tailTitle !== undefined) { |
| 832 | this.currentSessionTitle = tailTitle || undefined |
| 833 | } |
| 834 | } |
| 835 | } |
| 836 | const tagLine = tailLines.findLast(l => l.startsWith('{"type":"tag"')) |
| 837 | if (tagLine) { |
| 838 | const tailTag = extractLastJsonStringField(tagLine, 'tag') |
| 839 | // Same: tagSession(id, null) writes `tag:""` to clear. |
| 840 | if (tailTag !== undefined) { |
| 841 | this.currentSessionTag = tailTag || undefined |
| 842 | } |
| 843 | } |
| 844 | |
| 845 | // lastPrompt is re-appended so readLiteMetadata can show what the |
| 846 | // user was most recently doing. Written first so customTitle/tag/etc |
| 847 | // land closer to EOF (they're the more critical fields for tail reads). |
| 848 | if (this.currentSessionLastPrompt) { |
| 849 | appendEntryToFile(this.sessionFile, { |
| 850 | type: 'last-prompt', |
| 851 | lastPrompt: this.currentSessionLastPrompt, |
| 852 | sessionId, |
| 853 | }) |
| 854 | } |
| 855 | // Unconditional: cache was refreshed from tail above; re-append keeps |
| 856 | // the entry at EOF so compaction-pushed content doesn't evict it. |
| 857 | if (this.currentSessionTitle) { |
| 858 | appendEntryToFile(this.sessionFile, { |
| 859 | type: 'custom-title', |
no test coverage detected