diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-25 17:33:50 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-25 17:33:50 +0200 |
| commit | 39d0371f284a5fb20eb132a60c795d6814b41b0a (patch) | |
| tree | 2d79eeb20b628d3a7c0f7731689280ac3ee9f4ac /docs/superpowers | |
| parent | 1cc12b86dfb036ea4ee5100ca6653d3c3b054195 (diff) | |
| download | qtmaildir-39d0371f284a5fb20eb132a60c795d6814b41b0a.tar.gz qtmaildir-39d0371f284a5fb20eb132a60c795d6814b41b0a.zip | |
docs(backlog): close item 166
The fix landed in 6ea6980 and the row was left open. Its section moves to
the closed file with what the fix turned out to need: the loop, the two
query forms that were measured and rejected, the split-index fixture,
and the live read-only verification.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HFuRPtzFrSxCQjFk6tq7gD
Diffstat (limited to 'docs/superpowers')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | 90 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 67 |
2 files changed, 91 insertions, 66 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md index b9eb8cf..a0337f1 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md @@ -8112,3 +8112,93 @@ separate Release configure with the option OFF reported a clean `0.27.0`. The suite is 37 of 38, the one failure being item 136 on an unrelated path. **Size: XS**, as sized. + +## 166. Mail you send to your own other account loses `inbox` + +**Observed (agent, 2026-08-25, while setting up msmtp.)** Four test messages +were sent to one of the user's own accounts, one from each configured sending +account. All four were delivered and indexed. The two sent from accounts whose +Sent folder is fetched locally arrived in the recipient account's Inbox +**without the `inbox` tag**, so they were absent from that account's Inbox view. +The two sent from an account whose Sent folder is not fetched kept `inbox` +normally. + +**Cause: established, and it is the `post-new` hook, not this binary.** +`strip_inbox_from_sent()` in `assets/hooks/post-new` removes `inbox` from any +message matching a configured sent folder's PATH. Its docstring states the +assumption exactly: "the provenance is the file's own path: a message inside a +configured sent folder is one this system sent, and `inbox` was never true of +it." + +That holds for one file. It fails for one MESSAGE, because **notmuch +deduplicates by Message-ID and a message can have several files**. When the +sender and the recipient are both the user's own accounts, mbsync fetches two +copies: the sender's Sent copy and the recipient's Inbox copy. notmuch stores +them as ONE message with two filenames. The carve-out's query matches via the +Sent filename and strips `inbox` from the message object, which is the same +object the recipient's Inbox copy belongs to. + +Measured: one message, two paths, one in the sender account's sent folder and +one in the recipient account's `Inbox/cur`. + +The assumption is not merely incomplete, it is false in this case: the message +was genuinely sent AND genuinely received. There is no single right answer for +"was `inbox` ever true of this message", because it was true of one file and +false of another. + +**Approach.** Not settled, and the choice matters more than the code: + +1. **Strip only when EVERY file is in a sent folder.** Closest to the existing + intent, and it makes the predicate match the docstring's claim. A + self-addressed message keeps `inbox`, which is right: it did arrive. +2. **Strip only when the message has exactly one file.** Simpler to express, + but it silently stops protecting any sent message that happens to be + duplicated for an unrelated reason. +3. **Leave it.** Self-addressed mail is rare outside testing. The cost is that + it is invisible when it happens, and it looks exactly like the sync defect + item 104 turned out to be, which is how this was found. + +Option 1 is the one that makes the code true to what it already says it does. + +**Constraints.** + +- **The hook is this repo's**, `assets/hooks/post-new`, which the live + `database.hook_dir` symlinks to. It has its own suites beside it; run + `./test_post_new.py` and `./test_mailrules.py` from `assets/hooks/`. +- The hook **tags real mail unattended, every ten minutes, on the user's live + index.** A predicate that is wrong in the other direction would strip `inbox` + from arriving mail, which is the failure mode PROTECTED_REMOVALS exists to + prevent. Test against a throwaway database first. +- `notmuch tag` matching zero messages SUCCEEDS, so a log line saying the + carve-out ran is not evidence it matched anything. The count added for item + 164 is what distinguishes them; use it. +- Do not fix this by narrowing the query to exclude the recipient account. The + bug is in the per-file predicate, not in which folders are configured. + +**Fixed 2026-08-25, option 1**, the one the entry named: strip only when +every file is in a sent folder. + +`sent_only()` in `assets/hooks/post-new` filters the matches and the tag is +then applied per id. **It is a loop because no query can express it**, and both +plausible query forms were measured against a real two-file message before the +loop was written: `not path:"Inbox/**"` does NOT exclude the message, and +`notmuch count --output=files` on a path query reports every file of every +matching message rather than the files that matched. Both read as if they +worked and are wrong for one reason, that a notmuch term is a predicate over a +MESSAGE while the distinction here is between its FILES. + +The root comes from `database.mail_root`, not `database.path`, since this index +is split and no message file sits under the index directory. The mutation +putting `database.path` back passes every pre-existing test, because the +ordinary fixture keeps the index inside the mail root and both keys return the +same string; `setup_accounts(split_index=True)` is what catches it, and is the +Python counterpart to `NotmuchFixture::splitIndex()`. + +Two mutations fail: `all` to `any` loses `inbox` on the self-addressed message, +`mail_root` to `path` silently stops stripping anything. + +**Verified read-only against the live index**, tagging nothing: of 807 messages +matching a sent path, 780 are still stripped and 27 are spared, every one of +them two files with one in another account's Inbox. No arrival is affected. + +**Size: S.** Done. diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md index 3cdc71a..1f14e67 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md @@ -239,7 +239,7 @@ taking that too literally. | 163 | The message pane shows a stale path, and the composer forks the draft | defect | S | **done, 2026-08-25.** mbsync renames an uploaded file to add its `,U=<uid>` infix while the model still holds the name the query returned. `MaildirName::resolveRenamed()` returns the path unchanged when it exists, else finds the file in that one directory whose unique stem matches; it refuses an ambiguous match and yields nothing for a genuinely missing file. Wired into all THREE read sites: the pane, Reply/Forward, and the draft reopen. The reopen was the one that cost data, forking a draft into two files with two Message-IDs, both reaching the server | | 164 | A draft this application saved keeps `inbox` | defect | S | open, 2026-08-25, **cause corrected 2026-08-25**. The first diagnosis blamed a missing drafts helper and was WRONG: `NOT_ARRIVALS` in `qtmaildirconf.py` is `("sent", "drafts")`, the folder list includes every account's drafts folder, and `notmuch count` confirms the carve-out query MATCHES the affected draft. The carve-out is scoped to `tag:new`, and the draft carries `inbox` while `tag:new` is 0, so it was never in scope when the hook ran. Measured separately: an mbsync-style rename does NOT re-add `new.tags`, so the retag theory is out too. What remains unestablished is WHICH pass tagged it; establish that before writing code | | 165 | A draft gets a new Message-ID on every autosave | enhancement | ? | open, 2026-08-25, found while hand-testing 163 and 164. `MessageBuilder::build()` generates an id unconditionally and every autosave calls it, so each revision is a distinct MESSAGE to notmuch and to the server rather than a new version of one. Invisible while the file is replaced correctly, which item 163's fix restores; it is what turned that fork into two messages rather than one duplicated file. Needs a DECISION on what a draft's identity is before any code: a stable id reused at send, a stable id discarded at send, or the status quo. Neither `ComposeContext` nor `OutgoingMessage` has a field to carry an id, so it is not a changed call site | -| 166 | Mail you send to your own other account loses `inbox` | defect | S | open, found 2026-08-25. Wholly this repo's: the hooks live in `assets/hooks/` with their own suites, and the live `post-new` symlinks to them | +| 166 | Mail you send to your own other account loses `inbox` | defect | S | **done 2026-08-25**, unreleased. `sent_only()` keeps a message only when EVERY file is inside a sent folder, which is what the carve-out's docstring already claimed. No query can express it, measured; the root comes from `database.mail_root`, with a split-index fixture the ordinary layout cannot provide. Verified read-only against the live index: 780 of 807 still stripped, 27 spared, no arrival affected | | 167 | No way to tell one build of an unreleased version from another | enhancement | XS | **done 2026-08-25**, unreleased. The user chose a counter over a git description: `QTMAILDIR_BUILD_NUMBER`, a cmake option ON by default, increments a counter in the BUILD directory on every build and writes `buildnumber.h`. `QTMAILDIR_VERSION_DISPLAY` carries it; `QTMAILDIR_VERSION` stays clean and is what the window title, `applicationVersion` and the release procedure use | Sizes are rough: XS under an hour, S a sitting, M a session. @@ -537,71 +537,6 @@ reaches it (item 42), so most of this exists. **Size: S** for the on-demand button, XS for the visibility half. Ask which. -## 166. Mail you send to your own other account loses `inbox` - -**Observed (agent, 2026-08-25, while setting up msmtp.)** Four test messages -were sent to one of the user's own accounts, one from each configured sending -account. All four were delivered and indexed. The two sent from accounts whose -Sent folder is fetched locally arrived in the recipient account's Inbox -**without the `inbox` tag**, so they were absent from that account's Inbox view. -The two sent from an account whose Sent folder is not fetched kept `inbox` -normally. - -**Cause: established, and it is the `post-new` hook, not this binary.** -`strip_inbox_from_sent()` in `assets/hooks/post-new` removes `inbox` from any -message matching a configured sent folder's PATH. Its docstring states the -assumption exactly: "the provenance is the file's own path: a message inside a -configured sent folder is one this system sent, and `inbox` was never true of -it." - -That holds for one file. It fails for one MESSAGE, because **notmuch -deduplicates by Message-ID and a message can have several files**. When the -sender and the recipient are both the user's own accounts, mbsync fetches two -copies: the sender's Sent copy and the recipient's Inbox copy. notmuch stores -them as ONE message with two filenames. The carve-out's query matches via the -Sent filename and strips `inbox` from the message object, which is the same -object the recipient's Inbox copy belongs to. - -Measured: one message, two paths, one in the sender account's sent folder and -one in the recipient account's `Inbox/cur`. - -The assumption is not merely incomplete, it is false in this case: the message -was genuinely sent AND genuinely received. There is no single right answer for -"was `inbox` ever true of this message", because it was true of one file and -false of another. - -**Approach.** Not settled, and the choice matters more than the code: - -1. **Strip only when EVERY file is in a sent folder.** Closest to the existing - intent, and it makes the predicate match the docstring's claim. A - self-addressed message keeps `inbox`, which is right: it did arrive. -2. **Strip only when the message has exactly one file.** Simpler to express, - but it silently stops protecting any sent message that happens to be - duplicated for an unrelated reason. -3. **Leave it.** Self-addressed mail is rare outside testing. The cost is that - it is invisible when it happens, and it looks exactly like the sync defect - item 104 turned out to be, which is how this was found. - -Option 1 is the one that makes the code true to what it already says it does. - -**Constraints.** - -- **The hook is this repo's**, `assets/hooks/post-new`, which the live - `database.hook_dir` symlinks to. It has its own suites beside it; run - `./test_post_new.py` and `./test_mailrules.py` from `assets/hooks/`. -- The hook **tags real mail unattended, every ten minutes, on the user's live - index.** A predicate that is wrong in the other direction would strip `inbox` - from arriving mail, which is the failure mode PROTECTED_REMOVALS exists to - prevent. Test against a throwaway database first. -- `notmuch tag` matching zero messages SUCCEEDS, so a log line saying the - carve-out ran is not evidence it matched anything. The count added for item - 164 is what distinguishes them; use it. -- Do not fix this by narrowing the query to exclude the recipient account. The - bug is in the per-file predicate, not in which folders are configured. - -**Size: S.** - - ## 112. Toggle unread on a whole thread cannot reach "all unread" on a partly-read thread **Observed (user, 2026-08-17):** clicking a thread root and asking to mark the |
