tasks: key derived captures by external id and record who resolved

A task extracted from mail deduped on the live-norm index only, so once he
finished it the row left the live set and the next poll of the same immutable
message re-extracted it as a fresh candidate. mavmaild is a read-only reader
and marks nothing read, so that repeats forever. Derived rows now carry an
ext_id built from the message uid and the extracted span, unique across every
status, while voice keeps live-only norm dedupe because saying an errand again
is the recurrence signal. A derived source can no longer capture straight to
open, and saying a task out loud that Maven had only proposed promotes the
candidate instead of answering that it is already in the list.

SetTaskStatus was classified AuthRead. Resolving a task is not additive, it
erases work off his list, so it is a write, and the row now records the caller
that moved it. ListTasks was unbounded. The list-query matcher claimed any
utterance with "что мне делать", including "с чем мне помочь", and the urgency
stripper matched inside words.

Found in review of #60.
This commit is contained in:
kami
2026-08-01 14:16:39 +04:00
parent 7f42cc73be
commit 708a69375f
14 changed files with 571 additions and 132 deletions
+17
View File
@@ -160,6 +160,23 @@ ALTER TABLE reminders ADD COLUMN next_fire_ts INTEGER;`, // #2
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_live_norm ON tasks (norm) WHERE status IN ('candidate','open');
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks (status, created_ts DESC);`,
// #15 — external identity and resolution attribution for tasks.
//
// ext_id is the identity of the thing a derived task was extracted FROM
// (message id plus the extracted span), and its unique index covers EVERY
// row, not just the live ones. The live-only norm index is right for
// voice, where him saying the errand again is the recurrence signal. It is
// wrong for a mailbox: mavmaild is a read-only reader, nothing marks a
// message read, so a task he already finished would be re-extracted from
// the same immutable text on the next poll and land back on his list as a
// fresh candidate, forever.
//
// resolved_by records which caller moved the task. resolved_ts said when
// and never by what, so a wrong resolution left no trace at all.
`ALTER TABLE tasks ADD COLUMN ext_id TEXT;
ALTER TABLE tasks ADD COLUMN resolved_by TEXT NOT NULL DEFAULT '';
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_ext_id ON tasks (ext_id) WHERE ext_id IS NOT NULL;`,
}
// migrate applies every migration with a number greater than the DB's current