Skip to content

Make the full driver retain, so its conformance run is not vacuous - #150

Merged
YellowSnnowmann merged 2 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/150-recording-provider-retains-writes
Sep 9, 2026
Merged

Make the full driver retain, so its conformance run is not vacuous#150
YellowSnnowmann merged 2 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/150-recording-provider-retains-writes

Conversation

@YellowSnnowmann

Copy link
Copy Markdown
Contributor

Summary

The driver added in #148 accepted writes and discarded them, so its conformance run was vacuous. This makes it
retain, and adds the guard that would have caught it.

Related issue

Follow-up to #147 / #148. Unblocks tinyhumansai/openhuman#6161.

API or behavior changes

RecordingProvider now stores what it is given at the entry tier. Reads (get, forget, list,
namespaces, recall, export_page, import_records) answer from that store instead of returning empty.
Canned answers still take precedence where a caller set one — with_recall_result and
with_namespace_summaries exist so a host can drive a known result set without writing rows, and several do.

Not breaking: every previous answer was empty, so nothing that passed before can start failing on a value it
did not expect.

What went wrong

store returned Ok(()) and get returned Ok(None). That is a legitimate binding — the contract allows
it, and NullMemoryProvider is exactly that — so assert_provider probed retains_writes, got false, and
skipped all ten storage assertions. the_full_driver_conforms went green having asserted nothing about
storage.

The suite's own module docs describe this trap, and the crate exports retains_writes precisely so a caller
can check for it:

assert_provider skips every write-path assertion when the driver does not retain, so a double that
silently dropped writes would let a whole run pass vacuously. Probing for that directly is how a caller
proves its harness is real.

I did not spend it on our own driver. the_full_driver_retains_writes now does. Without that test the failure
mode is invisible — the suite passes either way, which is what makes it worth a test rather than a comment.

How it was found: bumping OpenHuman's submodule to #148 and running that host's memory suite.
driver_memory_round_trips_through_the_bound_driver fails against a driver that does not round-trip, and that
test is about the binding rather than the engine, so it should have passed.

Scope of the storage

list applies namespace, category and session — the contract's isolation rules, not query semantics. recall
is a case-insensitive substring match over content, which is what InMemoryProvider already documents as
"enough for did-the-write-land-and-come-back, and deliberately not enough to test ranking". Nothing ranks,
scores or searches. A host test that needs more than this is asserting engine behaviour and belongs upstream of
the host.

Validation

  • cargo fmt --all -- --check
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo build --all-targets --all-features
  • cargo test --all-features

Plus:

  • cargo test -p tinymemory-tinycortex --test full_provider_conformance35 passed. This is the
    target that runs assert_provider against the real engine, so it is what proves the fake and TinyCortex
    still agree now that the storage assertions actually execute.
  • the_full_driver_conforms now runs the full storage path rather than returning early at the retention
    probe.

Tests

  • the_full_driver_retains_writes — the regression guard.
  • The existing the_full_driver_conforms becomes meaningful for the first time: it now executes
    assert_store_get_round_trip, assert_upsert_replaces_rather_than_duplicates,
    assert_list_filters_narrow, assert_taint_is_preserved, assert_recall_respects_limit_and_namespace,
    assert_namespaces_preserve_their_section, assert_recall_respects_source_scope,
    assert_export_import_round_trip, assert_awkward_content_round_trips and assert_kv_round_trip, none of
    which ran before.

Each of those failures was fixed in turn rather than worked around — recall returning empty, then a stored
entry not appearing in any export page.

Documentation

The entries field documents the vacuous-pass failure and points at the guard. The portability helpers
document why they are shared. The recall and list bodies state what they deliberately do not do.

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints
  • No secrets, tokens, or .env contents in the diff or the description

…t vacuous

The driver landed accepting writes and discarding them: `store` returned
`Ok(())`, `get` returned `Ok(None)`. That is a legitimate binding — the
contract allows it and `NullMemoryProvider` is exactly that — so
`assert_provider` probed `retains_writes`, got false, and skipped all ten
storage assertions. `the_full_driver_conforms` went green having asserted
nothing about storage.

The suite's own docs warned about this, and the crate exports `retains_writes`
so a caller can catch it:

> a double that silently dropped writes would let a whole run pass vacuously.
> Probing for that directly is how a caller proves its harness is real.

Found by bumping OpenHuman's submodule to this crate and running that host's
memory suite, where `driver_memory_round_trips_through_the_bound_driver` fails
against a driver that does not round-trip. That test is about the binding, not
the engine, so it should have passed.

So: entries are stored, and `get` / `forget` / `list` / `namespaces` / `recall`
/ `export_page` / `import_records` read them. `list` applies namespace,
category and session, which are the contract's isolation rules rather than
query semantics. Nothing ranks, scores or searches beyond the case-insensitive
substring `InMemoryProvider` already documents as "enough for did-the-write-
land-and-come-back, and deliberately not enough to test ordering".

Canned answers still win where a caller set one — `with_recall_result` and
`with_namespace_summaries` exist so a host can drive a known result set without
writing rows, and several do.

`the_full_driver_retains_writes` is the regression guard. Without it this
failure mode is invisible: the suite passes either way, which is what made it
worth a test of its own rather than a comment.

The portability tier is now one implementation shared by both drivers
(`export_entries_page`, `decode_export_record`). The cursor arithmetic, the
`Invalid` on an unrecognised cursor and the `None`-terminates rule belong to
the contract rather than to either driver, and writing them twice was two
chances to get the terminator wrong in different ways — which is the same
duplication that produced three `InMemoryProvider`s across two repositories.

Refs tinyhumansai#147
@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 7360fbcb-8b1a-42e6-b4da-968779a2a828


Comment @coderabbitai help to get the list of available commands.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes: 1 lane(s) blocking, worst finding is high.

Fix or reply to the findings below and push. The next review clears this automatically once they are gone — you should not need to dismiss anything by hand.

             $0.0179 · 103,626 in / 23,447 out · 11,546 cached (11%) · openrouter/openai/text-embedding-3-small, deepseek/deepseek-v4-flash, z-ai/glm-5.2 · 551 embedded
critique:    $0.0069 · 41,704 in  / 18,144 out · 2,048 cached (5%)   · deepseek/deepseek-v4-flash
security:    $0.0088 · 37,076 in  / 5,154 out  · 9,498 cached (26%)  · deepseek/deepseek-v4-flash, z-ai/glm-5.2
tests:       $0.0014 · 16,025 in  / 88 out     · 0 cached (0%)       · deepseek/deepseek-v4-flash
description: $0.0008 · 8,821 in   / 61 out     · 0 cached (0%)       · deepseek/deepseek-v4-flash

content: content.to_string(),
namespace: Some(namespace.to_string()),
category,
timestamp: "1970-01-01T00:00:00Z".to_string(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority high critique confident

Preserve original timestamp during import

The import_records method sets timestamp to a fixed string "1970-01-01T00:00:00Z" instead of reading it from the export record's payload. This breaks timestamp preservation across export/import round-trips. The conformance suite's portability tests likely check that imported entries match the original, including timestamp. The decode_export_record function should return the timestamp, and it should be used in the MemoryEntry construction.

[RULE] data-loss ·

if scope.is_some_and(SourceScope::is_empty) {
return Ok(Vec::new());
}
let needle = query.to_lowercase();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique likely

Apply category and session_id filters in recall

The recall method only filters by namespace and does not apply the category and session_id fields from _opts. This means that recall with those filters set will return entries that should be excluded, causing conformance tests that rely on those filters to fail. The list method correctly applies these filters, so the inconsistency is likely an oversight. Add filter closures for category and session_id similar to the ones in list.

[RULE] unimplemented-filter ·

@tinysweeper tinysweeper Bot added the priority: p1 Next. Wrong behaviour a user will hit, or a security weakness behind a condition. label Sep 9, 2026
…ady does

`OwnedRecallOpts` carries `namespace`, `category` and `session_id`, and recall
honoured only the first. `list` has always applied all three. A caller that
narrowed a recall by category and got rows from another one has been told
something false about its own store, and nothing in the suite would have said
so.

Both drivers had it, so both are fixed. The reference driver's copy is
pre-existing rather than something this branch introduced, but leaving one
driver narrowing and the other not is exactly the divergence that makes a
conformance crate stop meaning anything.

`min_score` is deliberately still ignored. That one is ranking, and neither
driver ranks — `recall` here is a case-insensitive substring match, documented
as enough for "did the write land and come back" and not enough to test
ordering.

Raised by tinysweeper on tinyhumansai#150.
@YellowSnnowmann

Copy link
Copy Markdown
Contributor Author

Both findings checked against the code. One is a false positive, one is real and now fixed.

❌ high — "Preserve original timestamp during import" — false positive

The suggested fix is not possible, because there is no timestamp in an export record to preserve.

export_entries_page builds the payload as:

payload: serde_json::json!({
    "key": e.key,
    "content": e.content,
    "category": e.category.to_string(),
    "session_id": e.session_id,
}),

No timestamp field. grep -c '"timestamp"' over reference/mod.rs returns 0, so
decode_export_record has nothing to read and returning it is not an option without first changing the shared
export format — which would change the contract for the reference driver too.

The finding's stated reason is that "the conformance suite's portability tests likely check that imported
entries match the original, including timestamp". They do not: grep -c timestamp over the whole of
suite/mod.rs returns 0. No assertion exists that this could fail.

And the value is not novel. InMemoryProvider — untouched by this PR, and not flagged — stamps the identical
constant at reference/mod.rs:188:

timestamp: "1970-01-01T00:00:00Z".to_string(),

So the behaviour matches the driver already shipped beside it. There may be a case for carrying timestamps
through the portability tier at all, but that is a contract change for both drivers and the suite, not a
data-loss defect in this diff.

✅ medium — "Apply category and session_id filters in recall" — correct, fixed in 21082d5

This one is right, and its reasoning is the right reasoning: list applies namespace, category and session,
and recall applied only namespace. OwnedRecallOpts genuinely carries all three. A caller that narrowed a
recall by category and got rows from another one has been told something false about its own store, and
nothing in the suite would have caught it.

Two notes on the fix:

  • Both drivers had it. The reference driver's recall filters namespace only while its list filters all
    three, so this is pre-existing rather than introduced here. Fixed in both — leaving one driver narrowing and
    the other not is the divergence that makes a conformance crate stop meaning anything.
  • min_score is still ignored, deliberately. That one is ranking, and neither driver ranks. recall here
    is a case-insensitive substring match, documented as enough for "did the write land and come back" and not
    enough to test ordering.

Verification after the fix

fmt, clippy --all-targets --all-features -- -D warnings, build --all-targets --all-features and
test --all-features all clean. tinymemory-tinycortex --test full_provider_conformance — the target that
runs assert_provider against the real engine — still passes 35/35, so narrowing recall did not change
what TinyCortex has to uphold.

One thing worth flagging for the tool rather than this PR: the run reports indexed at 3ef78ae4c30b, which is
one commit behind the branch head it reviewed.

@YellowSnnowmann
YellowSnnowmann merged commit d217a03 into tinyhumansai:main Sep 9, 2026
22 checks passed

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previously-blocking findings are resolved. Clearing the changes request.

             $0.0589 · 82,381 in / 31,480 out · 29,686 cached (36%) · openrouter/openai/text-embedding-3-small, deepseek/deepseek-v4-flash, z-ai/glm-5.2 · 576 embedded
critique:    $0.0040 · 30,179 in / 7,494 out  · 0 cached (0%)       · deepseek/deepseek-v4-flash
security:    $0.0342 · 26,562 in / 17,302 out · 9,956 cached (37%)  · deepseek/deepseek-v4-flash, z-ai/glm-5.2
tests:       $0.0093 · 16,010 in / 2,509 out  · 12,520 cached (78%) · z-ai/glm-5.2
description: $0.0114 · 9,630 in  / 4,175 out  · 7,210 cached (75%)  · z-ai/glm-5.2

records,
next_cursor,
})
let rows: Vec<MemoryEntry> = self.rows()?.values().cloned().collect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique confident

Sort rows before computing export page

export_entries_page documents that rows must be a stable order (sorted by id), but the caller collects HashMap values without sorting. Because HashMap iteration order is non-deterministic, the cursor offset may refer to a different record across calls, causing duplicate or skipped records in paginated export.

[RULE] unsorted-rows ·

@tinysweeper

tinysweeper Bot commented Sep 9, 2026

Copy link
Copy Markdown

How this change flows

0 changed behaviours across 5 relationships. 6 surrounding behaviours are shown (60 graph nodes walked). 45 further behaviours left out to keep the diagram readable.

flowchart LR
  n0["Result"]:::impacted
  n1["rows"]:::impacted
  n2["export_page"]:::impacted
  n3["import_records"]:::impacted
  n4["recall"]:::impacted
  n5["namespaces"]:::impacted
  n1 -->|uses| n0
  n2 -->|uses| n0
  n3 -->|uses| n0
  n4 -->|uses| n0
  n5 -->|uses| n0
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge.

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later. and removed priority: p1 Next. Wrong behaviour a user will hit, or a security weakness behind a condition. labels Sep 9, 2026
YellowSnnowmann added a commit that referenced this pull request Sep 9, 2026
Four families accepted writes and answered as if none had happened:
`put_document` stored while `list_documents` returned `[]`, `put_tool_rule`
stored while `tool_rules` returned `[]`, `set_goals` stored while `goals`
returned the default, and `put_relation` stored while `relations` returned
`[]`. `delete_tool_rule` answered `false` for a rule that was there.

This is the same defect #150 fixed for the entry tier, in the families #150
did not check. The shape is worth naming because it is the one this driver
keeps reproducing: a family advertised through `as_*()`, its writes accepted,
its reads empty — which passes every structural check the suite makes and
fails only when a host asks for the row back.

Found downstream. tinyhumansai/openhuman#6161 binds this driver in place of
its in-process engine, and 22 of that host's handler tests fail on the round
trip — `doc_put` then `doc_list`, `put_tool_rule` then `tool_rules`. They are
host tests, not engine tests, so the answer is a driver that stores rather
than tests that go.

`the_full_driver_retains_every_family_it_serves` is the guard, and it is a
probe rather than a suite assertion for the same reason `retains_writes` is:
`assert_provider` gates optional families on `as_*()`, and a driver that
advertises one and discards its writes satisfies every shape check it makes.
Nothing in the suite can see this class. One probe per storing family can.

Scope held deliberately: `relations` filters on namespace, subject and
predicate — the contract's own selectors — and nothing ranks or scores.
`delete_tool_rule` requires the id *and* the tool name to match, because the
pair is what the caller asserted even though the id alone is unique.

Refs #147
YellowSnnowmann added a commit to tinyhumansai/openhuman that referenced this pull request Sep 9, 2026
`memory/test_support` built a real `TinycortexProvider` over a temp workspace
for 59 call sites, and that is what kept `tinycortex` and `tinymemory-core` —
133k lines — on this crate's test critical path long after they left the
product build (#5560).

Its docstring justified the engine on the grounds that the only alternative was
the bus, and a `dlopen`ed module is a process singleton that hangs when a second
test loads it. That was a false choice: the third option is a driver that is
neither the engine nor the bus. `tinymemory-conformance` now ships one, held to
the same contract as TinyCortex by `assert_provider` — the engine is run against
those same assertions upstream, so what a test observes here is contract
behaviour rather than one engine's behaviour.

The fixture is renamed with it. `install_tinycortex_for_test` installing a
conformance fake would be the kind of name that costs someone an hour.

45 tests are deleted rather than repointed. Each one fails against a driver that
does not filter, rank, aggregate or extract — which is to say each one was
asserting engine behaviour through a host wrapper, and upstream owns every one
of those behaviours. Repointing them would have been worse than deleting them:
they would then assert the fake's filtering, which is code written to make the
test pass.

The line was drawn by measurement, not by reading. Binding a driver that answers
empty separates the two populations by construction — a test that passes with no
data is asserting what this crate does, and one that fails is asserting data the
driver returned. 997 of 1030 memory tests passed that way, so the repoint is
free for everything that stays and the decision surface was 45 tests, not the
~300 a file-by-file reading would have suggested.

Two of those 45 turned out not to belong there. `driver_memory_round_trips_
through_the_bound_driver` and `experience_store_round_trips_over_the_bound_
driver` are about the binding rather than the engine, and they failed only
because the fake had a defect — it accepted writes and discarded them
(tinyhumansai/tinymemory#150). With that fixed they pass, and they stay.

The engine is still a dev-dependency at this commit, deliberately. Both drivers
are constructible in the same build until the manifest is cut, which is what
makes the comparison above repeatable rather than a claim.

Refs #6161
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant