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:
+20
-6
@@ -82,9 +82,11 @@ func newMailIntake(st *store.Store, phr phraser.Phraser, cfg *config.Config, bus
|
||||
// reader's header filter is what keeps the resident model off newsletters.
|
||||
//
|
||||
// Every candidate is captured with Status "candidate", Source "email:<mailbox>"
|
||||
// and the subject as Evidence. CaptureTask dedupes on normalised text among
|
||||
// live rows, so a mailbox re-read after a restart produces Created=0 rather
|
||||
// than a second copy of every task.
|
||||
// and the subject as Evidence, under an ExternalID naming the message and the
|
||||
// span it was extracted from. That key is unique over every row whatever its
|
||||
// status, so a mailbox re-read after a restart produces Created=0 — and, more
|
||||
// to the point, a task he already marked done is not re-proposed the next time
|
||||
// the same unread message is read again.
|
||||
func (m *mailIntake) ingest(ctx context.Context, req ipc.IngestMailReq) (ipc.IngestMailResp, error) {
|
||||
msg := email.Message{
|
||||
UID: req.UID,
|
||||
@@ -124,15 +126,16 @@ func (m *mailIntake) ingest(ctx context.Context, req ipc.IngestMailReq) (ipc.Ing
|
||||
// something she read is a suggestion until he confirms it on /tasks.
|
||||
Status: store.TaskCandidate,
|
||||
}
|
||||
t.ExternalID = mailExternalID(source, req.UID, c.Text)
|
||||
if due, ok := email.ParseDue(c.Due); ok {
|
||||
t.Due = &due
|
||||
}
|
||||
id, created, err := m.st.CaptureTask(ctx, t)
|
||||
res, err := m.st.CaptureTask(ctx, t)
|
||||
if err != nil {
|
||||
return resp, fmt.Errorf("mail intake: capture: %w", err)
|
||||
}
|
||||
resp.TaskIDs = append(resp.TaskIDs, id)
|
||||
if created {
|
||||
resp.TaskIDs = append(resp.TaskIDs, res.ID)
|
||||
if res.Created {
|
||||
resp.Created++
|
||||
// Only a row that was actually created. CaptureTask dedupes on
|
||||
// normalised text among live rows, so a mailbox re-read after a
|
||||
@@ -165,3 +168,14 @@ func truncateRunes(s string, n int) string {
|
||||
}
|
||||
return string(r[:n]) + "…"
|
||||
}
|
||||
|
||||
// mailExternalID names the message and the span a candidate was extracted
|
||||
// from. The mailbox and UID identify the message; the normalised text
|
||||
// identifies which of the candidates in it this is, so a message yielding two
|
||||
// tasks gets two keys and a re-read of it gets neither twice.
|
||||
//
|
||||
// UIDs are stable per mailbox, and a mailbox that renumbers (UIDVALIDITY
|
||||
// changing) re-proposes its tasks once, which is the safe direction.
|
||||
func mailExternalID(source string, uid uint32, text string) string {
|
||||
return fmt.Sprintf("%s#%d:%s", source, uid, store.NormalizeTaskText(text))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user