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
A person in msgvault is pegged to a stream of conversations that never stops: email, chats, meetings, and now the attachments they send. The profile, by contrast, only changes when someone edits it. So the archive knows that a contact changed jobs, moved cities, got a dog, or stopped mentioning a partner months before the profile does, and "last time we talked" is whatever the user remembers.
The typed attribute layer (#535) and the structured primitives (#565) were built so that facts about a person are typed, historized, and provenance-aware. What is missing is the thing that keeps them current: a scheduled sweep that looks at what happened with a person since the last pass, updates the fields it can update safely, proposes the rest, and keeps the old values as history. Nobody breaks up with a past partner in a database; the name should still be there, closed with a date.
This issue proposes that sweep, plus the attribute catalog it maintains, so a personal-CRM profile like the one below stays populated without anyone typing:
Contact: email, phone, social profiles, chat handles
Profile: role, company, location, born in, birthday, address, membership, religion, politics, personality
Family: partner, pets, kids
Fun now, fun growing up, favorite food, favorite place, how we met, ask me about
Last contacted, contact frequency, primary channel
Last time we talked
What works today, and why it is not enough
Three pieces are in place, and one of them already does part of this without any model:
feat(activity): add dated contact activity #599 computes person_contact_state from the dated activity spine: first and last contact, last inbound and outbound, interaction count. That is "last contacted" and the basis for "primary channel" and "contact frequency", deterministically, for every person.
Add portable field metadata and typed person attributes #535 seeds primary_channel, contact_frequency, ask_me_about, and last_contacted as attribute definitions with bitemporal values (active_from/active_until plus created_at/superseded_at) and a source/confidence/actor envelope. The provenance vocabulary already distinguishes declared sources (user, carddav_import, vcard_import) from extraction and enrichment, and only the latter may carry a confidence.
What does not exist: any scheduled process that reads that activity on a person's behalf, a candidate/evidence model for extracted facts, a policy that says which facts may be written unattended and which must be reviewed, a consented inference provider, or a "last time we talked" brief. #534's constraints already say that new profile facts are typed and provenance-aware, that mutable facts retain history, and that enrichment writes candidates rather than curated facts. All of that stays true here: the model proposes; msgvault decides, per attribute class, whether a proposal may be applied without a human, and every application is reversible with its evidence attached.
Ask
Add a scheduled people sweep that keeps curated person attributes current from the archive, in focused increments.
1. Attribute catalog and extraction classes
Seed the catalog above so it is usable by hand on day one, mapped onto the primitives that already exist rather than duplicated as free-text attributes:
how we met review-only; ask me about auto-eligible
Tags
person_categories
review-only
Last contacted, primary channel, contact frequency
derived from person_contact_state; written into the seeded attributes
deterministic, everyone, no model
Last time we talked
dated versioned brief, see 5
generated per sweep
Every seed carries a subject scope: about the person, about the relationship with the user, or about a related person. A statement about a partner or child is never filed as a fact about the person.
2. Who gets swept
Deterministic fields update for every durable person; they cost nothing. Extraction runs only for persons marked as tracked (msgvault person track <id>, bulk by tag or list; publishing to an address book can imply tracking). This bounds model spend to people the user actually maintains.
3. Evidence, retrieval-first
Per tracked person, take only activity past that person's sweep watermark. For each attribute family, run a semantic probe over the person's new conversation-window, meeting, and visual-attachment embeddings ("changed jobs", "moved to", "my partner", "our dog", "birthday", "favorite restaurant"...) plus a lexical pass, and keep only windows above a similarity floor. Visual embeddings are used to find moments; the model receives the surrounding text and any stored caption or OCR, never the image bytes, in this version. Assemble a bounded evidence pack under a per-person token budget: verbatim excerpts with exact message, conversation-window, meeting-span, or attachment-occurrence pointers and dates.
4. Extraction, validation, and the two-tier write
One structured-output call per person returns candidates (subject, key, value, observed date, change date if the evidence names one, confidence, evidence indices), a brief (see 5), and uncertainties. Then, in msgvault, not in the prompt:
Alignment: each quoted excerpt must fuzzy-match its pointed source; a candidate whose evidence does not align is dropped and counted as a hallucination signal.
Subject boundary and sensitive-key checks per the catalog.
Rejection memory: a hash of (person, key, normalized value) that the user rejected is never re-proposed. Rejections are writes, never deletes.
Dedup against open candidates and current values.
Routing: a candidate is auto-applied only if its key is on the configured allowlist, confidence is at or above the configured threshold, and it does not contradict a current value whose source is declared (user, carddav_import, vcard_import). Everything else enters the review queue with pending, accepted, rejected, unsure, superseded, auto_accepted states.
Writes are bitemporal supersedes, never deletes. A new partner value closes the previous one with active_until = the change date the evidence gave, else the observed date; the old row stays queryable with its evidence. source=extraction, source_ref=candidate, actor=sweep. msgvault person attributes revert <value-id> undoes any auto-write and records the rejection.
Candidates and evidence persist as person_fact_candidates and person_fact_evidence, alongside person_sweep_state (tracked, per-lane watermarks, last run, spend) and a sweep run log. Contradictions stay visible; the sweep never resolves them by last-write-wins.
5. Last time we talked
The timestamp stays deterministic from #599. The sweep additionally writes a short structured brief per tracked person: highlights the person shared, open threads, follow-up questions, appreciation the user actually expressed, each with evidence pointers and dates, rendered to a compact paragraph for the person page and API/MCP. Each regeneration is a new dated version with its input boundary, provider, model, and program version; it never rewrites what the user previously saw. The brief can carry possible_attributes, which enter the same candidate flow.
6. Provider and consent
One inference interface with JSON-schema output, prompt/program version, and a recorded consent (provider, model, retention and training posture, allowed sources and date range) before any message text leaves the machine, in the shape #616 uses for Mistral. First adapter: ChatGPT/Codex sign-in (OAuth device login), because that is the account many users already pay for. Stated risk: it is a consumer login flow used for unattended calls, so acceptance depends on the provider's terms and on the maintainer; the OpenAI-compatible API-key adapter (OpenAI, OpenRouter, local servers) is the fallback that keeps the feature independent of that decision, and the existing Ollama [chat] server is the fully local option. Nothing here changes what the archive stores; only the derived candidates, values, and briefs are written.
7. Surfaces
[people.sweep] config: schedule (daily deterministic, weekly extraction by default), per-person and per-run token budgets, auto-apply allowlist and threshold, sensitive opt-in, provider selection.
CLI: person track|untrack, person sweep <id> [--dry-run], person candidates list|accept|reject|unsure, person attributes revert, person brief <id>.
API and generated clients for candidates, decisions, briefs, and sweep state; MCP read tools for candidates and briefs.
Web review queue and brief on the person page when the person workspace lands.
Evaluation
Frozen synthetic fixtures with expected facts, expected omissions, subject-attribution traps, and stale-fact cases; regression metrics for factual support, alignment rate, attribution errors, and no re-proposal after rejection, run in the same harness proposed for retrieval quality in #534. Sweeps must be idempotent: a second run over the same watermark produces no new candidates.
Delivery boundary
Increments, each its own PR: (a) catalog seeds, tracking flag, deterministic sweep, no model; (b) provider interface, consent, first adapter; (c) candidates, evidence, review CLI/API; (d) retrieval-first extraction and tiered auto-apply; (e) briefs; (f) web review UI. Out of scope: external enrichment providers (#534 step 15), image uploads to a vision model, free-text notes on the profile, and any change to what the archive ingests.
The situation
A person in msgvault is pegged to a stream of conversations that never stops: email, chats, meetings, and now the attachments they send. The profile, by contrast, only changes when someone edits it. So the archive knows that a contact changed jobs, moved cities, got a dog, or stopped mentioning a partner months before the profile does, and "last time we talked" is whatever the user remembers.
The typed attribute layer (#535) and the structured primitives (#565) were built so that facts about a person are typed, historized, and provenance-aware. What is missing is the thing that keeps them current: a scheduled sweep that looks at what happened with a person since the last pass, updates the fields it can update safely, proposes the rest, and keeps the old values as history. Nobody breaks up with a past partner in a database; the name should still be there, closed with a date.
This issue proposes that sweep, plus the attribute catalog it maintains, so a personal-CRM profile like the one below stays populated without anyone typing:
What works today, and why it is not enough
Three pieces are in place, and one of them already does part of this without any model:
person_contact_statefrom the dated activity spine: first and last contact, last inbound and outbound, interaction count. That is "last contacted" and the basis for "primary channel" and "contact frequency", deterministically, for every person.primary_channel,contact_frequency,ask_me_about, andlast_contactedas attribute definitions with bitemporal values (active_from/active_untilpluscreated_at/superseded_at) and asource/confidence/actorenvelope. The provenance vocabulary already distinguishes declared sources (user,carddav_import,vcard_import) fromextractionandenrichment, and only the latter may carry a confidence.What does not exist: any scheduled process that reads that activity on a person's behalf, a candidate/evidence model for extracted facts, a policy that says which facts may be written unattended and which must be reviewed, a consented inference provider, or a "last time we talked" brief. #534's constraints already say that new profile facts are typed and provenance-aware, that mutable facts retain history, and that enrichment writes candidates rather than curated facts. All of that stays true here: the model proposes; msgvault decides, per attribute class, whether a proposal may be applied without a human, and every application is reversible with its evidence attached.
Ask
Add a scheduled people sweep that keeps curated person attributes current from the archive, in focused increments.
1. Attribute catalog and extraction classes
Seed the catalog above so it is usable by hand on day one, mapped onto the primitives that already exist rather than duplicated as free-text attributes:
person_contact_points(#565)location,birthplaceperson_datesperson_addressesmembership(multi-value)family.pets(multi-value)interests.fun_now,interests.fun_growing_up,favorites.food,favorites.place(multi-value)relationship.how_we_met,relationship.ask_me_aboutperson_categoriesperson_contact_state; written into the seeded attributesEvery seed carries a subject scope: about the person, about the relationship with the user, or about a related person. A statement about a partner or child is never filed as a fact about the person.
2. Who gets swept
Deterministic fields update for every durable person; they cost nothing. Extraction runs only for persons marked as tracked (
msgvault person track <id>, bulk by tag or list; publishing to an address book can imply tracking). This bounds model spend to people the user actually maintains.3. Evidence, retrieval-first
Per tracked person, take only activity past that person's sweep watermark. For each attribute family, run a semantic probe over the person's new conversation-window, meeting, and visual-attachment embeddings ("changed jobs", "moved to", "my partner", "our dog", "birthday", "favorite restaurant"...) plus a lexical pass, and keep only windows above a similarity floor. Visual embeddings are used to find moments; the model receives the surrounding text and any stored caption or OCR, never the image bytes, in this version. Assemble a bounded evidence pack under a per-person token budget: verbatim excerpts with exact message, conversation-window, meeting-span, or attachment-occurrence pointers and dates.
4. Extraction, validation, and the two-tier write
One structured-output call per person returns candidates (subject, key, value, observed date, change date if the evidence names one, confidence, evidence indices), a brief (see 5), and uncertainties. Then, in msgvault, not in the prompt:
user,carddav_import,vcard_import). Everything else enters the review queue withpending,accepted,rejected,unsure,superseded,auto_acceptedstates.active_until= the change date the evidence gave, else the observed date; the old row stays queryable with its evidence.source=extraction,source_ref=candidate,actor=sweep.msgvault person attributes revert <value-id>undoes any auto-write and records the rejection.Candidates and evidence persist as
person_fact_candidatesandperson_fact_evidence, alongsideperson_sweep_state(tracked, per-lane watermarks, last run, spend) and a sweep run log. Contradictions stay visible; the sweep never resolves them by last-write-wins.5. Last time we talked
The timestamp stays deterministic from #599. The sweep additionally writes a short structured brief per tracked person: highlights the person shared, open threads, follow-up questions, appreciation the user actually expressed, each with evidence pointers and dates, rendered to a compact paragraph for the person page and API/MCP. Each regeneration is a new dated version with its input boundary, provider, model, and program version; it never rewrites what the user previously saw. The brief can carry
possible_attributes, which enter the same candidate flow.6. Provider and consent
One inference interface with JSON-schema output, prompt/program version, and a recorded consent (provider, model, retention and training posture, allowed sources and date range) before any message text leaves the machine, in the shape #616 uses for Mistral. First adapter: ChatGPT/Codex sign-in (OAuth device login), because that is the account many users already pay for. Stated risk: it is a consumer login flow used for unattended calls, so acceptance depends on the provider's terms and on the maintainer; the OpenAI-compatible API-key adapter (OpenAI, OpenRouter, local servers) is the fallback that keeps the feature independent of that decision, and the existing Ollama
[chat]server is the fully local option. Nothing here changes what the archive stores; only the derived candidates, values, and briefs are written.7. Surfaces
[people.sweep]config: schedule (daily deterministic, weekly extraction by default), per-person and per-run token budgets, auto-apply allowlist and threshold, sensitive opt-in, provider selection.person track|untrack,person sweep <id> [--dry-run],person candidates list|accept|reject|unsure,person attributes revert,person brief <id>.Evaluation
Frozen synthetic fixtures with expected facts, expected omissions, subject-attribution traps, and stale-fact cases; regression metrics for factual support, alignment rate, attribution errors, and no re-proposal after rejection, run in the same harness proposed for retrieval quality in #534. Sweeps must be idempotent: a second run over the same watermark produces no new candidates.
Delivery boundary
Increments, each its own PR: (a) catalog seeds, tracking flag, deterministic sweep, no model; (b) provider interface, consent, first adapter; (c) candidates, evidence, review CLI/API; (d) retrieval-first extraction and tiered auto-apply; (e) briefs; (f) web review UI. Out of scope: external enrichment providers (#534 step 15), image uploads to a vision model, free-text notes on the profile, and any change to what the archive ingests.
Refs #534, #535, #565, #589, #599, #611.