Skip to content

Add reach-confined forget by id (forget_within) - #248

Merged
senamakel merged 11 commits into
tinyhumansai:mainfrom
senamakel:forget-reach
Oct 10, 2026
Merged

senamakel merged 11 commits into
tinyhumansai:mainfrom
senamakel:forget-reach

Conversation

@senamakel

@senamakel senamakel commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds MemoryEngine::forget_within(ids, reach), a forget by id that only looks for the ids inside a reach.

ForgetTarget::Ids is unscoped by design. On the CortexDB engine it lists every scope under the TinyMemory root and reads the ids' labels in each one. A host that confines a caller to its own subtree already narrows the ids with get before forgetting. The forget itself still swept the whole tree, though, so on a shared hosted engine it read other users' scopes with the caller's credential. OpenHuman's SaaS memory isolation test (tinyhumansai/openhuman#7271) found this. Nothing leaked back to the caller and nothing outside its tree was deleted, but the lookup read scopes it had no business reading.

  • api: forget_within is a new default trait method. The default, forget_within_by_get, runs get with the reach in batches of MAX_GET_IDS, then calls forget(Ids) on the ids it found. forget_within_ids validates and dedups the ids (no ids or a blank id is InvalidRequest). Both are exported.
  • cortex: overrides forget_within. It looks the ids up only in all_scopes_for(reach), the scopes a read with that reach uses. A reach without descendants needs no listing at all. A subtree reach lists only under its own node. The listing must be complete, as it is for an unscoped forget by id.
  • tools: memory_forget by ids now calls forget_within with the scope's reach when the scope has one. An unscoped ToolScope keeps using ForgetTarget::Ids.

Related issue

None. Found by tinyhumansai/openhuman#7271 ("Finding: forget-by-id sweeps the whole tree").

API or behavior changes

The change is additive, so it needs a minor bump. MemoryEngine gains forget_within, which has a default. The api crate gains forget_within_by_get and forget_within_ids. ForgetTarget is unchanged, and so is the behaviour of forget(ForgetTarget::Ids). Existing engine impls keep compiling. A wrapper engine that forwards calls should also forward forget_within. If it doesn't, it gets the get-then-forget default, which is still correct but sweeps again through the inner engine's unscoped forget by id.

Validation

  • cargo fmt --all -- --check: ok
  • cargo clippy --all-targets --all-features -- -D warnings: ok
  • cargo build --all-targets --all-features: ok
  • cargo test --all-features: all pass (869 integrations lib tests, 153 tools, 93 api, plus the integration and doc tests)
  • RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features: ok

Tests

  • cortex/engine/mod_forget_within_tests.rs runs on both wires (Direct and hosted doubles). The setup has three trees: user:ann, user:ann/agent:scout and user:anna. anna starts with ann.
    • A subtree reach forgets ann's two items. Every scope or listing prefix named on the wire (query scope/prefix and forget bodies) is at or below app:tinymemory/user:ann. Nothing names user:anna, and anna's item survives.
    • An exact reach sends no scope listing and reads only ann's own kind scopes.
    • An id outside the reach is not forgotten and sends no forget.
    • No ids is refused before any request.
    • Mutation check: I rewrote the override to sweep every_scope() and filter by admits. That fails with named "app:tinymemory" outside "app:tinymemory/user:ann" and "an exact reach needs no scope listing". Using every_scope() unfiltered fails the counts.
  • Conformance namespaces check: a forget_within under agent b's reach removes none of a, scout or job. A forget_within over agent a's subtree removes scout but not the root.
  • explore/mod_tests.rs: default-impl tests for id validation and dedup, forgetting only ids in reach, sending no forget when nothing is in reach, MAX_GET_IDS batching, and refusing before reading.
  • tools/write/mod_tests.rs: a scoped memory_forget goes through forget_within with the scope's reach, and an unscoped one through ForgetTarget::Ids.

Not covered: a subtree listing still sends prefix=<node path> with no trailing /. On a backend that matches string prefixes, the listing response may therefore include a sibling whose name starts the same way (user:anna under user:ann). Those names are dropped client-side by Reach::admits and never read. This is the same listing every subtree list/fetch already sends, so it's a separate, wider question and is left unchanged here.

Documentation

  • docs/specs/memory-v2.md: the trait listing, "Get and forget by id", the Scope paragraph and the tools section.
  • docs/architecture/namespaces.md: the reach table and the confined-caller guidance.
  • docs/architecture/api.md: the trait method table.
  • crates/tinymemory-integrations/src/cortex/README.md: the Forget bullet.

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

Summary by CodeRabbit

  • New Features
    • Added reach-scoped forgetting by ID. Requested items are removed only if they’re found within the specified reach; IDs outside it remain untouched.
    • ID-based forgetting in scoped memory tools now uses the reach-scoped operation. Unscoped forgetting by ID continues to search without a reach restriction.
    • Empty ID lists and blank IDs are rejected as invalid requests.
  • Documentation
    • Clarified the difference between unscoped ID-based forgetting and forgetting within a specified reach.

senamakel and others added 11 commits October 10, 2026 07:21
Adds a forget_within method to MemoryEngine that removes only the named
ids lying within a given reach, leaving ids outside it untouched and
reading no node the reach does not admit. A default implementation reads
the ids back through get and forgets what returns, so hosts confining a
caller to its own subtree can forget by id without touching another tree.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Expose forget_within_by_get and forget_within_ids from the explore module so
callers confined to a reach can forget by id without scanning outside it. The
ForgetTarget::Ids docs now point at forget_within rather than the get-then-forget
dance, since an engine may look for ids in every node it holds.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds a forget_within path that removes items by id only within the scopes a
reach admits, so a confined caller never lists or reads outside its reach.
The existing everywhere-by-id logic is factored into a shared helper that
both paths use.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Extend the paging test engine to record forget targets and return a
report, then add tests for forget_within covering blank and duplicate id
rejection, forgetting only ids inside the reach, skipping the engine call
when nothing is in reach, batching reads by MAX_GET_IDS, and rejecting an
empty id list before any read.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Extend the namespaces conformance case with checks that a forget by id
confined to a reach removes only the ids inside it, leaving items beyond
the reach untouched.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Clone the expected item ids before collecting them into a set so the
conformance check compiles without moving values it still needs.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Register the new forget-within test module in the cortex engine so its
cases run as part of the existing test suite.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
A forget by ids under a scope now routes through forget_within with the
scope's reach instead of forgetting across the whole tree, so ids outside
the scope are no longer affected. Unscoped forgets still use the plain
forget by id, and tests cover both paths.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The scoped forget test now builds its scope by setting the reach field on a default ToolScope instead of calling with_reach, matching the current scope type. Store calls were also reformatted to satisfy rustfmt.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Adds `forget_within(ids, reach)` to the engine trait docs, which removes only
the ids inside a reach and reads no node outside it while looking, unlike
`ForgetTarget::Ids` which is unscoped. The Cortex integration notes and the
namespace and memory-v2 specs now describe the default get-then-forget
behaviour and the conformance coverage for a forget by id within a reach.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Oct 10, 2026

Copy link
Copy Markdown

Review in Change Stack →Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: cca62a04-af4f-40ba-8589-ed21c21dd928

📥 Commits

Reviewing files that changed from the base of the PR and between e8ca6fa and ee5285e.


📒 Files selected for processing (16)
  • crates/tinymemory-api/src/conformance/suite/mod.rs
  • crates/tinymemory-api/src/conformance/suite/namespaces.rs
  • crates/tinymemory-api/src/engine/mod.rs
  • crates/tinymemory-api/src/explore/mod.rs
  • crates/tinymemory-api/src/explore/mod_tests.rs
  • crates/tinymemory-api/src/lib.rs
  • crates/tinymemory-api/src/query/mod.rs
  • crates/tinymemory-integrations/src/cortex/README.md
  • crates/tinymemory-integrations/src/cortex/engine/forget.rs
  • crates/tinymemory-integrations/src/cortex/engine/mod.rs
  • crates/tinymemory-integrations/src/cortex/engine/mod_forget_within_tests.rs
  • crates/tinymemory-tools/src/tools/write/mod.rs
  • crates/tinymemory-tools/src/tools/write/mod_tests.rs
  • docs/architecture/api.md
  • docs/architecture/namespaces.md
  • docs/specs/memory-v2.md

 ____________________________________________________________________
< Sometimes, I feel like a code reviewer in a world of copy-pasters. >
 --------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@senamakel
senamakel merged commit a8a07a9 into tinyhumansai:main Oct 10, 2026
17 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant