21: the ledger raises a session's state by the stricter of the grant's label and the result's own, so a clean session that recalls a memory-tainted note becomes untrusted and cannot write it into memory.md (found on straylight, 2026-09-25). 22: the answers to a note name notes.md, and for an untrusted add the file that was not changed. The new given tests are ledger_label.rs and new versions of memoryd's edit.rs and serve.rs; the old copies are kept as -task06 and -task10. On a scratch tree at the end of task 20: the named tests fail and then pass, 5 and 4 mutations are caught, and the gate passes with 944 tests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
139 lines
4.8 KiB
Markdown
139 lines
4.8 KiB
Markdown
# M5a task 22: the answers to a note say where it went
|
|
|
|
**Branch:** `m5a` (run `git switch m5a`; `git status --short` must be empty, otherwise stop)
|
|
**Commit subject:** `memoryd: note answers name notes.md and the file that was not changed`
|
|
|
|
## Goal
|
|
|
|
A fix for what the M5a device check showed (2026-09-25). In a session that has read untrusted
|
|
content, an `add` to `memory.md` or `user.md` is saved as an untrusted note instead. `memoryd`
|
|
answered `saved as a note (untrusted): this session has read untrusted content`, and a `note`
|
|
answered `noted (untrusted)`. Both times the model told the owner "Added to memory.md", which was
|
|
false. The new answers name `notes.md`, and for an `add` also the file that was not changed (M5a
|
|
spec, section 3, as changed in `0e5b1d2`):
|
|
|
|
| Case | Before | After |
|
|
|---|---|---|
|
|
| `note`, clean session | `noted` | `saved in notes.md` |
|
|
| `note`, untrusted session | `noted (untrusted)` | `saved in notes.md, marked untrusted: this session has read untrusted content` |
|
|
| `add`, untrusted session | `saved as a note (untrusted): this session has read untrusted content` | `not added to memory.md: this session has read untrusted content, so it was saved in notes.md as an untrusted note` (`user.md` for the other file) |
|
|
|
|
Nothing else changes: the note written to `notes.md`, its header and the journal stay the same.
|
|
|
|
## Files
|
|
|
|
- Copy: **new versions** of the given `crates/memoryd/tests/edit.rs` and
|
|
`crates/memoryd/tests/serve.rs`, from `docs/plans/M5a/files/` (the same paths under it). Do not
|
|
copy `edit.rs-task06` or `serve.rs-task10`; they are the older versions.
|
|
- Modify: `crates/memoryd/src/edit.rs`, `docs/implementer-log.md`
|
|
|
|
## The edits, exactly
|
|
|
|
All in `crates/memoryd/src/edit.rs`. Nothing else in the file changes.
|
|
|
|
**1.** The three lines
|
|
|
|
```rust
|
|
/// The answer when an untrusted session's `add` became a note.
|
|
pub const SAVED_AS_NOTE: &str =
|
|
"saved as a note (untrusted): this session has read untrusted content";
|
|
```
|
|
|
|
become
|
|
|
|
```rust
|
|
/// The answer when an untrusted session's `add` became a note. It names the file it did not change:
|
|
/// on the device the model read a shorter answer as success.
|
|
pub fn saved_as_note(file: MemoryFile) -> String {
|
|
format!(
|
|
"not added to {}: this session has read untrusted content, so it was saved in notes.md as an untrusted note",
|
|
file.name()
|
|
)
|
|
}
|
|
|
|
/// The answer to a `note` from a session that has not read untrusted content.
|
|
pub const NOTED: &str = "saved in notes.md";
|
|
|
|
/// The answer to a `note` from a session that has read untrusted content.
|
|
pub const NOTED_UNTRUSTED: &str =
|
|
"saved in notes.md, marked untrusted: this session has read untrusted content";
|
|
```
|
|
|
|
The `format!` string stays on one line, longer than 100 characters; `cargo fmt` leaves it so.
|
|
|
|
**2.** In `apply`, the comment line
|
|
|
|
```rust
|
|
// replaced by SAVED_AS_NOTE; any other action -> Err(untrusted_refusal(file)).
|
|
```
|
|
|
|
becomes
|
|
|
|
```rust
|
|
// replaced by saved_as_note(file); any other action -> Err(untrusted_refusal(file)).
|
|
```
|
|
|
|
and, a few lines below it, the line
|
|
|
|
```rust
|
|
answer: SAVED_AS_NOTE.to_string(),
|
|
```
|
|
|
|
becomes
|
|
|
|
```rust
|
|
answer: saved_as_note(file),
|
|
```
|
|
|
|
**3.** In `noted`, the two lines
|
|
|
|
```rust
|
|
"noted (untrusted)"
|
|
```
|
|
|
|
and
|
|
|
|
```rust
|
|
"noted"
|
|
```
|
|
|
|
become `NOTED_UNTRUSTED` and `NOTED`, so the block reads:
|
|
|
|
```rust
|
|
answer: if ctx.untrusted {
|
|
NOTED_UNTRUSTED
|
|
} else {
|
|
NOTED
|
|
}
|
|
.to_string(),
|
|
```
|
|
|
|
After the edits, `grep -rn SAVED_AS_NOTE crates/` prints nothing.
|
|
|
|
## Steps
|
|
|
|
- [ ] **1. Copy the two tests:**
|
|
`cp docs/plans/M5a/files/crates/memoryd/tests/edit.rs docs/plans/M5a/files/crates/memoryd/tests/serve.rs crates/memoryd/tests/`
|
|
- [ ] **2. See them fail.** `cargo test --no-fail-fast -p memoryd --test edit --test serve`.
|
|
Expected: four tests fail, `a_note_is_labelled_by_the_session` and
|
|
`an_untrusted_session_can_take_notes_but_not_change_identity` in `edit`, and
|
|
`an_untrusted_session_writes_notes_not_identity` and `review_holds_identity_writes_but_not_notes`
|
|
in `serve`. Every other test passes.
|
|
- [ ] **3. Make the edits.** Run `cargo fmt --all`.
|
|
- [ ] **4. See them pass.** The command from step 2: every test passes. Then `cargo test -p memoryd`:
|
|
every suite passes.
|
|
- [ ] **5. Run the gate.** `make gate`, with a shell timeout of at least 600000 ms. Expected last
|
|
line: `gate: ok`, with 944 tests, the same as after task 21 (this task changes tests, adds none).
|
|
- [ ] **6. Log and commit.** `git add crates/memoryd docs/implementer-log.md && git commit`
|
|
|
|
## Done when
|
|
|
|
- `make gate` prints `gate: ok` with 944 tests, and `grep -rn SAVED_AS_NOTE crates/` prints
|
|
nothing.
|
|
|
|
## Stop and report if
|
|
|
|
- Any quoted block is not in the file exactly as shown.
|
|
- Step 2 shows a different set of failing tests than the four named.
|
|
- A test other than these four fails after the edits. Do not change a test.
|