M3-53 + M3-58: name a memo, and let somebody change the name #61

Merged
wk merged 7 commits from m3-53-memo-titles into main 2026-08-11 11:54:42 -04:00
Member

Closes #53. Closes #58.

Two issues in one PR, which is Wes's call on both — recorded in .agent/m3-53-memo-titles/PLAN.md
and PLAN-58.md rather than argued. It runs past the fifteen-to-twenty-minute review the build
standard asks for.

Auto-naming (#53)

A memo gets a short title taken from what was said in it, generated on the phone, opt-in and off by
default. The title is a fourth file — <base>_title.txt, one line of UTF-8 — because the capture's
own filename is a cross-client contract that vault inbox processing already reads. Three tiers: the
Foundation Models tier where the phone can run it, NLTagger keywords everywhere else, and the
opening line when the tagger finds nothing. Turning the switch on names the memos already on the
phone, newest first.

Editing a name (#58)

A Name field in the detail screen's header. A generated title is a guess about somebody's words
and is sometimes wrong in a way only the person who recorded the memo can see.

TitleFile.write keeps its claim semantics — link(2) into place, refuses to overwrite, which is
what stops two naming passes fighting over one memo. Editing gets its own path rather than a flag on
that one: replace renames over whatever is there, remove takes the file away, and both check the
kind of what is under the name first.

Clearing the field removes the file, which leaves the memo eligible for auto-naming again. The
generator skips a memo whose title file exists, so with the switch on a cleared title comes back on
the next pass. Clearing says this title is wrong; the switch in Settings says do not name my
memos
. That is deliberate and it is the one part of this that surprises.

Nothing marks a typed title as different from a generated one, and nothing needs to — the file
existing is the whole rule.

What was validated

  • make test — 839 tests, green. make lint — clean.
  • The four Codex passes are in .agent/m3-53-memo-titles/, with every finding's disposition in
    SUMMARY.md and SUMMARY-58.md. The reviews found real faults in both halves: a killed title
    write stranding the whole title in a file nothing recognised, a naming pass that could publish a
    stale title over an edit, an edit that could outlive a deletion, MemoTitle validating a candidate
    and returning something else, and a rename that would have destroyed a symbolic link.
  • Simulator: built, launched, memos with titles rendering in the list.

What was not validated

  • The Name field has not been used by a finger. The simulator could not be driven — AppleScript
    clicks do not land as touches and there is no UI test target here — so typing, clearing, the
    sixty-character clamp, the refusal sentence, Return and tap-away-to-commit are covered only
    underneath, through MemoLibrary and TitleFile. Nothing has exercised the SwiftUI wiring.
  • Nothing on a physical device for the editing half.
  • A naming pass cancelled while suspended in a model request; the write-after-delete ordering, which
    is a structural guarantee rather than a tested one; two windows on one memo; a title edited over
    the cable with the screen open.

Both gaps are listed in full in SUMMARY-58.md under Not tested.

🤖 Generated with Claude Code

Closes #53. Closes #58. Two issues in one PR, which is Wes's call on both — recorded in `.agent/m3-53-memo-titles/PLAN.md` and `PLAN-58.md` rather than argued. It runs past the fifteen-to-twenty-minute review the build standard asks for. ## Auto-naming (#53) A memo gets a short title taken from what was said in it, generated on the phone, opt-in and off by default. The title is a fourth file — `<base>_title.txt`, one line of UTF-8 — because the capture's own filename is a cross-client contract that vault inbox processing already reads. Three tiers: the Foundation Models tier where the phone can run it, `NLTagger` keywords everywhere else, and the opening line when the tagger finds nothing. Turning the switch on names the memos already on the phone, newest first. ## Editing a name (#58) A **Name** field in the detail screen's header. A generated title is a guess about somebody's words and is sometimes wrong in a way only the person who recorded the memo can see. `TitleFile.write` keeps its claim semantics — `link(2)` into place, refuses to overwrite, which is what stops two naming passes fighting over one memo. Editing gets its own path rather than a flag on that one: `replace` renames over whatever is there, `remove` takes the file away, and both check the kind of what is under the name first. **Clearing the field removes the file, which leaves the memo eligible for auto-naming again.** The generator skips a memo whose title file *exists*, so with the switch on a cleared title comes back on the next pass. Clearing says *this title is wrong*; the switch in Settings says *do not name my memos*. That is deliberate and it is the one part of this that surprises. **Nothing marks a typed title as different from a generated one**, and nothing needs to — the file existing is the whole rule. ## What was validated - `make test` — 839 tests, green. `make lint` — clean. - The four Codex passes are in `.agent/m3-53-memo-titles/`, with every finding's disposition in `SUMMARY.md` and `SUMMARY-58.md`. The reviews found real faults in both halves: a killed title write stranding the whole title in a file nothing recognised, a naming pass that could publish a stale title over an edit, an edit that could outlive a deletion, `MemoTitle` validating a candidate and returning something else, and a `rename` that would have destroyed a symbolic link. - Simulator: built, launched, memos with titles rendering in the list. ## What was **not** validated - **The Name field has not been used by a finger.** The simulator could not be driven — AppleScript clicks do not land as touches and there is no UI test target here — so typing, clearing, the sixty-character clamp, the refusal sentence, Return and tap-away-to-commit are covered only underneath, through `MemoLibrary` and `TitleFile`. Nothing has exercised the SwiftUI wiring. - **Nothing on a physical device** for the editing half. - A naming pass cancelled while suspended in a model request; the write-after-delete ordering, which is a structural guarantee rather than a tested one; two windows on one memo; a title edited over the cable with the screen open. Both gaps are listed in full in `SUMMARY-58.md` under *Not tested*. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Opt-in auto-naming, off by default. A memo's title is a fourth file beside
it, <base>_title.txt, and the capture's own filename never changes — that
name is the cross-client contract vault inbox processing reads.

Three tiers: Apple's on-device model where it runs, NLTagger keywords
everywhere else, and the opening line truncated when neither finds
anything. Naming runs as a pass over the memo list, not in stop() or the
assemblies, so it cannot fail a recording.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nine findings accepted. The ones that changed behaviour:

- A title could still be written for a memo deleted between the check and
  the write. The title is now taken back when the memo turns out to be gone.
- Reading a title no longer depends on the switch — only generating does.
  A title on disk is a fact about the folder.
- The pass re-reads every capture, so a title edited through the Files app
  reaches the row instead of waiting for a relaunch.
- The excerpt budget was set from English and always exceeded the context
  window for Chinese, Japanese, Korean and Vietnamese.
- A failed write no longer shows a title that is in no file.
- The write's temporary name is unique, so it cannot delete a file
  somebody else put in the way.
- Backgrounding is watched at the application rather than per window.
- No Settings button under the Apple Intelligence sentence: that URL opens
  Stash's own page, which cannot fix it.
- README said three files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four security findings, all accepted:

- The commit step runs on the main actor, where deletion already runs, so
  a deletion is strictly before it or strictly after it. Two rounds of
  review found a version of the write-after-delete race; extra checks
  narrow it and cannot close it, because each check is its own operation.
- Forbidden filename characters are filtered by Unicode scalar. A
  separator followed by a combining mark is one Character that is not
  equal to it, so it reached the file with a real separator in it.
- Windows device names are refused, which the comment already claimed.
- Something occupying the title's name no longer sends a memo through the
  transcript and the model on every refresh for ever.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Codex named a mutation for each finding; each fix was checked by making
that mutation and confirming a test goes red.

- The model tier had no routing test, so deleting it stayed green.
  candidate() takes its titlers as parameters now.
- The context budget was asserted as a constant, not shown to be applied.
- The bounded read could not fail: a file of one repeated letter reads
  the same either way. The fixture discriminates now.
- supportsLocale was never exercised. The mapping is a pure function
  over both facts rather than a call into the framework.
- The claim test used a task group, which may run serially. Real threads.
- No test for a directory blocking the title name, for Windows device
  names, or for which sentence each availability state shows.

And one real gap it found: candidate() trusted each tier to sanitise its
own output. Both do, so nothing reached disk unsanitised, but the rule
lived in two places instead of one. It is applied in candidate() now.

The deletion-race gap is not fixed and is recorded in SUMMARY.md: closing
it needs a test-only suspension point in the production path, and the
guarantee is isolation rather than logic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first prompt asked for "a short factual title describing what the
recording is about", which invites a sentence — and a sentence needs a
subject and a verb, so the model supplied them whether or not the
transcript did. "First day of work and personal plans" was a real
result on a memo where nobody had said it was a first day.

Modelled on Open WebUI's chat-title prompt, which is better for reasons
worth naming:

- Examples, all bare noun phrases. A noun phrase has nowhere to put an
  invented fact, and this is what carries most of the improvement.
- One subject, singular. Memos covering several things were getting all
  of them joined with "and".
- Accuracy over creativity, and the speaker's own words.
- 2-4 words rather than 3-6.

The transcript is fenced in <transcript> tags rather than sent as the
bare prompt: a memo is somebody talking, and talking includes asking
questions and giving instructions.

Sampling is greedy — the obvious phrase is the right answer for a title,
and the same memo now names itself the same way every time.

Not copied: Open WebUI's three lines demanding raw JSON. That exists
because it parses free text from an arbitrary model; @Generable
constrains decoding to the type.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wes, 2026-08-11: "anything that already has a title shouldn't be
editable by the auto-naming system, it doesn't need to be explicitly
edited by the user."

No code change — TitleGenerator already skips any memo whose title file
exists, and asks whether the file is there rather than whether it reads
as a title. What changes is that this is now stated as the reason a
hand-edited title is safe, so nothing later invents a flag marking which
titles a person wrote. Resolves the open question #58 was filed with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Name field in the memo detail screen's header. A generated title is
sometimes wrong in a way only the person who recorded the memo can see,
and there was no way to change one.

`TitleFile.write` keeps its claim semantics — it links into place and
refuses to overwrite, which is what stops two naming passes fighting over
one memo. Editing gets its own path rather than a flag on that one:
`replace` renames over whatever is there, `remove` takes the file away,
and both check the kind of what is under the name before touching it.
Submitting resigns focus and losing focus is the only thing that writes,
so Return and tapping away cannot write the same draft twice.

Clearing the field removes the file, which leaves the memo eligible for
auto-naming again. Wes's call of 2026-08-11, written into AGENTS.md and
the decision file rather than left to be discovered.

Also from the three Codex passes over the branch, all of which found
M3-53 faults as well as this change's:

- A title write killed part-way left the whole title in a hidden file
  that nothing recognised. The launch sweep and deletion both clear
  them, matching the full shape of the name rather than its prefix so
  that a copy somebody made aside is left standing.
- The naming commit rechecks cancellation, and generation is gated on
  the app being on screen rather than on the backgrounding notification.
- `MemoTitle` applies its rules to what truncation left rather than to
  what it was given, bounds a title in UTF-8 bytes as well as
  characters, and refuses the superscript Windows device names.
- `replace` uses `lstat`, so a dangling symbolic link under the title's
  name is not destroyed by a rename.
- A recordings folder that cannot be listed is a deletion failure rather
  than "no leftover files".
- A title generated from a real memo had been copied into the
  documentation of two public repositories. Removed; the point is made
  by describing the failure instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wk merged commit 518d3c5d69 into main 2026-08-11 11:54:42 -04:00
wk deleted branch m3-53-memo-titles 2026-08-11 11:54:42 -04:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Stash/stash-ios!61
No description provided.