Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/design/brain/vault-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ card; its event id joins `event_ids`.
| `persons` | O | list. |
| `tags` | O | list. |
| `filed_by` | O | Matrix localpart of the sender. |
| `addressee` | O | who the message is spoken to, as it names them. |
| `addressee` | O | who the message is spoken to, as it names them; a correction can change it. |
| `medium` | O | `voice` \| `image` \| `video` \| `file` \| `text`. |
| `mode` | O | `monologue` \| `dialogue` \| `note`. |
| `duration_ms` | O | integer. |
Expand Down
2 changes: 1 addition & 1 deletion docs/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ The AI mishears names and gets things wrong. Correct it in the memory's thread,
> **archivist**: 📔 **Sandcastle at the lake** · Sunday, 20 September 2026 ...
> **you** (in the thread): It was Lisa who built the tower, not Bart. And it was the 19th.

The archivist answers with 👀, puts the corrected card in the thread, and confirms with ✅. You can correct the title, the date, who it is about, the summary and the facts. The recorded words themselves stay as they were.
The archivist answers with 👀, puts the corrected card in the thread, and confirms with ✅. You can correct the title, the date, who it is about, who it was for, the summary and the facts. The recorded words themselves stay as they were.

famstack saves your correction in the family's archive under your name, and the card is yours from then on: the nightly run never changes it back. To correct it again, reply in the same thread.

Expand Down
7 changes: 5 additions & 2 deletions stacklets/memory/bot/cli/diary.py
Original file line number Diff line number Diff line change
Expand Up @@ -1463,13 +1463,16 @@ def _report(cards, plan, tree, failed, *, bucket: str, room_id: str,
family list only), "topics" (topics
from the vocabulary only), "quotes" (passages copied word for word from
the entry's words; keep the card's quotes unless the correction touches
them), and "date": the day the entry happened as YYYY-MM-DD when the
correction states it, otherwise null.
them), "date": the day the entry happened as YYYY-MM-DD when the
correction states it, otherwise null, and "addressee": whom the entry is
spoken to when the correction says so (a name from the family list, or
the correction's own words such as "the kids"), otherwise null.
"""


def _card_prompt(card) -> str:
lines = [f"Title: {card.title}", f"Date: {card.on.isoformat()}",
f"Spoken to: {card.addressee or 'no one in particular'}",
f"Description: {card.description}",
f"People: {', '.join(card.persons)}",
f"Topics: {', '.join(t for t in card.tags if not t.startswith('Person: '))}",
Expand Down
22 changes: 21 additions & 1 deletion stacklets/memory/bot/diary_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ class Extraction:
# A day a family member's correction states, as YYYY-MM-DD. Honoured
# only for an entry that has corrections (see `to_card`).
date: str = ""
# Whom the entry is spoken to, as a correction states it. Only
# `correct` uses it; a first reading takes the addressee from the room.
addressee: str = ""


_MAX_QUOTES = 3
Expand Down Expand Up @@ -93,6 +96,21 @@ def _kind_only(fact: str) -> bool:
return bool(sep) and label.strip().lower() in _KIND_LABELS


def _addressee(value, people: "dict[str, str]") -> str:
"""Whom a correction says the entry was for.

A household member's name resolves to its canonical form; anyone else
("the kids", "Grandpa") is kept as the family wrote it. Nothing, or a
model's null, is "".
"""
if not isinstance(value, str):
return ""
words = " ".join(value.split())
if not words or words.lower() in ("null", "none"):
return ""
return people.get(words.lower(), words)


def _iso_day(value) -> str:
"""`value` if it is a YYYY-MM-DD day, else ""."""
if not isinstance(value, str) or not re.fullmatch(r"\d{4}-\d{2}-\d{2}", value.strip()):
Expand Down Expand Up @@ -150,6 +168,7 @@ def text(key: str) -> str:
quotes=_strings(raw.get("quotes"))[:_MAX_QUOTES],
model=model,
date=_iso_day(raw.get("date")),
addressee=_addressee(raw.get("addressee"), people),
)


Expand Down Expand Up @@ -239,7 +258,7 @@ def correct(card: Card, extraction: Extraction, *, event_id: str) -> Card:
`extraction` is the model's reading of the current card with the
correction applied. It replaces everything the model reads out of an
entry (title, description, people, tags, summary, facts, quotes) and,
when it names a day, the date. The family's words, replies and files
when the correction states them, the date and whom the entry was for. The family's words, replies and files
stay as they are. The correction's event id joins the card's, so the
card leads back to the message that changed it; the words of the
correction are kept by the vault commit, not on the card.
Expand All @@ -249,6 +268,7 @@ def correct(card: Card, extraction: Extraction, *, event_id: str) -> Card:
on, confidence = date.fromisoformat(extraction.date), "corrected"
return replace(
card, on=on, confidence=confidence,
addressee=extraction.addressee or card.addressee,
title=extraction.title.strip() or card.title,
description=extraction.description.strip(),
persons=list(extraction.persons),
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/stacklets/test_memory_diary_card.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,25 @@ def test_a_correction_can_move_the_date(self):
assert fixed.confidence == "corrected"
assert diary_card.to_entry(fixed).basis == diary.basis_text("corrected")

def test_a_correction_can_say_who_the_memory_was_for(self):
""""This was for Marge, not Homie": the reading took a nickname for
the person spoken to, and the family puts it right."""
card = replace(_card(), addressee="Homie")
fixed = diary_card.correct(card, _read(addressee="marge"), event_id="$fix")

assert fixed.addressee == "Marge"

def test_a_correction_that_names_no_one_keeps_who_it_was_for(self):
card = replace(_card(), addressee="Bart")
assert diary_card.correct(card, _read(), event_id="$fix").addressee == "Bart"

def test_someone_outside_the_household_is_kept_as_the_family_named_them(self):
"""A memo can be for "the kids" or for Grandpa: the family's words
stand when they name no household member."""
card = replace(_card(), addressee="Bart")
fixed = diary_card.correct(card, _read(addressee=" the kids "), event_id="$fix")
assert fixed.addressee == "the kids"

def test_a_correction_without_a_date_keeps_the_date(self):
fixed = diary_card.correct(_card(), _extraction(), event_id="$fix")

Expand Down
Loading