You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
cleanupComposeTab(tabId) is documented as "Drop all state associated with a compose tab (called on send / close)" (src/background/state.ts:135-138), but there is no close path:
src/background/sent-copy.ts:120 calls it on onAfterSend — the send half.
src/background/background.ts:12 imports cleanupComposeTab but never calls it — the import is dead, and no listener fires when a compose window is closed without sending (grep -rn "cleanupComposeTab\|tabs.onRemoved" src).
So every compose window the user opens and closes without sending — or whose send was cancelled by a guard (evaluateBeforeSendGuards BCC/policy-editor branches) or by a failed encryption — leaves its entry in the composeTabs map for the lifetime of the background script, including any policy / signId the user configured.
Why it matters
State leak: the map grows for the whole background session. Small per entry, but unbounded in a long-running Thunderbird.
Stale persisted state: persistEncryptState() writes every entry with encrypt: true to storage.local (state.ts:60-68). Entries for compose windows that no longer exist keep being rewritten. They are filtered on restore (restoreEncryptState checks the tab still exists) but only at background start, so the persisted blob accumulates dead tab ids in between.
Stale state can be inherited: windows.onCreated only sets a default when the tab id is absent (background.ts:142, deliberately — see CLAUDE.md). A leftover entry for a reused tab id means a fresh compose window silently starts with the previous window's encrypt flag and recipient policy instead of the shouldEncrypt default.
Suggested fix (small)
Register a compose-tab close listener in background.ts (next to the existing messages.onDeleted listener) that calls cleanupComposeTab(tabId), clears any in-flight upload record for that tab, and re-persists (persistEncryptState() / persistInFlightUploads()), mirroring what sent-copy.ts:118-123 does after a send.
browser.tabs.onRemoved is not in src/types/thunderbird.d.ts yet (only tabs.query is declared, line 22-24) — add the minimal declaration alongside the existing windows.onRemoved block at line 38.
The dead cleanupComposeTab import in background.ts disappears on its own once the listener uses it.
Cover it in tests/state.test.ts in the same style as the existing cleanupComposeTab test (line 62): entry present → close → entry gone and the persisted blob no longer lists the tab.
Separate second commit: CLAUDE.md is out of date
The "Tests not gated in CI" section of CLAUDE.md says .github/workflows/build.yml runs typecheck and build "but NOT npm test (tracked, unfixed)". That stopped being true in #151 — the workflow now has a Run tests step running npm test. The rest of that paragraph (tests are outside tsconfig.json's include, so test type errors are caught by neither typecheck nor vitest) is still accurate and should stay. Please fix the first sentence in its own commit.
What happens
cleanupComposeTab(tabId)is documented as "Drop all state associated with a compose tab (called on send / close)" (src/background/state.ts:135-138), but there is no close path:src/background/sent-copy.ts:120calls it ononAfterSend— the send half.src/background/background.ts:12importscleanupComposeTabbut never calls it — the import is dead, and no listener fires when a compose window is closed without sending (grep -rn "cleanupComposeTab\|tabs.onRemoved" src).So every compose window the user opens and closes without sending — or whose send was cancelled by a guard (
evaluateBeforeSendGuardsBCC/policy-editor branches) or by a failed encryption — leaves its entry in thecomposeTabsmap for the lifetime of the background script, including anypolicy/signIdthe user configured.Why it matters
persistEncryptState()writes every entry withencrypt: truetostorage.local(state.ts:60-68). Entries for compose windows that no longer exist keep being rewritten. They are filtered on restore (restoreEncryptStatechecks the tab still exists) but only at background start, so the persisted blob accumulates dead tab ids in between.windows.onCreatedonly sets a default when the tab id is absent (background.ts:142, deliberately — see CLAUDE.md). A leftover entry for a reused tab id means a fresh compose window silently starts with the previous window's encrypt flag and recipient policy instead of theshouldEncryptdefault.Suggested fix (small)
background.ts(next to the existingmessages.onDeletedlistener) that callscleanupComposeTab(tabId), clears any in-flight upload record for that tab, and re-persists (persistEncryptState()/persistInFlightUploads()), mirroring whatsent-copy.ts:118-123does after a send.browser.tabs.onRemovedis not insrc/types/thunderbird.d.tsyet (onlytabs.queryis declared, line 22-24) — add the minimal declaration alongside the existingwindows.onRemovedblock at line 38.cleanupComposeTabimport inbackground.tsdisappears on its own once the listener uses it.tests/state.test.tsin the same style as the existingcleanupComposeTabtest (line 62): entry present → close → entry gone and the persisted blob no longer lists the tab.Separate second commit: CLAUDE.md is out of date
The "Tests not gated in CI" section of
CLAUDE.mdsays.github/workflows/build.ymlruns typecheck and build "but NOTnpm test(tracked, unfixed)". That stopped being true in #151 — the workflow now has aRun testsstep runningnpm test. The rest of that paragraph (tests are outsidetsconfig.json'sinclude, so test type errors are caught by neithertypechecknor vitest) is still accurate and should stay. Please fix the first sentence in its own commit.