fix v0.4 spec: address 7 review issues (deferral storage, cache rebuild, key conflict, etc.)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -138,13 +138,17 @@ CREATE TABLE task (
|
||||
createdAt TEXT NOT NULL,
|
||||
updatedAt TEXT NOT NULL,
|
||||
filePath TEXT NOT NULL,
|
||||
linkedMessageId TEXT
|
||||
linkedMessageId TEXT,
|
||||
isSomeday INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX idx_task_status ON task(status);
|
||||
CREATE INDEX idx_task_deferUntil ON task(deferUntil);
|
||||
CREATE INDEX idx_task_dueDate ON task(dueDate);
|
||||
CREATE INDEX idx_task_linkedMessageId ON task(linkedMessageId);
|
||||
```
|
||||
|
||||
`updatedAt` is populated from VTODO `LAST-MODIFIED` during parsing and updated alongside it during writes.
|
||||
|
||||
### Migration v3_label: Labels and item-label junction
|
||||
|
||||
```sql
|
||||
@@ -165,21 +169,29 @@ CREATE TABLE itemLabel (
|
||||
);
|
||||
```
|
||||
|
||||
### Migration v3_deferral: Deferral tracking
|
||||
### Migration v3_deferral: Email deferral tracking
|
||||
|
||||
The `deferral` table is for **emails only**. Tasks use `task.deferUntil` and `task.isSomeday` (derived from the VTODO file, survives cache rebuilds).
|
||||
|
||||
```sql
|
||||
CREATE TABLE deferral (
|
||||
id TEXT PRIMARY KEY,
|
||||
itemType TEXT NOT NULL, -- "email" or "task"
|
||||
itemId TEXT NOT NULL,
|
||||
messageId TEXT NOT NULL REFERENCES message(id) ON DELETE CASCADE,
|
||||
deferUntil TEXT, -- ISO8601 date, NULL = someday
|
||||
originalMailbox TEXT, -- for emails: restore to this mailbox on resurface
|
||||
originalMailbox TEXT, -- restore to this mailbox on resurface
|
||||
createdAt TEXT NOT NULL,
|
||||
UNIQUE(itemType, itemId)
|
||||
UNIQUE(messageId)
|
||||
);
|
||||
CREATE INDEX idx_deferral_until ON deferral(deferUntil);
|
||||
```
|
||||
|
||||
Add `isSomeday` to the task table (in v3_task migration):
|
||||
|
||||
```sql
|
||||
-- Added to the task CREATE TABLE above:
|
||||
isSomeday INTEGER NOT NULL DEFAULT 0,
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TaskStore Module
|
||||
@@ -261,25 +273,35 @@ Writes standard iCalendar from a TaskRecord. Line folding at 75 octets per RFC 5
|
||||
**Deferring a task:**
|
||||
1. Update VTODO `DTSTART` property, rewrite `.ics` file
|
||||
2. Update `task.deferUntil` in SQLite cache
|
||||
3. Create `deferral` record for unified query
|
||||
4. Task disappears from Inbox, appears in Today when date arrives
|
||||
3. Task disappears from Inbox (query checks `task.deferUntil`), appears in Today when date arrives
|
||||
|
||||
Note: Tasks do NOT use the `deferral` table. The `task.deferUntil` column is authoritative — it's derived from the VTODO file (source of truth) and survives cache rebuilds. The `deferral` table exists only for emails, which have no native defer field.
|
||||
|
||||
**Deferring to Someday:**
|
||||
- Same as above but `deferUntil = NULL` in deferral record
|
||||
- For emails: create deferral record with `deferUntil = NULL`
|
||||
- For tasks: set a `someday` flag (use `DTSTART` with a sentinel value, or a dedicated `X-MAGNUM-SOMEDAY:TRUE` property in the VTODO, plus `task.isSomeday` in cache)
|
||||
- Item only appears in Someday perspective, never resurfaces automatically
|
||||
- User manually pulls it back by removing the deferral
|
||||
- User manually pulls it back by clearing the deferral/someday flag
|
||||
|
||||
### Resurfacing
|
||||
|
||||
On each sync cycle (and on app launch), the SyncEngine checks:
|
||||
On each sync cycle (and on app launch), the SyncEngine checks both sources:
|
||||
|
||||
```sql
|
||||
-- Emails
|
||||
SELECT * FROM deferral WHERE deferUntil IS NOT NULL AND deferUntil <= datetime('now')
|
||||
|
||||
-- Tasks
|
||||
SELECT * FROM task WHERE deferUntil IS NOT NULL AND deferUntil <= datetime('now') AND isSomeday = 0
|
||||
```
|
||||
|
||||
For each matched deferral:
|
||||
- Delete the deferral record → item reappears in Today perspective
|
||||
- For tasks: clear `deferUntil` in the task record and VTODO file
|
||||
For each matched email deferral:
|
||||
- Delete the deferral record → email reappears in Inbox
|
||||
|
||||
For each matched task:
|
||||
- Clear `deferUntil` in the task record and VTODO file → task reappears in Inbox
|
||||
|
||||
**Resurfaced items go to Inbox, not Today.** The Today perspective shows items with `dueDate = today` (a deadline, not a deferral). This is the GTD distinction: a deferred item was "hidden until ready to process" — when it resurfaces, it goes back to the inbox for triage, just like a new email would.
|
||||
|
||||
This is a simple poll, not a timer. Items may resurface slightly late (by up to the sync interval, default 5 minutes). Acceptable for v0.4.
|
||||
|
||||
@@ -291,11 +313,11 @@ This is a simple poll, not a timer. Items may resurface slightly late (by up to
|
||||
|
||||
| Perspective | Content | Query Logic |
|
||||
|-------------|---------|-------------|
|
||||
| Inbox | Unprocessed items | Emails in INBOX mailbox (no deferral) + tasks with status=NEEDS-ACTION (no deferral) |
|
||||
| Today | Due/deferred to today | Items with deferUntil ≤ today OR dueDate = today. Includes resurfaced deferrals. |
|
||||
| Upcoming | Future dated items | Items with deferUntil > today OR dueDate > today, ordered by date |
|
||||
| Inbox | Unprocessed items | Emails in INBOX (no deferral) + tasks with status=NEEDS-ACTION, deferUntil is NULL, isSomeday=0 |
|
||||
| Today | Due today | Emails/tasks with dueDate = today |
|
||||
| Upcoming | Future dated items | Items with dueDate > today, ordered by date |
|
||||
| Projects | Grouped by project | Items with a label where isProject=true, grouped by label name |
|
||||
| Someday | Parked indefinitely | Items with a deferral where deferUntil IS NULL |
|
||||
| Someday | Parked indefinitely | Emails with deferral where deferUntil IS NULL + tasks with isSomeday=1 |
|
||||
| Archive | Done/filed | Emails in Archive folder + tasks with status=COMPLETED or CANCELLED |
|
||||
|
||||
### Unified Query
|
||||
@@ -306,11 +328,11 @@ Each perspective runs a query that unions emails and tasks:
|
||||
-- Example: Inbox perspective
|
||||
SELECT 'email' as itemType, id, subject as title, date, ...
|
||||
FROM message WHERE mailboxId = :inboxId
|
||||
AND id NOT IN (SELECT itemId FROM deferral WHERE itemType = 'email')
|
||||
AND id NOT IN (SELECT messageId FROM deferral)
|
||||
UNION ALL
|
||||
SELECT 'task' as itemType, id, summary as title, createdAt as date, ...
|
||||
FROM task WHERE status = 'NEEDS-ACTION'
|
||||
AND id NOT IN (SELECT itemId FROM deferral WHERE itemType = 'task')
|
||||
AND deferUntil IS NULL AND isSomeday = 0
|
||||
ORDER BY date DESC
|
||||
```
|
||||
|
||||
@@ -340,7 +362,12 @@ The unified query returns `ItemSummary` values. GRDB ValueObservation on both ta
|
||||
| Read/unread | `⇧⌘U` | IMAP STORE ±\Seen |
|
||||
| Move to folder | `⇧⌘M` | IMAP MOVE |
|
||||
|
||||
Note: `⌫` does double duty — for emails in mail folders it deletes (trash), in GTD context it discards (archive). The behavior depends on the active perspective. In Inbox/Today/Upcoming perspectives, `⌫` means "discard" (archive). In mailbox folder views, `⌫` means "delete" (trash). This matches the GTD mental model: discarding isn't destroying, it's filing away.
|
||||
**v0.3 → v0.4 behavioral change for `⌫`:** In v0.3, `⌫` always meant "delete" (move to Trash). In v0.4, the behavior depends on the active perspective:
|
||||
|
||||
- **GTD perspectives** (Inbox, Today, Upcoming, Projects, Someday): `⌫` means "discard" — archive the email or cancel the task. This matches GTD: discarding isn't destroying, it's removing from the action queue.
|
||||
- **Mailbox folder views** (when browsing a specific IMAP folder): `⌫` retains v0.3 behavior — "delete" (move to Trash).
|
||||
|
||||
This is a deliberate change. The `e` key (archive) remains available in all views as an alternative.
|
||||
|
||||
### Defer Picker
|
||||
|
||||
@@ -394,6 +421,8 @@ SELECT * FROM task WHERE linkedMessageId IN (
|
||||
|
||||
Tasks render with a distinct visual style (checkbox icon, status badge) but are inline in the timeline, sorted by `createdAt`.
|
||||
|
||||
**Orphaned links:** If a linked email is deleted from the server during IMAP sync, the task's `linkedMessageId` becomes orphaned. The task remains visible as a standalone item — it simply no longer appears in any thread detail view. The `ATTACH:mid:` property stays in the VTODO file (harmless). No user notification needed.
|
||||
|
||||
---
|
||||
|
||||
## Label Management
|
||||
@@ -411,7 +440,10 @@ Pressing `p` opens a compact popover:
|
||||
|
||||
### Storage
|
||||
|
||||
Labels are local-only in v0.4. For tasks, the label names are also written to the VTODO `CATEGORIES` property so they survive a cache rebuild.
|
||||
Labels are local-only in v0.4.
|
||||
|
||||
- **Task labels** are written to the VTODO `CATEGORIES` property, so they survive a cache rebuild. During `rebuildCache()`, the TaskStore parses `CATEGORIES` from each VTODO file and populates the `itemLabel` junction table.
|
||||
- **Email labels** are stored only in the `itemLabel` junction table in SQLite. They do NOT survive a cache rebuild. This is a known v0.4 limitation — emails have no standard mechanism for custom labels outside of IMAP keywords (which not all servers support). Acceptable for v0.4; can be addressed later with IMAP keywords or a local manifest file.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user