feat(memory): host-managed memory lifecycle — two-lane retrieval + after-turn record (mem0 flow) - #5327
Conversation
…ter-turn record (mem0 flow) Implements the mem0 host-managed memory flow on top of #5205, with the surface area confined to the native memory provider and run-level orchestration. Retrieval (once per run): - The loop fetches long-term (user-general) and short-term (per-thread, `threads/<thread_id>/`) memory once at the first prompt build of a run and injects both into the prompt's "memory" section, replacing the dead `memory_snippets: Vec::new()` in loop_support. A per-run OnceCell caches the fetch; subsequent model steps reuse it. - Native `retrieve_context` scopes by `invocation.scope.thread_id`: Some(T) → only that thread's `threads/<T>/` subtree (short-term); None → the user's general memory, excluding `threads/*` (long-term). The lanes are disjoint, so the host concatenates them (short-term first) under the existing 4 KiB admission budget. - Graceful degradation throughout: a memory failure degrades the lane to empty and never breaks a turn. Recording (after each turn): - New low-level `MemoryService::record_interaction(invocation, { messages, run_id, metadata })` — the mem0 `add` data shape (`user_id`/`agent_id`/ `thread_id` ride the invocation scope). A default no-op trait impl lets each provider opt in: the host passes the DATA and the provider decides what to do with it (store verbatim, run LLM extraction, or nothing). Implements the reserved `memory.interaction.record.v1` vocabulary. - The native provider stores the full turn history under `threads/<thread_id>/`. - A host `AfterTurnMemoryRecorder` fires at the run-end seam (`turn_run_executor::apply_exit`, gated on `Completed`), reads the exchange from the thread transcript with the owner-rewritten scope, and hands it down. Post-terminal and best-effort: every error is `debug!`-logged and never fails the already-completed run. Reads and writes resolve on the local-dev runtime path; the production graph wires `None` (deferred, issue #5013 — the same optionality as `user_profile_source`). Tested at unit + caller level across ironclaw_memory{,_native}, host_runtime, loop_support, turns, and reborn (two-lane fetch, prompt rendering, once-per-run cache, native record→retrieve, and a full-turn record through the executor). A full-composition e2e (record in one run → surface in a later run's model request) is called out as a follow-up. Design: docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (2)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR adds a host-managed memory lifecycle: dual-lane memory retrieval, prompt injection with per-run caching, after-turn transcript persistence, and runtime wiring for optional memory retrieval/writes across reborn execution paths. ChangesReborn memory lifecycle
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant ThreadBackedLoopContextPort
participant ProductionMemoryPromptContextService
participant MemoryService
participant InstructionBundleBuilder
ThreadBackedLoopContextPort->>ProductionMemoryPromptContextService: load_memory_snippets(request)
ProductionMemoryPromptContextService->>MemoryService: read_thread / read_long_term
ProductionMemoryPromptContextService-->>ThreadBackedLoopContextPort: admitted snippets
ThreadBackedLoopContextPort->>InstructionBundleBuilder: load_loop_context(memory_snippets)
InstructionBundleBuilder-->>ThreadBackedLoopContextPort: rendered memory section
sequenceDiagram
participant RebornTurnRunExecutor
participant AfterTurnMemoryRecorder
participant SessionThreadService
participant MemoryService
RebornTurnRunExecutor->>AfterTurnMemoryRecorder: record_completed_run(state)
AfterTurnMemoryRecorder->>SessionThreadService: read thread history
SessionThreadService-->>AfterTurnMemoryRecorder: ordered run messages
AfterTurnMemoryRecorder->>MemoryService: record_interaction(turn_run_id, transcript, metadata)
MemoryService-->>AfterTurnMemoryRecorder: recorded / skipped
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request implements the host-managed memory lifecycle (mem0 flow) for the Reborn planned loop, introducing proactive two-lane memory retrieval (short-term thread-scoped and long-term user-scoped) and an after-turn interaction recording seam. The feedback highlights a potential latency spike caused by retrying failed memory fetches on every model step, which can be resolved by caching empty results on failure. Additionally, a bug was identified in the interaction recorder where chronological searching might capture a tool call instead of the final assistant reply, and an improvement was suggested to reuse correlation IDs for better distributed tracing.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tests/support/reborn/harness.rs (1)
861-903: 🎯 Functional Correctness | 🔴 CriticalMissing required field
after_turn_memory_writerin thisDefaultPlannedRuntimePartsliteral.The struct
DefaultPlannedRuntimePartsrequires anafter_turn_memory_writerfield, which is present in other initializations within the codebase but omitted in the literal attests/support/reborn/harness.rs(lines 861-903). Addingmemory_context_service: Nonewithout includingafter_turn_memory_writerresults in a compile error (error[E0063]: missing field).🐛 Proposed fix
user_profile_source: Arc::new(EmptyUserProfileSource), memory_context_service: None, + after_turn_memory_writer: None, model_policy_guard: None,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/support/reborn/harness.rs` around lines 861 - 903, The DefaultPlannedRuntimeParts initializer in build_default_planned_runtime is missing the required after_turn_memory_writer field, causing the struct literal to fail to compile. Update this construction to include after_turn_memory_writer alongside the other runtime dependencies, matching how other DefaultPlannedRuntimeParts instances are populated in the codebase. Use the existing build_default_planned_runtime and DefaultPlannedRuntimeParts symbols to locate the spot and keep the field wiring consistent with the surrounding memory_context_service setup.
🧹 Nitpick comments (3)
docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md (1)
18-18: 📐 Maintainability & Code Quality | 🔵 TrivialOptional: specify a language on the fenced code block.
Static analysis flags the flow block as missing a language hint (MD040). Use a fenced language (e.g.
```text) for consistent rendering.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md` at line 18, The fenced flow block is missing a language hint and triggers MD040; update the markdown fence in the spec so the block uses a declared language, matching the existing fenced block style elsewhere in the document. Locate the fenced block in the design doc and change it to use a language like text for consistent rendering and static analysis compliance.Source: Linters/SAST tools
crates/ironclaw_loop_support/src/lib.rs (1)
1968-1975: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value
latest_user_message_textskips memory if the most recent user message is blank, even when an earlier user message has text.
findreturns the lastUsermessage and the trailing.filteronly inspects that one. If the latest user turn is whitespace-only (e.g. an attachment-only message), the query resolves toNoneand memory is skipped for the run, even though a prior non-blank user message exists. The doc comment ("there is no non-blank user message yet") implies it scans for any non-blank user message.This degrades gracefully (no memory rather than a failure), so it's low impact, but seeding from the latest non-blank user message would match the documented intent.
♻️ Seed from the latest non-blank user message
fn latest_user_message_text(messages: &[ContextMessage]) -> Option<String> { messages .iter() .rev() - .find(|message| message.kind == MessageKind::User) - .map(|message| message.content.clone()) - .filter(|content| !content.trim().is_empty()) + .filter(|message| message.kind == MessageKind::User) + .map(|message| message.content.clone()) + .find(|content| !content.trim().is_empty()) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_loop_support/src/lib.rs` around lines 1968 - 1975, The latest_user_message_text helper only checks the most recent User message and then filters out blanks, so a whitespace-only latest turn can suppress memory even when an earlier non-blank user message exists. Update latest_user_message_text in lib.rs to search for the latest non-blank User message directly (i.e., skip blank User messages during the reverse scan) so it matches the documented intent and returns text from the most recent meaningful user input.crates/ironclaw_reborn_composition/src/runtime.rs (1)
3082-3131: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider resolving the document-store provider once and reusing it across the three sinks.
memory_service_resolver.resolve_document_store(Arc::clone(&extension_filesystem), None)is now invoked three times (user_profile_source,memory_context_service,after_turn_memory_writer) with identical inputs, rebuilding the bound provider each time and creating three independent call sites that must stay in lockstep (the inline comments already note this pairing requirement). Resolving once into anOption<Arc<dyn MemoryService>>and cloning it into the three adapters removes the redundant builds and the drift risk.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_composition/src/runtime.rs` around lines 3082 - 3131, Resolve the document-store provider once in runtime.rs instead of calling memory_service_resolver.resolve_document_store three times for user_profile_source, memory_context_service, and after_turn_memory_writer. Add a shared local Option/Arc result near those fields, then clone or map that single resolved provider into MemoryBackedUserProfileSourceAdapter, ProductionMemoryPromptContextService::new, and the after_turn_memory_writer sink so all three stay in lockstep and avoid redundant rebuilds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_memory_native/src/service.rs`:
- Around line 396-435: `record_interaction` is appending the same turn history
multiple times because it ignores `run_id` from
`MemoryInvocation`/`TurnRunState`. Update the `record_interaction` flow in
`service.rs` to be idempotent per `run_id`: before calling `self.write` for
`threads/<T>/log.md`, check whether that `run_id` has already been recorded and
skip the append if so, or make the target entry uniquely keyed by `run_id`. Keep
the existing short-term path and `append: true` behavior, but ensure repeated
`execute_claimed_run` / `TurnStatus::Completed` transitions cannot duplicate
context.
---
Outside diff comments:
In `@tests/support/reborn/harness.rs`:
- Around line 861-903: The DefaultPlannedRuntimeParts initializer in
build_default_planned_runtime is missing the required after_turn_memory_writer
field, causing the struct literal to fail to compile. Update this construction
to include after_turn_memory_writer alongside the other runtime dependencies,
matching how other DefaultPlannedRuntimeParts instances are populated in the
codebase. Use the existing build_default_planned_runtime and
DefaultPlannedRuntimeParts symbols to locate the spot and keep the field wiring
consistent with the surrounding memory_context_service setup.
---
Nitpick comments:
In `@crates/ironclaw_loop_support/src/lib.rs`:
- Around line 1968-1975: The latest_user_message_text helper only checks the
most recent User message and then filters out blanks, so a whitespace-only
latest turn can suppress memory even when an earlier non-blank user message
exists. Update latest_user_message_text in lib.rs to search for the latest
non-blank User message directly (i.e., skip blank User messages during the
reverse scan) so it matches the documented intent and returns text from the most
recent meaningful user input.
In `@crates/ironclaw_reborn_composition/src/runtime.rs`:
- Around line 3082-3131: Resolve the document-store provider once in runtime.rs
instead of calling memory_service_resolver.resolve_document_store three times
for user_profile_source, memory_context_service, and after_turn_memory_writer.
Add a shared local Option/Arc result near those fields, then clone or map that
single resolved provider into MemoryBackedUserProfileSourceAdapter,
ProductionMemoryPromptContextService::new, and the after_turn_memory_writer sink
so all three stay in lockstep and avoid redundant rebuilds.
In `@docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md`:
- Line 18: The fenced flow block is missing a language hint and triggers MD040;
update the markdown fence in the spec so the block uses a declared language,
matching the existing fenced block style elsewhere in the document. Locate the
fenced block in the design doc and change it to use a language like text for
consistent rendering and static analysis compliance.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: e4347cd2-7fb4-4015-b5a6-5afde211af74
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (24)
crates/ironclaw_host_runtime/src/memory_context.rscrates/ironclaw_host_runtime/tests/memory_prompt_context.rscrates/ironclaw_loop_support/src/lib.rscrates/ironclaw_memory/Cargo.tomlcrates/ironclaw_memory/src/lib.rscrates/ironclaw_memory/src/service.rscrates/ironclaw_memory_native/src/lib.rscrates/ironclaw_memory_native/src/service.rscrates/ironclaw_memory_native/tests/memory_service_facade.rscrates/ironclaw_product_workflow/tests/inbound_turn_contract.rscrates/ironclaw_product_workflow/tests/support/planned_agent_loop.rscrates/ironclaw_reborn/Cargo.tomlcrates/ironclaw_reborn/src/after_turn_memory.rscrates/ironclaw_reborn/src/lib.rscrates/ironclaw_reborn/src/loop_driver_host.rscrates/ironclaw_reborn/src/runtime.rscrates/ironclaw_reborn/src/turn_run_executor.rscrates/ironclaw_reborn/tests/llm_gateway.rscrates/ironclaw_reborn/tests/loop_driver_host.rscrates/ironclaw_reborn_composition/src/runtime.rscrates/ironclaw_reborn_composition/tests/product_live_adapters.rscrates/ironclaw_turns/tests/agent_loop_host_contract.rsdocs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.mdtests/support/reborn/harness.rs
…dempotency, build break Addresses the adversarial audit + the mem0 data-parity audit + CodeRabbit on #5327. - H1 + parity (transcript): `build_exchange` → `build_transcript` now captures the FULL ordered run transcript — every user/assistant/tool message of the turn, in sequence order, every finalized assistant including the FINAL answer (fixes recording the first/intermediate assistant on multi-step runs), each tagged with its actor `name`. This is the data mem0's `add` receives. - Run-vs-session (parity): rename `MemoryServiceRecordRequest.run_id` → `turn_run_id` (per-turn provenance). mem0's session id maps to `scope.thread_id` (the conversation), NOT this field — documented on the contract + recorder so a mem0 provider can't mis-map (which would write under the turn id but read under the session id → silent short-term-recall miss). - Idempotency (CodeRabbit): native `record_interaction` writes the transcript to a per-run file `threads/<thread_id>/<turn_run_id>.md` with `append: false` (overwrite), so a scheduler re-run of an already-Completed run can't duplicate the exchange or grow an unbounded `log.md`. - Parity: add `MemoryInteractionMessage.name` (mem0 message name → per-memory `actor_id`); populate `metadata` with `{turn_run_id, correlation_id}` provenance. - Build break (CodeRabbit, critical): add the missing `after_turn_memory_writer` field to the root crate's `tests/support/reborn/harness.rs` (was `E0063`). The gate now compiles the root crate's reborn tests. - Audit M1: the per-run memory `OnceCell` no longer freezes to empty when the first prompt build has no user message — it seeds only once a real request exists. - Audit L3: `// arch-exempt: optional_arc` annotations on the new optional fields; corrected the misleading "mirrors user_profile_source" comments. - Docs: long-term lane is full-tuple `(tenant,user,agent,project)` scoped; the `threads/` reservation is advisory (L1); fetch-once-per-run has no mid-run invalidation in v1 (Q6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @coderabbitai — both findings are addressed in Your findings
Also fixed this pass (audit + mem0 data parity)
Requesting a full re-review. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
✏️ Learnings added
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_host_runtime/tests/memory_prompt_context.rs`:
- Around line 398-416: The budget-path test in memory_prompt_context.rs can
still pass with an empty snippets result, so add an explicit non-empty assertion
on the output from load_memory_snippets in the same test before checking
total_bytes and short_term_refs. Use the existing snippets variable and assert
that at least one snippet is returned, then keep the current budget and
short-term precedence checks so a regression that drops all candidates is
caught.
In `@crates/ironclaw_memory_native/src/service.rs`:
- Around line 366-381: The thread-aware filtering in service.rs is happening
after backend.search() has already been capped by request.max_snippets, so
short-term calls can lose thread-matching results when global hits rank higher.
Fix the lane selection logic in the search path around
invocation.scope.thread_id by applying the thread constraint before truncation,
either by pushing the prefix/exclusion into backend.search() or by over-fetching
and only enforcing request.max_snippets after the retain filter.
- Around line 661-686: The reserved `threads/` namespace is only being treated
as retrieval-only right now, but the write path still allows tool-authored
documents to be persisted there. Update the write handling in `service.rs` to
explicitly reject any relative path matching `THREAD_MEMORY_ROOT` before saving,
using the existing `is_thread_scoped_path`/`THREAD_MEMORY_ROOT` checks, and
return a clear failure instead of silently accepting the write. Make sure the
rejection is applied at the point where tool-originated writes are validated so
`threads/...` cannot be stored at all.
In `@crates/ironclaw_memory_native/tests/memory_service_facade.rs`:
- Around line 689-700: The test for record_interaction is not isolating the
threadless path because turn_run_id is also missing, so it can pass via a
generic no-op instead of exercising the intended branch. Update the
MemoryServiceRecordRequest in memory_service_facade::record_interaction to
supply a real turn_run_id while keeping thread_id absent, so the test
specifically verifies the “no thread to record under” behavior.
In `@crates/ironclaw_reborn/src/after_turn_memory.rs`:
- Around line 180-187: Remove the trimming step in the message-to-interaction
conversion so the original transcript text is preserved for record_interaction;
in after_turn_memory.rs, update the logic around MemoryInteractionMessage
construction to still filter out empty/blank content but pass the
message.content through unchanged instead of using map(str::trim), keeping the
full ordered transcript intact.
In `@docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md`:
- Around line 43-50: The spec still contains outdated memory-contract text that
conflicts with the current scope-based lane selection and per-run thread log
design. Rewrite the earlier sections to match the final contract everywhere in
this document: remove the superseded `thread_id` metadata/filtering and mid-run
invalidation language, replace `run_id` and `threads/<thread_id>/log.md`
references with the current per-run `threads/<thread_id>/<turn_run_id>.md`
shape, and align the `short_term` and host `add` descriptions with the shipped
behavior. Update the affected spec sections consistently so there is one
authoritative design narrative rather than layered corrections.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: e633ff8e-869d-409a-86c6-a35c75b1af87
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!**/Cargo.lock
📒 Files selected for processing (24)
crates/ironclaw_host_runtime/src/memory_context.rscrates/ironclaw_host_runtime/tests/memory_prompt_context.rscrates/ironclaw_loop_support/src/lib.rscrates/ironclaw_memory/Cargo.tomlcrates/ironclaw_memory/src/lib.rscrates/ironclaw_memory/src/service.rscrates/ironclaw_memory_native/src/lib.rscrates/ironclaw_memory_native/src/service.rscrates/ironclaw_memory_native/tests/memory_service_facade.rscrates/ironclaw_product_workflow/tests/inbound_turn_contract.rscrates/ironclaw_product_workflow/tests/support/planned_agent_loop.rscrates/ironclaw_reborn/Cargo.tomlcrates/ironclaw_reborn/src/after_turn_memory.rscrates/ironclaw_reborn/src/lib.rscrates/ironclaw_reborn/src/loop_driver_host.rscrates/ironclaw_reborn/src/runtime.rscrates/ironclaw_reborn/src/turn_run_executor.rscrates/ironclaw_reborn/tests/llm_gateway.rscrates/ironclaw_reborn/tests/loop_driver_host.rscrates/ironclaw_reborn_composition/src/runtime.rscrates/ironclaw_reborn_composition/tests/product_live_adapters.rscrates/ironclaw_turns/tests/agent_loop_host_contract.rsdocs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.mdtests/support/reborn/harness.rs
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_reborn/src/runtime.rs`:
- Around line 223-230: Soften the contract comment in runtime.rs around the
after-turn memory writer to match what record_completed_run() actually does.
Update the description near the Completed run handling and record_interaction
path so it says the executor forwards the full run transcript and only skips
runs without user/assistant content, rather than guaranteeing a strict [user,
assistant] pair. Keep the note about None degrading to no after-turn recording.
In `@crates/ironclaw_reborn/tests/loop_driver_host.rs`:
- Around line 1605-1727: Cover the real runtime wiring by adding a caller-level
test that goes through build_default_planned_runtime and verifies
after_turn_memory_writer is plumbed into the executor; the current
turn_runner_worker_records_after_turn_memory_on_completed_run test only
exercises with_after_turn_memory_recorder directly. Create a test that
constructs DefaultPlannedRuntimeParts with after_turn_memory_writer: Some(...)
and asserts the per-run thread doc is written after a completed run, so the
actual call site in build_default_planned_runtime_inner cannot regress
unnoticed.
In `@crates/ironclaw_turns/tests/agent_loop_host_contract.rs`:
- Around line 581-629: The memory section currently only checks that a
`msg:memory.*` message exists, but it does not verify that short-term snippets
keep priority over long-term ones at the render boundary. Update
`instruction_bundle_renders_memory_section_from_memory_snippets` to assert the
ordering of the rendered memory messages, and fix
`InstructionBundleBuilder::build` so it preserves the incoming
`LoopContextBundle.memory_snippets` insertion order instead of re-sorting with
`compare_snippet_refs`. Focus on the memory materialization path and the
model-visible `messages`/`materialized_messages` output.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 11e4279b-3044-44ba-a8cc-f2e52a6cc09f
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!**/Cargo.lock
📒 Files selected for processing (24)
crates/ironclaw_host_runtime/src/memory_context.rscrates/ironclaw_host_runtime/tests/memory_prompt_context.rscrates/ironclaw_loop_support/src/lib.rscrates/ironclaw_memory/Cargo.tomlcrates/ironclaw_memory/src/lib.rscrates/ironclaw_memory/src/service.rscrates/ironclaw_memory_native/src/lib.rscrates/ironclaw_memory_native/src/service.rscrates/ironclaw_memory_native/tests/memory_service_facade.rscrates/ironclaw_product_workflow/tests/inbound_turn_contract.rscrates/ironclaw_product_workflow/tests/support/planned_agent_loop.rscrates/ironclaw_reborn/Cargo.tomlcrates/ironclaw_reborn/src/after_turn_memory.rscrates/ironclaw_reborn/src/lib.rscrates/ironclaw_reborn/src/loop_driver_host.rscrates/ironclaw_reborn/src/runtime.rscrates/ironclaw_reborn/src/turn_run_executor.rscrates/ironclaw_reborn/tests/llm_gateway.rscrates/ironclaw_reborn/tests/loop_driver_host.rscrates/ironclaw_reborn_composition/src/runtime.rscrates/ironclaw_reborn_composition/tests/product_live_adapters.rscrates/ironclaw_turns/tests/agent_loop_host_contract.rsdocs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.mdtests/support/reborn/harness.rs
…ct, no transcript trim, render ordering, fetch-once-per-run
CodeRabbit:
- retrieve_context: over-fetch BEFORE the short-term thread-lane filter, then
truncate, so general hits in the global top-N can no longer starve the
thread-scoped lane (TDD).
- write: reject the reserved `threads/` namespace (fail loud). record_interaction
still writes there via a new private write_reserved_document bypass (TDD).
Un-defers audit L1.
- build_transcript: stop trimming message content — filter blank-only rows but
pass content through verbatim ("LLM data is never deleted") (TDD).
- InstructionBundleBuilder::build: preserve the host's short-term-first memory
order; drop the by-ref re-sort that scrambled lane priority at the render
boundary (TDD; updates the now-obsolete model_content-tiebreak test).
- loop_driver_host: caller-level test through build_default_planned_runtime that
the after_turn_memory_writer is plumbed into the executor (not just the builder
shortcut).
- threadless record_interaction test: supply a real turn_run_id to isolate the
no-thread branch; soften the after-turn writer contract comments
(runtime.rs + after_turn_memory.rs) to match the full-transcript behavior.
Gemini:
- load_memory_snippets_once: get_or_init + cache empty on failure = true
fetch-once-per-run, no retry-storm on a slow/down memory service (M1 early
return preserved).
- memory_context: share one correlation_id across both retrieval lanes.
Also: strengthen the aggregate-budget test with a non-empty assertion (so the
all-short-term check isn't vacuous), and align the design doc with shipped
behavior (no mid-run invalidation; full transcript; threads/ enforced).
cargo fmt + clippy clean (zero warnings); per-crate tests green: memory_native,
host_runtime, turns, loop_support, reborn.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-review round addressed —
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (2)
crates/ironclaw_reborn_composition/src/runtime.rs (1)
3117-3122: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winSoften the after-turn recording contract here too.
Line 3120 still says the executor records a strict
[user, assistant]exchange, butAfterTurnMemoryRecorder::record_completed_run()forwards the full transcript and only skips runs with no user/assistant content. This recreates the same cross-layer doc drift already fixed incrates/ironclaw_reborn/src/runtime.rs.Suggested diff
- // The executor records each Completed run's `[user, assistant]` exchange - // through `record_interaction`. `None` degrades to no after-turn recording, + // The executor forwards each Completed run's full transcript to + // `record_interaction`, skipping only runs with no user/assistant content. + // `None` degrades to no after-turn recording,As per coding guidelines, "Comments that promise guarantees across layers must either be enforced by code/tests or softened to describe intent."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_composition/src/runtime.rs` around lines 3117 - 3122, The comment in runtime.rs overstates the after-turn recording contract by saying the executor records a strict [user, assistant] exchange, but AfterTurnMemoryRecorder::record_completed_run() actually forwards the full transcript and only skips runs with no user/assistant content. Soften the wording in the nearby comment to describe the intent and fallback behavior instead of guaranteeing a strict two-message shape, keeping it aligned with the behavior already reflected in ironclaw_reborn/src/runtime.rs.Source: Coding guidelines
docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md (1)
19-29: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winCollapse this spec onto the shipped memory contract.
These sections still describe the superseded design:
long_termbeforeshort_term,thread_idmetadata/filter-based retrieval, after-turn writes feeding both lanes, andthreads/<thread_id>/log.md. The implemented contract in this PR is short-term-first ordering, scope-based lane selection, and per-runthreads/<thread_id>/<turn_run_id>.mdthat feeds only the short-term lane. As per coding guidelines,**/*.md: "After a refactor that relocates or renames code, grep for.mdandCLAUDE.mdreferences to the moved paths and update them in the same PR".Also applies to: 43-51, 93-96, 145-149
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md` around lines 19 - 29, Update this memory lifecycle spec to match the shipped contract by removing the superseded long_term-first, thread_id filter/metadata retrieval, and after-turn dual-lane write descriptions. In the sections around on_run_start, before_model_call, after_each_turn, and on_run_end, describe short-term-first ordering, scope-based lane selection, and per-run threads/<thread_id>/<turn_run_id>.md files that feed only the short-term lane. Also grep related markdown references such as the threads/<thread_id>/log.md path and replace any stale path or lifecycle wording so the spec reflects the current implementation.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_loop_support/src/lib.rs`:
- Around line 1979-1985: The helper latest_user_message_text currently returns
None when the newest user message is blank, even if an earlier MessageKind::User
message has content; update it to scan backward for the latest non-blank user
message instead of filtering after selecting the first user row. Keep the logic
localized in latest_user_message_text and ensure load_loop_context still uses it
to decide whether to call build_memory_prompt_context_request(). Add a
regression test through load_loop_context that covers a trailing blank user
message followed by an earlier non-blank user turn, and verify the memory fetch
path still runs.
In `@crates/ironclaw_memory_native/src/service.rs`:
- Around line 460-477: The write_reserved_document helper currently relies on
callers to honor the reserved threads/ contract, but it never enforces it
itself. Add an explicit target check in write_reserved_document (and/or a
focused test) to reject any non-threads/ path before
resolve_target_path/document_path is used, so the bypass guarantee matches the
MemoryService::record_interaction-only intent.
In `@crates/ironclaw_reborn/src/after_turn_memory.rs`:
- Around line 80-91: The `after_turn_memory` fallback paths are swallowing
thread-history read and memory-write errors without the required explicit
annotation. Add inline `// silent-ok: <reason>` comments at the
`list_thread_history` error branch and the memory-write failure branch to
document the intentional boundary fallback. Use the existing `after-turn memory`
handling in `after_turn_memory.rs` as the place to annotate these swallowed
errors so audits can distinguish deliberate degradation from accidental error
loss.
In `@crates/ironclaw_reborn/src/turn_run_executor.rs`:
- Around line 295-298: The post-completion call in
turn_run_executor::TurnRunExecutor::execute is awaiting
recorder.record_completed_run inline, which can block the scheduler worker.
Update the Completed branch to offload this best-effort side effect via a
bounded background task/worker or wrap the await in a short timeout, while
keeping the existing state check and after_turn_memory_recorder lookup intact.
In `@crates/ironclaw_reborn/tests/loop_driver_host.rs`:
- Around line 1674-1716: The test is shutting down the worker too early, which
can cancel the after-turn memory recorder before the thread log is written. In
the loop_driver_host test, keep polling MemoryServiceReadRequest via
memory_writer until the log at the computed path is read and asserted, and only
call scheduler_handle.shutdown() after those memory assertions complete. Use the
existing wait_for_run_status, memory_writer.read, and scheduler_handle symbols
to keep the sequencing stable.
---
Duplicate comments:
In `@crates/ironclaw_reborn_composition/src/runtime.rs`:
- Around line 3117-3122: The comment in runtime.rs overstates the after-turn
recording contract by saying the executor records a strict [user, assistant]
exchange, but AfterTurnMemoryRecorder::record_completed_run() actually forwards
the full transcript and only skips runs with no user/assistant content. Soften
the wording in the nearby comment to describe the intent and fallback behavior
instead of guaranteeing a strict two-message shape, keeping it aligned with the
behavior already reflected in ironclaw_reborn/src/runtime.rs.
In `@docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md`:
- Around line 19-29: Update this memory lifecycle spec to match the shipped
contract by removing the superseded long_term-first, thread_id filter/metadata
retrieval, and after-turn dual-lane write descriptions. In the sections around
on_run_start, before_model_call, after_each_turn, and on_run_end, describe
short-term-first ordering, scope-based lane selection, and per-run
threads/<thread_id>/<turn_run_id>.md files that feed only the short-term lane.
Also grep related markdown references such as the threads/<thread_id>/log.md
path and replace any stale path or lifecycle wording so the spec reflects the
current implementation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 293c6347-7ae7-490d-8961-628757daeee5
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!**/Cargo.lock
📒 Files selected for processing (25)
crates/ironclaw_host_runtime/src/memory_context.rscrates/ironclaw_host_runtime/tests/memory_prompt_context.rscrates/ironclaw_loop_support/src/lib.rscrates/ironclaw_memory/Cargo.tomlcrates/ironclaw_memory/src/lib.rscrates/ironclaw_memory/src/service.rscrates/ironclaw_memory_native/src/lib.rscrates/ironclaw_memory_native/src/service.rscrates/ironclaw_memory_native/tests/memory_service_facade.rscrates/ironclaw_product_workflow/tests/inbound_turn_contract.rscrates/ironclaw_product_workflow/tests/support/planned_agent_loop.rscrates/ironclaw_reborn/Cargo.tomlcrates/ironclaw_reborn/src/after_turn_memory.rscrates/ironclaw_reborn/src/lib.rscrates/ironclaw_reborn/src/loop_driver_host.rscrates/ironclaw_reborn/src/runtime.rscrates/ironclaw_reborn/src/turn_run_executor.rscrates/ironclaw_reborn/tests/llm_gateway.rscrates/ironclaw_reborn/tests/loop_driver_host.rscrates/ironclaw_reborn_composition/src/runtime.rscrates/ironclaw_reborn_composition/tests/product_live_adapters.rscrates/ironclaw_turns/src/run_profile/instruction_bundle.rscrates/ironclaw_turns/tests/agent_loop_host_contract.rsdocs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.mdtests/support/reborn/harness.rs
Resolve the single conflict in crates/ironclaw_memory/src/service.rs by combining both independently-added `#[cfg(test)] mod tests` blocks: the branch's `record_interaction` default-no-op test and the base's `write_request_*` target-validation tests now live in one module. Workspace compiles clean post-merge (cargo check --workspace --tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ite guard, bounded recorder, silent-ok, test race CodeRabbit (round 3): - latest_user_message_text: return the latest NON-BLANK user message so a blank trailing user turn doesn't drop proactive memory for the run (+ unit test). - write_reserved_document: enforce the `threads/` bypass — reject any non-threads resolved path so the public `write` guard can't be circumvented via this helper. - after_turn_memory: annotate the two intentional post-terminal fallbacks with `// silent-ok:` per the fail-loud convention. - turn_run_executor: bound the inline after-turn recorder await with a 30s timeout so a slow/hung provider can't occupy the scheduler worker. - loop_driver_host: move the sibling test's scheduler shutdown after the memory read/asserts so it can't race the recorder. - reborn_composition: soften the after-turn `[user, assistant]` contract comment to match full-transcript behavior (same fix as runtime.rs). fmt + clippy clean; tests green (loop_support 336, memory_native 20, reborn 258 + loop_driver_host 109). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Round 3 addressed + base merged —
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/ironclaw_reborn_composition/src/runtime.rs (1)
3097-3147: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
resolve_document_storeis now resolved three times over the same filesystem.
user_profile_source(Line 3097),memory_context_service(Line 3120), andafter_turn_memory_writer(Line 3139) each independently calllocal_runtime.memory_service_resolver.resolve_document_store(Arc::clone(&extension_filesystem), None), building three separate provider instances for one runtime. Resolve once into a localOption<Arc<dyn MemoryService>>and map it into the three shapes (profile adapter / prompt-context wrap / raw) to keep one construction point — which the surrounding comments already claim is the intent.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_composition/src/runtime.rs` around lines 3097 - 3147, The same document-store provider is being resolved three times from local_runtime, which creates duplicate instances instead of one shared runtime binding. Refactor the runtime setup in runtime.rs to resolve local_runtime.memory_service_resolver.resolve_document_store(...) once into a local Option and reuse that value for user_profile_source, memory_context_service, and after_turn_memory_writer, mapping it into the appropriate adapter/wrapper/raw forms via MemoryBackedUserProfileSource::new, ProductionMemoryPromptContextService::new, and the direct Arc value.
♻️ Duplicate comments (1)
docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md (1)
19-29: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winSpec still layers corrections instead of one authoritative contract.
The flow pseudocode (
filter={thread_id}, line 21), Surface A (tag writes with thread_id on DocumentMetadata+thread_id filter on search, lines 43-46), Q3 (record_interaction(messages, run_id, ...), line 79), and the Phase-2 log (threads/<thread_id>/log.md, line 148) all contradict the shipped scope-based lane selection and per-runthreads/<thread_id>/<turn_run_id>.md. The progress log corrects these below rather than fixing them in place, so the document still self-contradicts. As per coding guidelines (**/*.md: "grep for .md references to the moved paths and update them in the same PR").Also applies to: 43-50, 146-149
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md` around lines 19 - 29, The spec has conflicting memory-lifecycle contracts across the pseudocode, Surface A, Q3, and the Phase-2 log, so update all affected references to one authoritative model instead of layering corrections. Align the flow in the main lifecycle section, the `record_interaction`/Q3 text, and the log/path examples so they all use the same scope-based lane selection and per-run `threads/<thread_id>/<turn_run_id>.md` structure, and remove any stale `thread_id`-filter or `threads/<thread_id>/log.md` wording that contradicts it.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_host_runtime/src/memory_context.rs`:
- Around line 146-164: The two independent `retrieve_lane` calls in
`MemoryContext::retrieve_context` are currently awaited sequentially, which
unnecessarily adds latency on the run-start prompt path. Update this logic to
fetch the short-term and long-term lanes concurrently using `tokio::join!`,
while preserving the existing short-term-first ordering when combining results;
keep the shared `context_profile_id` and `request.max_snippets` handling intact
and use the existing `retrieve_lane` calls as the key symbols to locate the
change.
---
Outside diff comments:
In `@crates/ironclaw_reborn_composition/src/runtime.rs`:
- Around line 3097-3147: The same document-store provider is being resolved
three times from local_runtime, which creates duplicate instances instead of one
shared runtime binding. Refactor the runtime setup in runtime.rs to resolve
local_runtime.memory_service_resolver.resolve_document_store(...) once into a
local Option and reuse that value for user_profile_source,
memory_context_service, and after_turn_memory_writer, mapping it into the
appropriate adapter/wrapper/raw forms via MemoryBackedUserProfileSource::new,
ProductionMemoryPromptContextService::new, and the direct Arc value.
---
Duplicate comments:
In `@docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md`:
- Around line 19-29: The spec has conflicting memory-lifecycle contracts across
the pseudocode, Surface A, Q3, and the Phase-2 log, so update all affected
references to one authoritative model instead of layering corrections. Align the
flow in the main lifecycle section, the `record_interaction`/Q3 text, and the
log/path examples so they all use the same scope-based lane selection and
per-run `threads/<thread_id>/<turn_run_id>.md` structure, and remove any stale
`thread_id`-filter or `threads/<thread_id>/log.md` wording that contradicts it.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 92d30a1a-719b-409f-8a40-68e50330ba1f
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!**/Cargo.lock
📒 Files selected for processing (25)
crates/ironclaw_host_runtime/src/memory_context.rscrates/ironclaw_host_runtime/tests/memory_prompt_context.rscrates/ironclaw_loop_support/src/lib.rscrates/ironclaw_memory/Cargo.tomlcrates/ironclaw_memory/src/lib.rscrates/ironclaw_memory/src/service.rscrates/ironclaw_memory_native/src/lib.rscrates/ironclaw_memory_native/src/service.rscrates/ironclaw_memory_native/tests/memory_service_facade.rscrates/ironclaw_product_workflow/tests/inbound_turn_contract.rscrates/ironclaw_product_workflow/tests/support/planned_agent_loop.rscrates/ironclaw_reborn/Cargo.tomlcrates/ironclaw_reborn/src/after_turn_memory.rscrates/ironclaw_reborn/src/lib.rscrates/ironclaw_reborn/src/loop_driver_host.rscrates/ironclaw_reborn/src/runtime.rscrates/ironclaw_reborn/src/turn_run_executor.rscrates/ironclaw_reborn/tests/llm_gateway.rscrates/ironclaw_reborn/tests/loop_driver_host.rscrates/ironclaw_reborn_composition/src/runtime.rscrates/ironclaw_reborn_composition/tests/product_live_adapters.rscrates/ironclaw_turns/src/run_profile/instruction_bundle.rscrates/ironclaw_turns/tests/agent_loop_host_contract.rsdocs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.mdtests/support/reborn/harness.rs
There was a problem hiding this comment.
❌ IronLoop Review: reviewer
Verdict: ❌ Changes requested
Findings: 1 blocking / 0 notes
Next: Fix the blocking findings, push the PR branch, then re-run this reviewer.
Head: 404760cf45efe5795b6cf56b5a0d948a1c79b2ef
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
Found a runtime behavior regression in the new two-lane memory prompt assembly: the host admits long-term snippets before short-term snippets, so the active thread memory can be starved under the shared snippet/count budget despite the render layer preserving host order for short-term priority.
Findings
1. ❌ [MEDIUM] Short-term memory can be starved by long-term results
Location: crates/ironclaw_host_runtime/src/memory_context.rs:91
The combined admission loop consumes long_term before short_term and stops at request.max_snippets or the aggregate byte cap. If long-term retrieval returns enough admissible snippets, the active thread's short-term memory is never surfaced. This contradicts the render-side contract added in instruction_bundle.rs that memory arrives in host order with short-term before long-term so the active conversation keeps priority under the shared budget. Chain short_term before long_term here, and update the host-runtime tests that currently assert long-term wins under budget pressure.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloop review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloop reviewwhen the fix may affect multiple areas. - Use
@ironloop statusto check queued/running/completed/stale/stalled state while reviewers run.
| let mut admitted = Vec::new(); | ||
| let mut total_bytes = 0usize; | ||
| for snippet in snippets { | ||
| for snippet in long_term.into_iter().chain(short_term) { |
There was a problem hiding this comment.
This admits long-term snippets before short-term snippets, then stops at the shared count/byte budget. When long-term returns enough hits, the active thread lane is completely starved, even though the render layer now preserves host order specifically so short-term memory keeps priority under budget pressure. Please chain short_term before long_term and update the budget/order tests accordingly.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
crates/ironclaw_reborn_composition/src/runtime.rs (1)
3458-3466: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winStill resolving the document-store provider three times — same concern flagged in a prior review round.
memory_service_resolver.resolve_document_store(...)is called separately foruser_profile_source,memory_context_service, andafter_turn_memory_writer, with identical arguments each time. The in-code comments explicitly promise these three consumers share "the SAME document-store provider" / "the SAMEmemory_service_resolver" — but the code doesn't actually guarantee a single instance. If resolution isn't a cheap, referentially-stable lookup, prompt reads and after-turn writes can drift onto different provider instances, silently breaking the shared-cache/consistency assumption noted elsewhere in this PR ("fixed a cache issue that could freeze memory to empty").♻️ Resolve once, fan out from the shared handle
+ let resolved_memory_provider = local_runtime.and_then(|local_runtime| { + local_runtime.memory_service_resolver.resolve_document_store( + Arc::clone(&local_runtime.extension_filesystem) + as Arc<dyn ironclaw_filesystem::RootFilesystem>, + None, + ) + }); + user_profile_source: match local_runtime.and_then(|local_runtime| { - local_runtime - .memory_service_resolver - .resolve_document_store( - Arc::clone(&local_runtime.extension_filesystem) - as Arc<dyn ironclaw_filesystem::RootFilesystem>, - None, - ) - .map(MemoryBackedUserProfileSource::new) + resolved_memory_provider + .clone() + .map(MemoryBackedUserProfileSource::new) }) { Some(source) => Arc::new(MemoryBackedUserProfileSourceAdapter(source)) as Arc<dyn HostUserProfileSource>, None => Arc::new(EmptyUserProfileSource) as Arc<dyn HostUserProfileSource>, }, - memory_context_service: local_runtime - .and_then(|local_runtime| { - local_runtime - .memory_service_resolver - .resolve_document_store( - Arc::clone(&local_runtime.extension_filesystem) - as Arc<dyn ironclaw_filesystem::RootFilesystem>, - None, - ) - .map(ProductionMemoryPromptContextService::new) - }) + memory_context_service: resolved_memory_provider + .clone() + .map(ProductionMemoryPromptContextService::new) .map(|service| Arc::new(service) as Arc<dyn MemoryPromptContextService>), - after_turn_memory_writer: local_runtime.and_then(|local_runtime| { - local_runtime - .memory_service_resolver - .resolve_document_store( - Arc::clone(&local_runtime.extension_filesystem) - as Arc<dyn ironclaw_filesystem::RootFilesystem>, - None, - ) - }), + after_turn_memory_writer: resolved_memory_provider,Also applies to: 3481-3492, 3500-3508
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_reborn_composition/src/runtime.rs` around lines 3458 - 3466, Resolve the document-store provider once in runtime.rs and reuse the same handle for user_profile_source, memory_context_service, and after_turn_memory_writer instead of calling memory_service_resolver.resolve_document_store(...) separately for each. Update the shared setup around local_runtime so the result of resolve_document_store is stored in a single variable and fanned out to MemoryBackedUserProfileSource::new, MemoryBackedMemoryContextService::new, and the after-turn writer path, preserving the “same document-store provider” assumption across all three consumers.crates/ironclaw_memory_native/src/service.rs (1)
137-139: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winStill unresolved:
threads/prefix checks remain case-sensitive.
is_thread_scoped_path/thread_memory_prefix(and every call site: thewritereservation guard, and both branches of the lane split inretrieve_context) still use plainstarts_with. A previously-flagged concern on this exact code (case-insensitivethreads/comparison) doesn't have a corresponding fix confirmation in this PR's history, unlike the sibling fixes for idempotency, over-fetch ordering, and reserved-write enforcement. On macOS/Windows,Threads/foo.mdaliases the reservedthreads/foo.mdpath on disk but slips past both the publicwriteguard and the lane retains — the same "silent retrieval black hole" this PR otherwise closes.🛡️ Proposed fix
fn thread_memory_prefix(thread_id: &ThreadId) -> String { - format!("{THREAD_MEMORY_ROOT}{}/", thread_id.as_str()) + format!("{THREAD_MEMORY_ROOT}{}/", thread_id.as_str()).to_ascii_lowercase() } fn is_thread_scoped_path(relative_path: &str) -> bool { - relative_path.starts_with(THREAD_MEMORY_ROOT) + relative_path.to_ascii_lowercase().starts_with(THREAD_MEMORY_ROOT) }And normalize the lane-retain comparisons (
result.path.relative_path().starts_with(&prefix)at line 392, and the same pattern viais_thread_scoped_pathat line 398) the same way.Also applies to: 385-405, 721-729
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/ironclaw_memory_native/src/service.rs` around lines 137 - 139, `is_thread_scoped_path` and `thread_memory_prefix` still do case-sensitive `starts_with` checks, so reserved `threads/` paths can slip through on case-insensitive filesystems. Update the path-prefix comparisons used by `write` and both `retrieve_context` lane branches to compare the normalized prefix case-insensitively, and make the retain checks on `result.path.relative_path()` use the same normalized logic so `Threads/...` is treated the same as `threads/...`.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@crates/ironclaw_loop_support/src/lib.rs`:
- Around line 219-230: The new memory-context wiring is being added inline in
lib.rs, which should stay a thin facade rather than hosting a whole new context
source. Move the memory-related pieces — memory_context_service,
with_memory_context_service, load_memory_snippets_once,
build_memory_prompt_context_request, and latest_user_message_text — into a
dedicated module such as src/memory.rs, then re-export the public entry points
from lib.rs. Keep lib.rs focused on adapter/context-source exposure and preserve
the existing memory_snippets_cache behavior and API surface through the new
module.
In `@crates/ironclaw_reborn/tests/loop_driver_host.rs`:
- Around line 1697-1727: The after-turn memory document polling logic is
duplicated in the text-host tests, including the repeated
MemoryInvocation/ResourceScope setup and the retry loop that reads
threads/<thread_id>/<run_id>.md. Extract this into a shared async helper near
the existing test utilities in loop_driver_host.rs, using the same
MemoryInvocation and read loop behavior, then update both call sites to use the
helper so the timeout/backoff policy stays consistent.
---
Duplicate comments:
In `@crates/ironclaw_memory_native/src/service.rs`:
- Around line 137-139: `is_thread_scoped_path` and `thread_memory_prefix` still
do case-sensitive `starts_with` checks, so reserved `threads/` paths can slip
through on case-insensitive filesystems. Update the path-prefix comparisons used
by `write` and both `retrieve_context` lane branches to compare the normalized
prefix case-insensitively, and make the retain checks on
`result.path.relative_path()` use the same normalized logic so `Threads/...` is
treated the same as `threads/...`.
In `@crates/ironclaw_reborn_composition/src/runtime.rs`:
- Around line 3458-3466: Resolve the document-store provider once in runtime.rs
and reuse the same handle for user_profile_source, memory_context_service, and
after_turn_memory_writer instead of calling
memory_service_resolver.resolve_document_store(...) separately for each. Update
the shared setup around local_runtime so the result of resolve_document_store is
stored in a single variable and fanned out to
MemoryBackedUserProfileSource::new, MemoryBackedMemoryContextService::new, and
the after-turn writer path, preserving the “same document-store provider”
assumption across all three consumers.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: fa9c0541-ad1e-4c8e-9734-a712feaa404a
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock,!**/Cargo.lock
📒 Files selected for processing (29)
crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rscrates/ironclaw_host_runtime/src/memory_context.rscrates/ironclaw_host_runtime/tests/memory_prompt_context.rscrates/ironclaw_loop_support/src/lib.rscrates/ironclaw_memory/Cargo.tomlcrates/ironclaw_memory/src/lib.rscrates/ironclaw_memory/src/service.rscrates/ironclaw_memory_native/src/lib.rscrates/ironclaw_memory_native/src/service.rscrates/ironclaw_memory_native/tests/memory_service_facade.rscrates/ironclaw_product_workflow/tests/inbound_turn_contract.rscrates/ironclaw_product_workflow/tests/support/planned_agent_loop.rscrates/ironclaw_reborn/Cargo.tomlcrates/ironclaw_reborn/src/after_turn_memory.rscrates/ironclaw_reborn/src/lib.rscrates/ironclaw_reborn/src/loop_driver_host.rscrates/ironclaw_reborn/src/runtime.rscrates/ironclaw_reborn/src/turn_run_executor.rscrates/ironclaw_reborn/tests/llm_gateway.rscrates/ironclaw_reborn/tests/loop_driver_host.rscrates/ironclaw_reborn_composition/src/runtime.rscrates/ironclaw_reborn_composition/tests/product_live_adapters.rscrates/ironclaw_turns/src/run_profile/instruction_bundle.rscrates/ironclaw_turns/tests/agent_loop_host_contract.rsdocs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.mdtests/integration/support/group.rstests/integration/support/planned_runtime_parts_shape.rstests/integration/wiring_parity.rstests/support/reborn_parity_qa/binary_e2e.rs
| /// Optional proactive-memory source. When wired, memory snippets are fetched | ||
| /// ONCE per run (cached in `memory_snippets_cache`) and surfaced into the | ||
| /// prompt's `"memory"` section; when absent, `memory_snippets` stays empty. | ||
| /// Optional; production wires `None` pending #5013 — a composition without a | ||
| /// memory backend degrades to no memory, never failing the turn. (Unlike the | ||
| /// non-optional null-object `user_profile_source`, this is a genuine `Option`.) | ||
| // arch-exempt: optional_arc, deferred production wiring, issue #5013 | ||
| memory_context_service: Option<Arc<dyn MemoryPromptContextService>>, | ||
| /// Per-run cache for the fetched memory snippets. Shared across clones via | ||
| /// `Arc` so the "fetch once per run" guarantee holds even if the port is | ||
| /// cloned, exactly like `identity_candidates`. | ||
| memory_snippets_cache: Arc<OnceCell<Vec<LoopContextSnippet>>>, |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Extract the new memory-context wiring into its own module.
This block adds a new context source (memory_context_service field, with_memory_context_service, load_memory_snippets_once, build_memory_prompt_context_request, latest_user_message_text) directly into lib.rs, which is already a large, heavily-touched file. As per coding guidelines, crates/ironclaw_loop_support/src/**/*.rs should "Add one file per host adapter or context source," and separately, touched files in the 1,500–3,000 line band "should get shorter unless a larger feature justifies growth." Moving this to e.g. src/memory.rs and re-exporting the public builder from lib.rs keeps the facade role of lib.rs intact ("Expose loop support adapters... from src/lib.rs") without folding a whole new context source inline.
Also applies to: 308-319, 419-525, 1996-2009
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@crates/ironclaw_loop_support/src/lib.rs` around lines 219 - 230, The new
memory-context wiring is being added inline in lib.rs, which should stay a thin
facade rather than hosting a whole new context source. Move the memory-related
pieces — memory_context_service, with_memory_context_service,
load_memory_snippets_once, build_memory_prompt_context_request, and
latest_user_message_text — into a dedicated module such as src/memory.rs, then
re-export the public entry points from lib.rs. Keep lib.rs focused on
adapter/context-source exposure and preserve the existing memory_snippets_cache
behavior and API surface through the new module.
Source: Coding guidelines
🗂️ Archived IronLoop Review: reviewerThis result is from an older PR head and is no longer the active review.
Archived summaryNo concrete blocking issues found in the memory context and after-turn recording changes. The PR adds scoped long/short-term memory retrieval, provider-neutral snippet sanitization, native thread-scoped recording, and focused regression coverage. |
|
@coderabbitai full review |
✅ Action performedFull review finished. |
|
/canary |
|
Started Reborn WebUI v2 live canary for |
There was a problem hiding this comment.
✅ IronLoop Review: reviewer
Verdict: ✅ Approved
Findings: 0 blocking / 0 notes
Next: No reviewer action needed.
Head: 8522b3512d343900c230c2a97a17094b59543c61
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
No concrete blocking issues found in the memory context and after-turn recording changes. The PR adds scoped long/short-term memory retrieval, provider-neutral snippet sanitization, native thread-scoped recording, and focused regression coverage.
Findings
None.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloop review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloop reviewwhen the fix may affect multiple areas. - Use
@ironloop statusto check queued/running/completed/stale/stalled state while reviewers run.
Superseded by a later IronLoop approved review for this reviewer.
There was a problem hiding this comment.
✅ IronLoop Review: reviewer
Verdict: ✅ Approved
Findings: 0 blocking / 0 notes
Next: No reviewer action needed.
Head: d1f873e49e557d13dd273f38f5afa208dfc0feea
Run details
Status: Current
Needs human: no
Needs validation: no
Summary
No concrete blocking issues found in the reviewed diff. The change adds proactive memory context retrieval and after-turn interaction recording with focused tests around lane ordering, caching, native provider behavior, and runtime wiring.
Findings
None.
Developer follow-up
After fixing this feedback:
- Push the fix to this PR branch.
- Re-run this reviewer with
@ironloop review --agent reviewerif you only changed this reviewer's findings. - Re-run all reviewers with
@ironloop reviewwhen the fix may affect multiple areas. - Use
@ironloop statusto check queued/running/completed/stale/stalled state while reviewers run.
|
/canary |
|
Started Reborn WebUI v2 live canary for |
565b0d8
into
stack/memory/01-userland-extension
…and-extension Brings PR #6345 (= #5205 + the host-managed memory lifecycle commit #5327) up to date with main by merging the freshly-updated #5205 branch. rerere auto-replayed all 24 shared #5205 conflict resolutions; this commit resolves the incremental lifecycle-commit conflicts against main's refactored runner: - Crate renames applied: ironclaw_reborn -> ironclaw_runner, ironclaw_loop_support -> ironclaw_loop_host (the lifecycle's new files after_turn_memory.rs / memory_context.rs relocated into the renamed crates). - loop_host prompt builder: placed the lifecycle's proactive two-lane memory fetch (load_memory_snippets_once) after main's `tokio::try_join!` context restructure, before `context.messages` is consumed; kept main's `resolution` re-export + trace_loop_host_latency_ok. - turn_run_executor / runtime: threaded BOTH main's required `active_run_lookup` + `gate_record_store` and the lifecycle's `after_turn_memory_recorder` through the executor construction. - runner Cargo.toml: main's non-optional ironclaw_llm + loop_host rename + processes test-support, plus the lifecycle's ironclaw_memory / ironclaw_memory_native deps. - loop_driver_host test: ported the 2 lifecycle memory tests onto main's APIs — the §3 subagent_gate_store -> await_edge {writer,settler,evidence} split (build_test_await_edge_trio), 4-arg RebornTurnRunExecutor::new, ThreadMessageRecord created_at/updated_at, SubmitTurnRequest.requested_model. - wiring_parity / planned_runtime_parts_shape: EXPECTED_PRODUCTION_SHAPE unions gate_record_store + memory_context_service + after_turn_memory_writer (16 Option fields). - architecture: allowed ironclaw_memory -> ironclaw_prompt_envelope (the lifecycle's prompt-safe memory-context wrapping, #5327). Verified: fmt, clippy -D warnings (runner, loop_host, composition), full workspace check, and targeted tests (architecture, runner memory lifecycle, wiring_parity, group_memory, golden_payload, first-party coverage). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ecycle — implements #3537 (#6345) * test(architecture): enforce reborn dependency boundaries as a workspace-wide allowlist PR #5163 made only the memory rules allowlist-based; the other ~30 `BoundaryRule` entries stayed blocklists that under-enforce — they forbid today's offenders but would silently admit a future internal dep (e.g. `ironclaw_turns`, `ironclaw_product_workflow`, `ironclaw_reborn`). Convert the whole harness to an allowlist: - `BoundaryRule` now carries `allowed: Vec<&'static str>` instead of `forbidden`. The runner computes `forbidden = workspace_ironclaw_crates() - allowed - crate_name`, so any unlisted internal dependency now fails the boundary test. - Each crate's `allowed` set is its actual normal `ironclaw_*` dependencies, matching the dependency guardrail documented in its CLAUDE.md/AGENTS.md. - The three previously-inline allowlist rules (`ironclaw_host_api`, `ironclaw_memory`, `ironclaw_memory_native`) are folded into `boundary_rules()` so the test body is a single uniform loop. Behavior-preserving on the current (correct) dependency graph; the win is forward enforcement. Addresses serrrfirat's deferred thread on #5163 (discussion_r3468163078). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): make the host the sole constructor of admitted memory context Before this change the native provider sanitized, wrapped, and hashed each memory snippet, and the host only *asserted* the `Untrusted memory content:` prefix in `admit_memory_context_snippet`. A future untrusted provider could pre-attach that prefix (or pre-shape the snippet) and slip text past the host's prompt-safety wrapper. Move all model-visible shaping into the host so the provider can never bypass prompt safety: - `MemoryServiceContextSnippet` now carries RAW snippet text plus the resolved scope/path components (`tenant_id`, `user_id`, `agent_id`, `project_id`, `relative_path`) — no `snippet_ref`/`safe_summary`/`model_content`. - The native `retrieve_context` ranks and scope-filters candidates, then returns them raw; it no longer sanitizes, truncates, hashes, or budgets. Removed the native `sanitize_snippet_text`, `truncate_to_char_boundary`, `validate_loop_safe_summary`, `memory_snippet_display_ref`, `feed_hash`, `collect_context_snippets`, the FNV/budget consts, and the prompt-envelope dep. - The host `memory_context.rs` builds the `memory-snippet:*` reference via the canonical `ironclaw_turns::run_profile::memory_snippet_display_ref`, sanitizes + wraps the raw text (`sanitize_snippet_text` relocated here), validates through the loop's own `LoopSafeSummary` gate (collapsing the native denylist copy into one source of truth), and enforces the per-snippet (512B) + aggregate (4 KiB) budgets in the admission loop with the same break semantics the native `collect_context_snippets` used. Behavior-preserving for the native provider: model-visible output is byte-for-byte identical (same wrapping, same FNV trailing-separator `memory-snippet:cb96ed00b13e6ae4` golden ref, same caps/ordering). New coverage proves a provider that returns text merely starting with the untrusted prefix is STILL re-sanitized + re-wrapped by the host (`adapter_re_sanitizes_provider_supplied_untrusted_prefix`, `sanitize_re_wraps_text_already_carrying_untrusted_prefix`), plus the legacy ref golden lock and the host-owned aggregate-budget test. Addresses serrrfirat's deferred security thread on #5163 (discussion_r3468163070; ref-stability discussion_r3466587649). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(memory): route user-profile reads through the MemoryService facade Profile READS built the native repository/backend directly in `user_profile_source.rs` and duplicated the scope/path decision (`profile_scope_and_path` + `PROFILE_DOCUMENT_PATH`) that `NativeMemoryService` already owns for WRITES (`profile_set`). That coupled profile reads to the concrete native provider and left provider selection unable to swap reads with the rest of the memory facade. Add a provider-neutral profile read to the contract and route the host read through it: - New `MemoryService::profile_read(invocation) -> MemoryServiceProfileReadResponse` (raw document bytes) on the `ironclaw_memory` trait, with a native implementation that reuses the SAME `profile_scope_and_path` as `profile_set` — so the scope/path decision lives in exactly one place per provider. - `MemoryBackedUserProfileSource` now holds `Arc<dyn MemoryService>` and reads via `profile_read`; the host keeps only the parse + 64 KiB size-cap + validation. Deleted the duplicate host `profile_scope_and_path` / `PROFILE_DOCUMENT_PATH` and their re-export. - Production wiring uses the new `MemoryBackedUserProfileSource::from_filesystem` factory (host owns the native-provider choice, matching the memory capability); the composition layer keeps passing the workspace filesystem. Behavior-preserving: the unit tests assert identical parse/validation outcomes (now through a stub `MemoryService`), and the end-to-end `user_profile_roundtrip` test still proves the agent-scoped write → user-scoped read round trip, now through `MemoryService::profile_read`. Addresses serrrfirat's deferred thread on #5163 (discussion_r3466587663). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(memory): document the two context-path deltas from the regression audit Both deltas live in the (host-driven) context path and are intentional; this commit records why so a future reader doesn't "fix" them back toward origin: - Native `retrieve_context` uses `.with_vector(false)` while origin's prompt-context search left `vector=true`. `false` is correct for the FTS-only native backend (no embeddings wired; a vector request fails closed) and matches the native `search` method. Documented inline. - Host `map_memory_service_error` maps a failed memory-scope build to `InvalidInvocation` (via the provider's `Input` kind) where origin used `Internal`. The arm is unreachable in practice — the host validates the context scope before calling `retrieve_context` — and `InvalidInvocation` fails closed on the same axis as query validation. Documented on the mapper. Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): host memory profile catalog, host ports, native v2 manifest + binding policy (#3537) Land the host-runtime side of the remaining #3537 milestones: - M1: author the three memory CapabilityProfileContracts (context_retrieval, interaction_log, document_store) as host-defined code in `memory_profiles`, with repo conformance tests driving the real catalog through the `ironclaw_capabilities` harness. - M2: register `host.storage.sql_transaction.first_party` + `host.events.audit` (new `ironclaw_host_api` constants) in `default_host_port_catalog()`. - M3: bundle the `ironclaw.memory.native` v2 Extension Manifest (HostBundled, first_party runtime) under `assets/memory_native/`, parsed/backed from host_runtime so the manifest's `service` must match the registered native provider identity ("TOML alone is not authority"). Conformance + schema validation tests over the real bundled schemas. - M4 (host side): fail-closed `MemoryBindingPolicy` (profile_id -> provider, default-native, production rejects disabled/unverified-third-party absent an (extension_id, profile_id, deployment_profile) override). The memory-tools dispatch site now consults the binding instead of hardwiring `NativeMemoryService::from_filesystem`; non-native bindings fail closed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): add [memory] profile-binding config section (#3537) Add the `[memory]` section to `RebornConfigFile` with `profile_bindings` (profile_id -> extension_id) and `admin_overrides` (scoped to (extension_id, profile_id, deployment_profile)). Validation is structural + deployment-agnostic (non-empty fields, valid override deployment_profile or `*`); profile-id validity and fail-closed production policy are owned by the host-runtime binding resolver, which holds the profile catalog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): wire memory binding through composition + CLI startup (#3537) Resolve the memory binding policy from the `[memory]` config section + the deployment profile at startup (fail-closed: production rejects memory.disabled / unverified third-party bindings without an override), and thread the resolved document-store binding to the builtin first-party handler registry on both the local-dev and production composition paths. The CLI resolves the policy in `build_services_input_with_options` and attaches it to `RebornBuildInput`; active third-party overrides are logged (redacted) at `debug!`. Replaces the hardwired native provider selection at the dispatch site with a config-driven, profile-bound resolution. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(memory): add manifest-v2 + host-storage-port ADRs; update memory-profiles status (#3537) Add the two ADRs the issue references (0001 Extension Manifest v2 hard cutover, 0002 native memory uses host storage ports) and move memory-profiles.md from "draft zero-behavior" to Active, with an Implemented/Deferred split. Documents the gated remainder explicitly: the reborn_memory_* dual-backend SQL tables + concrete storage-port adapter + scoped HostPortView into the handler (boundary: composition crates cannot depend on the root ironclaw crate where the SQL backends live), and the default flip (blocked on /memory data + API compatibility tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): address review on the #3537 memory binding (scope guard, profile binding, fail-loud) Apply the substantive + cheap review findings from the bot review pass: - Scope-equality guard (security): the provider-neutral snippet admission path (`memory_context::admit_memory_context_snippet`) now drops any snippet whose tenant/user/agent/project scope does not match the request scope before it is hashed or admitted. Native filters earlier, but a pluggable/third-party provider must not inject cross-scope content. Adds drop/keep tests. - Profile reads honor the binding: the local-dev user-profile source now builds the native-backed reader only when the document-store profile is bound to native, degrading to empty otherwise — so profile reads stay consistent with the memory tools instead of silently staying native. The resolved binding is carried on the local-dev store-graph input / local-runtime services. - Fail-loud: `document_store_binding` returns `Result` and surfaces a missing document-store binding instead of silently falling back to native. - Char-safe redaction: `MemoryActiveOverride::redacted_summary` truncates by characters, not bytes (the byte slice could not panic — id is ASCII — but the char form follows the repo rule and is encoding-robust). - More schema coverage: valid/invalid instance fixtures for document-read, document-write, and interaction-record (previously only context-retrieve). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(memory): single MemoryServiceResolver for every memory consumer (#3537) Collapse the per-call-site memory provider construction into one resolver. Before, the memory tools and the user-profile reader each called `NativeMemoryService::from_filesystem` and re-checked the binding inline, so the "which provider, and is it permitted?" decision was duplicated. `MemoryServiceResolver` (host_runtime::memory_provider) is now that decision in one place: given a profile + per-invocation inputs (filesystem + optional prompt-write-safety sink) it resolves the bound provider or returns None (fail-closed) for disabled / unimplemented-third-party bindings. It wraps `Option<MemoryBindingPolicy>` (None = native default) so it is Default and the tool structs that hold it need no fallible constructor. - Memory tools (MemoryCapabilityState) hold a resolver and build their service through it; they no longer reference NativeMemoryService or MemoryProviderBinding. - The local-dev user-profile source builds through the same resolver (native → MemoryBackedUserProfileSource, disabled/third-party → EmptyUserProfileSource). - The context retriever already takes an injected service and draws from the resolver once production-wired. - Composition threads one `MemoryServiceResolver` (built once per runtime from the resolved policy) instead of a bare `MemoryProviderBinding`; the `document_store_binding` helper is removed. Behavior-preserving for the native default; verified by an adversarial review (completeness / fail-closed / behavior-preservation / dead-code) plus the existing memory + user_profile_roundtrip suites. fmt + clippy clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(memory): collapse memory to a single always-on ironclaw.memory.native package Collapse the two capability declarations of the one filesystem-backed memory provider into one. The model-facing memory tools (read/write/search/tree) now belong to a dedicated `ironclaw.memory.native` first-party package on the same always-on lane as `builtin` (registered directly into the builtin extension registry, not the catalog/lifecycle extension lane), replacing the anonymous `builtin.memory_*` capabilities. - read/write `implements` the `memory.document_store.v1` profile; search/tree are native conveniences that implement no profile. - Input schemas are served inline by `resolve_native_memory_input_schema_ref` via a provider-keyed `surface.rs` branch — no asset materialization, mirroring the builtin package. - The package is trusted (per-turn `provider_trust` insert + a first-party `AdminEntry`) and granted the /memory mount via the local-dev capability policy, exactly as the builtin memory tools were. Provider-swapping stays on the document-store profile binding. Behavior, I/O, data, and on-by-default availability are unchanged; only the capability identity and the derived model tool names change (builtin__memory_* -> ironclaw__memory__native__*). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(memory): load native memory from the bundled v2 TOML manifest (honor #3537) Path 1 code-constructed the native package in Rust; #3537 explicitly wants a bundled v2 Extension Manifest. This reworks native memory to parse the bundled `assets/memory_native/manifest.toml` and register the resulting package on the SAME always-on first-party lane (not the catalog/lifecycle lane), so it stays unconditionally available with no install/enable step — a v2 manifest on the always-on lane. - The manifest is reshaped from the dormant host_internal/SQL form into four model-visible memory tools: `read`/`write` implement `memory.document_store.v1` (their schema refs match the profile op refs); `search`/`tree` are native conveniences. No required host ports (filesystem-backed); the SQL/audit ports stay catalogued vocabulary for the deferred SQL milestone. - `memory_native_extension::native_memory_first_party_package()` parses the TOML (via `ExtensionManifestRecord`) into an `ExtensionPackage`; the composition registry insertion + trust + /memory grant from the prior commit are reused unchanged (same native capability ids). - Input schemas are served inline on the always-on lane via `include_str!` of the bundled asset files (the single source of truth) — no materialization. Prompt docs are added (required for model visibility) and bundled likewise. - Conformance tests updated to the lean scope: native satisfies `memory.document_store.v1`; the context_retrieval/interaction_log profiles remain defined for the deferred host-managed flow with no live implementer. `NATIVE_MEMORY_FIRST_PARTY_PROVIDER` now aliases the canonical `NATIVE_MEMORY_EXTENSION_ID` (single identity source). Behavior, I/O, data, and on-by-default availability remain unchanged from the prior commit; this changes the manifest authoring form (code -> bundled v2 TOML). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(memory): document the live always-on native v2 memory surface Update memory-profiles.md and ADR 0002 to reflect that ironclaw.memory.native is now live: its bundled v2 TOML manifest is parsed and registered on the always-on first-party lane, implementing memory.document_store.v1 via model-facing read/write tools (search/tree are native conveniences). The live provider is filesystem-backed and declares no host ports; the SQL/audit ports stay catalogued for the deferred SQL-backed milestone. The context_retrieval/interaction_log profiles remain defined with no live implementer (deferred host-managed flow). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): keep display-preview summaries working for native memory ids The projection display-preview summarizer keys on the `memory_<op>` short names via `capability_matches`, which matched `builtin.memory_<op>` by suffix. The native tools are now `ironclaw.memory.native.<op>` (suffix `.<op>`, not `.memory_<op>`), so memory input summaries — including write-content secret redaction — would have silently stopped rendering in production. Teach `capability_matches` the new id shape and update the projection test fixtures to the native ids so they exercise it. Also rename the now-misnamed `builtin_memory_search_dispatches_*` host_runtime test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): complete native-package test + schema updates for host_runtime The native-memory consolidation (ffbd298d6, dc2fbb53b) moved the memory_* capabilities out of the builtin package into the always-on `ironclaw.memory.native` package and reshaped the bundled input schemas to the four live document-store tools, but the host_runtime test suite + two schemas were left asserting the old shape — leaving `Test ironclaw_host_runtime` red on 18 memory tests. - first_party_builtin_tools.rs: resolve the memory capabilities from the native package (register it in the test registry + add its first-party trust entry) and drop the memory ids from `all_builtin_capability_ids`. - memory_native_schema_validation.rs: validate the four live tool schemas (read/write/search/tree); the removed context-retrieve / interaction-record schemas belong to the deferred host-managed flow and are no longer bundled. - search.input.v1.json: accept the `q`/`text`/`pattern` aliases that `MemoryServiceSearchRequest::from_tool_input` already honors (anyOf), so a model call using an alias is not rejected at the pre-dispatch schema boundary. - document-read.input.v1.json: reject empty and absolute paths at the schema (minLength + `^[^/]` pattern), matching the scoped-path contract. - ironclaw_memory service.rs: restore the pre-lift null-target handling — an explicit JSON `null` write target is treated as omitted (daily_log) rather than rejected, the #4547 behavior the lift had regressed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): green the Reborn e2e harness + coverage for the native package Moving the memory_* capabilities into the `ironclaw.memory.native` package left two root-crate tests red, because the e2e harness and the e2e coverage allowlist still assumed memory lived in `builtin`: - reborn_trace_first_party_tool_coverage.rs: build the covered-capability set from the union of the builtin and native-memory packages, so the always-on first-party surface (which now spans two packages) is fully checked. - tests/support/reborn/harness.rs: register `native_memory_first_party_package` in the core-builtins runtime (via a shared `core_builtins_extension_registry` so the two core-builtins runtimes cannot drift), trust the native provider at the host-policy and per-run authority levels, and scope memory grants to filesystem effects so they fit the native provider's tight authority ceiling. Fixes `reborn_builtin_first_party_capability_e2e_coverage_is_complete` and `reborn_trace_memory_first_party_tools_parity` on `Reborn root tests` and `Tests (all-features)`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): config-driven mem0 document-store provider (#3537) Make the memory document-store provider swappable to mem0 entirely through config, with no hardcoded native assumption in the kernel — demonstrating the #3537 memory architecture is genuinely pluggable. Follows the established `ironclaw_embeddings::create_provider` config-driven-factory idiom. - ironclaw_host_runtime/memory_provider.rs: remove the hardwired native-or-none logic; `MemoryServiceResolver` is now a provider-agnostic registry (`BTreeMap<extension_id, Arc<dyn MemoryService>>` + a `with_third_party_document_store_provider` builder). `resolve_document_store` matches the binding (Native -> build native / ThirdParty(id) -> registered instance, None if unregistered / Disabled -> None). It names no concrete third-party provider, so host_runtime keeps zero provider deps. - crates/ironclaw_memory_mem0: new provider crate implementing MemoryService over the mem0 REST API. Real reqwest transport behind a `Mem0Transport` trait (SSRF-checked base URL, `Authorization: Token` header) with a panic-free mock for tests. Depends only on ironclaw_memory + ironclaw_host_api. - ironclaw_reborn_composition: `create_document_store_provider(binding, deps)` factory (embeddings idiom: match Native/ThirdParty/Disabled, build mem0 over its real transport with check_base_url, fail-closed None on missing creds) plus `build_memory_service_resolver` that registers third-party providers; wired into all three resolver-construction sites in factory.rs. - Config: `[memory] mem0_base_url` + env MEMORY_MEM0_{API_KEY,BASE_URL,APP_ID} (API key as SecretString), mirroring EmbeddingsConfig. - Tests: end-to-end swap proof (config -> policy -> factory -> registry -> resolve_document_store returns mem0, not native; write+search route through the mem0 mock transport), mem0 unit tests, and a caller-level tool-dispatch test proving the unchanged ironclaw.memory.native.* tools transparently route to mem0 under a binding. Architecture boundary allowlist updated for the new crate (composition may depend on it; host_runtime may not). Scope: document-store swap only. The host-managed retrieve/record lifecycle and SQL storage-port backing remain the deferred #5264 follow-ups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(memory): host drops provider-supplied cross-scope context snippets Add a full-pipeline (`load_memory_snippets`) negative test proving the host drops memory-context snippets whose resolved tenant/user scope does not match the request scope, even when the provider returns them — keeping only the in-scope snippet. The scope guard was unit-tested at the `admit_*` level; this exercises it end-to-end against a malicious or buggy provider, which is now a live possibility with config-bound third-party providers like mem0 (#5264). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): make the mem0 provider fully local (self-hosted OSS) Re-target the mem0 document-store provider from mem0's hosted cloud API to a self-hosted mem0 open-source instance on localhost — no dependency on api.mem0.ai and no cloud API key. Proven end-to-end against a real local stack (mem0ai + Qdrant + an Ollama embedder): store -> search-recall -> verbatim read round-trips, with the data physically in the local vector store. - transport.rs: the API key is now optional (`Option<&str>` — the auth header is sent only when a key is present); add `.timeout(30s)` + `.redirect(none)` (review hardening, finding #2). - service.rs: local OSS paths (`/memories`, `/search`, `GET /memories?user_id=`) instead of the hosted `/v1/memories/...`; `add` sends `infer:false` so mem0 stores content verbatim (document-store semantics need only the embedder, not the extraction LLM). `profile_set` is now field-preserving (read-merge-write, latest selected by `created_at`) instead of last-writer-wins (review finding #1 — no more silent profile-field loss); merge + infer-false unit tests added. - lib.rs: extension id `mem0.cloud.memory` -> `mem0.local.memory`; docs rewritten to the local OSS surface. - composition factory: build the provider with an optional key (no longer fails closed on a missing key); reborn_cli defaults `MEMORY_MEM0_BASE_URL` to `http://localhost:8888`; config doc updated. - swap-test fixtures updated to the local API; new `tests/live_local_mem0.rs` (`#[ignore]`'d) drives the real transport against a running local mem0. The document-store swap is proven at the provider level. The full LLM-agent loop using memory remains blocked on this branch by the pre-existing #5206 worker-pool stall (fixed on main) and is a tracked follow-up (#5264). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): keep mem0 off by default (no default base URL) Remove the `http://localhost:8888` default for the mem0 connection base URL so mem0 is fully opt-in: it activates only when an operator both binds the document-store profile to it AND supplies a base URL (the `[memory]` config or `MEMORY_MEM0_BASE_URL`). A bound-but-unconfigured mem0 fails closed in the factory, and the binding policy already defaults to native — so the shipped default memory layer is unchanged (native filesystem); mem0 never engages unless explicitly configured. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): apply consolidated review findings (docs, secrets, fail-loud, tests) From a 5-agent end-to-end review of the PR. All low-risk: - mem0 provider: reject embedded-credential base URLs (redacted in the error) and run `memory.mem0_base_url` through the config inline-secret guard; correct the stale "defaults to localhost:8888" doc-comments (there is no default — a bound-but-unset mem0 fails closed); add the missing `created_at` newest-profile test; fail loud (`CorruptProfile`) instead of silently dropping fields when an existing profile blob is unparseable; add the `// silent-ok:` annotation and drop cloud (`api.mem0.ai`, `/v1/`) remnants from test/doc strings. - reborn_cli: `optional_nonempty_env` fails loud on a non-UTF-8 value (NotPresent -> None, NotUnicode -> Err) for the three MEMORY_MEM0_* reads. - reborn_config: deployment-profile validation uses `RebornProfile::from_str` instead of matching string literals (types.md). - architecture: add a boundary-test guard asserting host_runtime stays memory-provider-neutral (only composition may name `ironclaw_memory_mem0`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(memory): document that prompt-write-safety is native-only (#5264) A third-party document_store binding (e.g. mem0) does not get write-time prompt-write-safety enforcement or per-write audit, since that engine lives inside the native provider. Spell out the security limitation in resolve_document_store + why it is acceptable for the off-by-default surface (third parties cannot reach the trusted prompt surface; all retrieved content is host-wrapped untrusted), and that hoisting it host-side is deferred to #5264. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(memory): feature-gate the mem0 provider behind `memory-mem0` (off by default) mem0 is now opt-in at build time, mirroring ironclaw_llm/root-llm-provider: ironclaw_memory_mem0 is an optional dependency enabled by a new `memory-mem0` feature on composition, so a default build carries no mem0 code or its reqwest/rustls transport. The factory's mem0 construction (plus its test seam, tests, and the swap integration test) are #[cfg(feature = "memory-mem0")]; a mem0 binding fails closed when the feature is not compiled in. The architecture boundary test asserts the gating (mirroring root-llm-provider), and CI runs the composition suite with the feature so the mem0 tests still execute (feature-off stays covered by the --no-default-features composition run in test.yml). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): address CodeRabbit re-review findings (mem0 correctness, schemas, secrets) From CodeRabbit's full re-review of the rebased PR. All low-risk: - mem0 provider: read fragments now sort by created_at (mem0 list order is not chronological) so append-style docs read back in order; a replace write (append=false) is rejected as an Unsupported operation instead of silently becoming an add that misreports append:true; check_base_url fails closed on hosted mem0 cloud hosts (mem0.ai / *.mem0.ai), enforcing self-hosted-OSS-only; the InvalidUrl error no longer echoes the configured URL (drops host/query), keeping only the cause. - reborn_config: mem0_base_url is validated non-empty + trimmed (check_non_empty_trimmed). - native memory schemas: tree.input rejects absolute / .. / backslash paths (fail closed, empty root still allowed); document-read.output requires word_count; search.input requires a non-empty query and forbids conflicting aliases (oneOf). - docs: memory-profiles.md non-goals updated (mem0 provider now exists, off by default, feature-gated); host_port.rs docstring no longer over-claims native backing (native memory is filesystem-backed, declares no host ports). - memory_binding: regression test locking that a case variant of the native id fails closed (ExtensionId grammar is lowercase-only) rather than misclassifying. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): address CodeRabbit re-review round 2 (target containment, fail-loud, docs) From CodeRabbit's fresh full re-review. Most notable: the native memory JSON input schemas are model-facing (advertised in parameters_schema) and are NOT host-validated against actual tool arguments before dispatch, so a traversal target could reach a provider verbatim. Added a provider-neutral reject_out_of_scope_target guard in MemoryServiceWriteRequest::from_tool_input (mirrors the schema pattern) so every bound provider -- including mem0, which stores the target verbatim -- gets containment, not just native. Also: - document-write.input schema rejects absolute / .. / backslash targets (fail closed, matching the sibling schemas). - mem0 response_items fails loud (UnrecognizedResponse) on an unrecognized 2xx body instead of silently returning empty (which let a malformed list response overwrite existing profile fields). - docs: manifest description scoped (search/tree implement no portable profile); extension_contracts + transport docstrings corrected (native is filesystem- backed / declares no host ports; non-JSON bodies degrade to Null, not an error). The mem0 replace-write rejection is provider-specific (mem0 OSS is append-only); native still supports replace, so the shared write prompt is left unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(memory): forward the memory-mem0 feature from the reborn CLI Completes the mem0 feature-gate: ironclaw_reborn_cli now exposes a memory-mem0 feature that enables ironclaw_reborn_composition/memory-mem0, so an ironclaw-reborn binary built with --features memory-mem0 can bind memory.document_store.v1 to a self-hosted mem0 server. Without it the feature was only reachable on the composition crate, never the actual binary. Mirrors the existing root-llm-provider / webui-v2-beta forwarding. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps): ignore RUSTSEC-2026-0187 (lopdf PDF stack-overflow, bounded DoS) New advisory on lopdf (via pdf-extract in ironclaw_extractors, PDF attachment extraction) with no patched release in our semver range yet. Bounded DoS only -- a crash of the extraction task on a hostile user-supplied PDF, not RCE and not the whole process. Matches the existing deny.toml ignore pattern; remove once lopdf/pdf-extract ship a fixed release. Unrelated to the memory work; surfaced because the main merge re-ran cargo-deny. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): workspace-bound local-dev mem0 isolation (fold workspace + scope into user_id) Local-dev native memory is isolated per workspace (its filesystem store lives under local_dev_root); mem0 (a shared server) was not, since the local-dev runtime uses a fixed scope. mem0 OSS enforces search/get_all filtering by user_id (and agent_id) but NOT by app_id -- a top-level app_id is accepted yet silently ignored when filtering (verified empirically: cross-app_id queries leak). So we encode the entire partition into the one key guaranteed enforced: - The provider folds the workspace partition (config.app_id, set per-workspace by the local-dev composition from local_dev_root) into the user_id namespace at all 7 namespace sites (search/write/read/tree/profile-read/profile-set/retrieve_context). app_id is still stamped as forward-compat metadata but is NOT relied on for isolation. - The local-dev composition derives a per-workspace app_id from the canonical local-dev root; production is untouched (no app_id -> pure scope namespace, so memory persists across restarts for the same scope). Result: local-dev mem0 partitions like native (workspace x tenant/user/agent/project). Verified by a live cross-isolation test (cross-workspace AND cross-scope both return zero on search and list) + a regression guard locking the user_id prefix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): make agent-facing memory surface backend-agnostic (ironclaw.memory.*) The bundled memory extension exposed its model-facing tools as `ironclaw.memory.native.*`, leaking the default provider's name into the one surface that must be backend-blind — the agent's tool list. With the mem0 swap the model still saw `ironclaw__memory__native__*` while running on mem0, contradicting #3537's agnostic goal. Rename the agent-facing extension id + capabilities to `ironclaw.memory.*` (model now sees `ironclaw__memory__{read,search,write,tree}`) and neutralize the manifest name/description. "native" is kept only where it's true — the internal default provider: `native_memory_provider` service, `ironclaw_memory_native` crate, `NativeMemoryService`, the bundled asset/prompt dirs. The last dot-segment (read/search/write/tree) is preserved, so the benchmark retrieval matcher (keys on `rsplit('.').next()`) is unaffected. Tests: host_runtime memory/manifest/surface, capability-profile conformance, and reborn_composition projection all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): address userland extension review feedback * Fix native memory CI harness wiring * feat(memory): host-managed memory lifecycle — two-lane retrieval + after-turn record (mem0 flow) (#5327) * feat(memory): host-managed memory lifecycle — two-lane retrieval + after-turn record (mem0 flow) Implements the mem0 host-managed memory flow on top of #5205, with the surface area confined to the native memory provider and run-level orchestration. Retrieval (once per run): - The loop fetches long-term (user-general) and short-term (per-thread, `threads/<thread_id>/`) memory once at the first prompt build of a run and injects both into the prompt's "memory" section, replacing the dead `memory_snippets: Vec::new()` in loop_support. A per-run OnceCell caches the fetch; subsequent model steps reuse it. - Native `retrieve_context` scopes by `invocation.scope.thread_id`: Some(T) → only that thread's `threads/<T>/` subtree (short-term); None → the user's general memory, excluding `threads/*` (long-term). The lanes are disjoint, so the host concatenates them (short-term first) under the existing 4 KiB admission budget. - Graceful degradation throughout: a memory failure degrades the lane to empty and never breaks a turn. Recording (after each turn): - New low-level `MemoryService::record_interaction(invocation, { messages, run_id, metadata })` — the mem0 `add` data shape (`user_id`/`agent_id`/ `thread_id` ride the invocation scope). A default no-op trait impl lets each provider opt in: the host passes the DATA and the provider decides what to do with it (store verbatim, run LLM extraction, or nothing). Implements the reserved `memory.interaction.record.v1` vocabulary. - The native provider stores the full turn history under `threads/<thread_id>/`. - A host `AfterTurnMemoryRecorder` fires at the run-end seam (`turn_run_executor::apply_exit`, gated on `Completed`), reads the exchange from the thread transcript with the owner-rewritten scope, and hands it down. Post-terminal and best-effort: every error is `debug!`-logged and never fails the already-completed run. Reads and writes resolve on the local-dev runtime path; the production graph wires `None` (deferred, issue #5013 — the same optionality as `user_profile_source`). Tested at unit + caller level across ironclaw_memory{,_native}, host_runtime, loop_support, turns, and reborn (two-lane fetch, prompt rendering, once-per-run cache, native record→retrieve, and a full-turn record through the executor). A full-composition e2e (record in one run → surface in a later run's model request) is called out as a follow-up. Design: docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): address review — full transcript (H1), run-vs-session, idempotency, build break Addresses the adversarial audit + the mem0 data-parity audit + CodeRabbit on #5327. - H1 + parity (transcript): `build_exchange` → `build_transcript` now captures the FULL ordered run transcript — every user/assistant/tool message of the turn, in sequence order, every finalized assistant including the FINAL answer (fixes recording the first/intermediate assistant on multi-step runs), each tagged with its actor `name`. This is the data mem0's `add` receives. - Run-vs-session (parity): rename `MemoryServiceRecordRequest.run_id` → `turn_run_id` (per-turn provenance). mem0's session id maps to `scope.thread_id` (the conversation), NOT this field — documented on the contract + recorder so a mem0 provider can't mis-map (which would write under the turn id but read under the session id → silent short-term-recall miss). - Idempotency (CodeRabbit): native `record_interaction` writes the transcript to a per-run file `threads/<thread_id>/<turn_run_id>.md` with `append: false` (overwrite), so a scheduler re-run of an already-Completed run can't duplicate the exchange or grow an unbounded `log.md`. - Parity: add `MemoryInteractionMessage.name` (mem0 message name → per-memory `actor_id`); populate `metadata` with `{turn_run_id, correlation_id}` provenance. - Build break (CodeRabbit, critical): add the missing `after_turn_memory_writer` field to the root crate's `tests/support/reborn/harness.rs` (was `E0063`). The gate now compiles the root crate's reborn tests. - Audit M1: the per-run memory `OnceCell` no longer freezes to empty when the first prompt build has no user message — it seeds only once a real request exists. - Audit L3: `// arch-exempt: optional_arc` annotations on the new optional fields; corrected the misleading "mirrors user_profile_source" comments. - Docs: long-term lane is full-tuple `(tenant,user,agent,project)` scoped; the `threads/` reservation is advisory (L1); fetch-once-per-run has no mid-run invalidation in v1 (Q6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): address re-review — lane starvation, threads/ write-reject, no transcript trim, render ordering, fetch-once-per-run CodeRabbit: - retrieve_context: over-fetch BEFORE the short-term thread-lane filter, then truncate, so general hits in the global top-N can no longer starve the thread-scoped lane (TDD). - write: reject the reserved `threads/` namespace (fail loud). record_interaction still writes there via a new private write_reserved_document bypass (TDD). Un-defers audit L1. - build_transcript: stop trimming message content — filter blank-only rows but pass content through verbatim ("LLM data is never deleted") (TDD). - InstructionBundleBuilder::build: preserve the host's short-term-first memory order; drop the by-ref re-sort that scrambled lane priority at the render boundary (TDD; updates the now-obsolete model_content-tiebreak test). - loop_driver_host: caller-level test through build_default_planned_runtime that the after_turn_memory_writer is plumbed into the executor (not just the builder shortcut). - threadless record_interaction test: supply a real turn_run_id to isolate the no-thread branch; soften the after-turn writer contract comments (runtime.rs + after_turn_memory.rs) to match the full-transcript behavior. Gemini: - load_memory_snippets_once: get_or_init + cache empty on failure = true fetch-once-per-run, no retry-storm on a slow/down memory service (M1 early return preserved). - memory_context: share one correlation_id across both retrieval lanes. Also: strengthen the aggregate-budget test with a non-empty assertion (so the all-short-term check isn't vacuous), and align the design doc with shipped behavior (no mid-run invalidation; full transcript; threads/ enforced). cargo fmt + clippy clean (zero warnings); per-crate tests green: memory_native, host_runtime, turns, loop_support, reborn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): address re-review round 3 — non-blank query, reserved-write guard, bounded recorder, silent-ok, test race CodeRabbit (round 3): - latest_user_message_text: return the latest NON-BLANK user message so a blank trailing user turn doesn't drop proactive memory for the run (+ unit test). - write_reserved_document: enforce the `threads/` bypass — reject any non-threads resolved path so the public `write` guard can't be circumvented via this helper. - after_turn_memory: annotate the two intentional post-terminal fallbacks with `// silent-ok:` per the fail-loud convention. - turn_run_executor: bound the inline after-turn recorder await with a 30s timeout so a slow/hung provider can't occupy the scheduler worker. - loop_driver_host: move the sibling test's scheduler shutdown after the memory read/asserts so it can't race the recorder. - reborn_composition: soften the after-turn `[user, assistant]` contract comment to match full-transcript behavior (same fix as runtime.rs). fmt + clippy clean; tests green (loop_support 336, memory_native 20, reborn 258 + loop_driver_host 109). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): address re-review round 4 — fetch memory lanes concurrently retrieve_context awaited the short-term and long-term lanes sequentially; they are independent (share only the Copy correlation id), so fetch them with tokio::join! to cut added run-start prompt-path latency, keeping short-term-first concatenation. (CodeRabbit, trivial.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(memory): self-contained read_long_term/read_thread on MemoryService Fold the prompt-context lane orchestration into the memory service (review feedback): the host no longer knows about "lanes." Two new provider-agnostic default trait methods own both the lane scoping AND the per-snippet safety: - read_long_term: clears the thread sub-scope (general memory), then sanitizes each candidate (scope-check + control-strip + size-cap + untrusted envelope). - read_thread: keeps the thread sub-scope (this conversation), same safety. Providers implement only the raw retrieve_context; they inherit the safe lane reads, so no provider can return unsafe memory context. ironclaw_memory gains a leaf dependency on ironclaw_prompt_envelope (no cycle); the sanitize helpers + scope check move there with their unit tests. The host ProductionMemoryPromptContextService collapses to two scoped reads + a trivial map to LoopContextSnippet (net -322 lines). The map keeps only the two loop-layer steps that can't move down without a dependency cycle: the model-visible reference and the loop's prompt-content denylist drop-filter (LoopSafeSummary), a prompt-layer policy applied to all model context. Deleted: the MemoryLane enum, retrieve_lane, admit_memory_context_snippet, ExpectedSnippetScope, and the host-side sanitizer. Behavior change: prompt order is now long-term then short-term (this conversation nearest the current message, per the mem0 on_run_start shape), which also flips which lane wins under the shared 4 KiB budget (was short-term-first). read_profile is unchanged: profile_read already returns the raw doc and the host parses it into the loop-shaped UserProfileContext — the correct provider-neutral split, not the lane confusion this refactor targets. fmt + clippy clean; tests green across ironclaw_memory, ironclaw_memory_native, ironclaw_host_runtime; consumers (mem0 / reborn / reborn_composition) compile. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(memory): address host lifecycle review feedback --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(memory): model memory as a v3 [memory] adapter with native + mem0 backends Migrate the userland memory extension to Extension Manifest v3 and retire the v2 capability-profile machinery, replacing it with a first-class [memory] adapter surface (issue #3537 / #5264). - v3 manifests for both backends: `ironclaw.memory` (native, filesystem-backed) and `mem0.local.memory` (mem0 OSS), each declaring `[memory] operations = ["document_store","context_retrieval","interaction_log"]`. - New `MemoryDescriptor` / `MemoryOperationKind` in `ironclaw_host_api::memory`. The `MemoryService` trait is the implementation-agnostic adapter the agent loop and the `ironclaw.memory.{read,write,search,tree}` tools talk to; the tool ids the model sees never change when the backend does. - Native + mem0 are interchangeable backends behind the adapter, selected by a compile-time `[memory]` binding (`memory-mem0` feature; fail-closed native default; no runtime swap). `MemoryBindingPolicy` collapses to a single `MemoryProviderBinding`; config `MemorySection`: `profile_bindings` -> `provider`. - Memory rides the always-on first-party lane (not the installable catalog), so it never appears as an installed extension. - Remove the now-dead capability-profile vocabulary: `CapabilityProfileId` / `CapabilityProfileContract` / `CapabilityProfileOperationContract`, the `ironclaw_capabilities` conformance harness, and the capability `implements` field (zero production readers; the adapter trait is the contract now). Keep `CapabilityProfileSchemaRef` (load-bearing for every capability's schema refs). Fix a group_memory regression the change surfaced: v3's `with_dispatch_effect` adds `DispatchCapability` to every memory tool (matching every other first-party tool -- echo/http/shell/...), so the test-harness trust ceiling must grant it too or every memory dispatch is `PolicyDenied`. Production ceilings already did. Verified: cargo fmt; workspace clippy --all-targets --all-features -D warnings (exit 0); architecture boundaries + manifest-reparse gate; group_memory (13); mem0<->native swap (5); host_runtime memory (382); reborn_composition (1402); default + all-features compile lanes. Pre-existing (base-confirmed, unrelated): factory local_dev_memory (2, FilesystemDenied) + sandbox_process (3, no Docker). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZ9i53L6mBFnnsvtPSrSzc * fix(ci): expect memory-mem0 in composition feature-flags self-test The memory PR added the off-by-default `memory-mem0` feature to `ironclaw_reborn_composition` and updated package-feature-flags.sh to emit `--features test-support,memory-mem0` for it, but the self-test's pinned expectation still read `--features test-support`. Update the assertion to match the script it guards. The change IS the test update, so no separate regression test applies. [skip-regression-check] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZ9i53L6mBFnnsvtPSrSzc * test(host-runtime): memory tool descriptors carry DispatchCapability post-merge main's manifest reparse gate + `with_dispatch_effect` apply DispatchCapability to every v3 first-party tool descriptor (consistent with builtin http et al.), so native memory's descriptors are now [DispatchCapability, ReadFilesystem, ...]. Update the effect assertions to match, and drop the memory ids from the builtin origin-gate-matrix spot-checks since memory moved to the standalone ironclaw.memory package. [skip-regression-check] Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BZ9i53L6mBFnnsvtPSrSzc * fix(memory): declare origin-gate matrices + registry-lane allowlist for the ironclaw.memory package Two masked layers made every ironclaw.memory.* dispatch fail in any composition with an extension host installed: 1. The memory tools moved out of the builtin package (which stamps OriginGateMatrix::builtin_loop_run_seed on every Rust-built descriptor) into the hand-authored memory_native v3 manifest, which declared no origin_gate_matrix. The S4 authorize fold fail-closes a missing matrix to Forbidden for every origin-stamped invocation, so model dispatch died with Authorization (PolicyDenied). Declare the behavior-preserving matrices in the manifest: read/search/tree stay Ungated for LoopRun via the reviewed allowlist (renamed from the retired builtin.memory_* ids, count unchanged), write stays gated_unless_granted, product/automation stay forbidden. 2. Once authorized, dispatch still failed UnknownCapability: the registry-lane provider allowlist (applied when the extension-host snapshot resolver cuts over) listed only the builtin provider, but the always-on ironclaw.memory package resolves through the same registry lane and is never published in the extension-host snapshot. Add the native memory provider to the allowlist. Regression coverage: factory::tests::local_dev_memory_* (caller-tier, red before this fix) now pass; native_memory_package_declares_behavior_neutral_origin_gate_matrix pins the descriptor-tier matrix contract that was dropped when memory left the builtin package; the origin-gate ratchet's TOML scan now also walks crates/ironclaw_host_runtime/assets so a host-bundled manifest can never again ship without a matrix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1q1QYdRY3XRg6RnYLZdNw * fix(ci): give QA smoke binaries the documented RUST_MIN_STACK headroom The reborn_qa_smoke_scenarios_e2e binary builds a full Reborn runtime and drives whole turns on the libtest current-thread stack; with this branch's memory pipeline additions its combined debug async frames overflow the 8 MiB default test-thread stack (measured need ~10 MiB; CI aborts with "fatal runtime error: stack overflow" in root tests (1), locally reproducible on qa_installing_bundled_extensions_exposes_complete_model_surface_e2e). Set RUST_MIN_STACK=64 MiB on the root-tests job — the same value and pathology reborn_qa_recorded_behavior.rs already documents for its recorder ("builds two runtimes plus a live turn, whose combined debug async frame overflows the default test-thread stack") — and document the requirement on the smoke binary itself. The full 26-test binary passes under this value. [skip-regression-check] CI environment headroom for a debug-profile stack exhaustion; the binary's existing tests are the regression surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1q1QYdRY3XRg6RnYLZdNw * test(reborn): finish builtin.memory_* -> ironclaw.memory.* rename in composition tests + re-bless surface hash Two stragglers from the memory-package rename that only fail in CI lanes wider than --lib: webui_v2_e2e asserted builtin.memory_write is visible in the local-dev capability surface (composition-core bucket), and the golden payload snapshots pinned the pre-origin-gate-matrix surface sha256 (integration coverage lane 3). Rename the ids and re-bless; the snapshot diff is exactly the surface-hash token — the capability list, names, and descriptions are unchanged. Also rename the opaque id in the local_dev result-staging test for consistency. [skip-regression-check] test-only rename + snapshot re-bless; the renamed assertions are themselves the regression coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1q1QYdRY3XRg6RnYLZdNw * test(reborn): align channel-connection projection with the #6618 retain-generated-code contract Main's #6618 refined the extension_search sanitizer to RETAIN a generated-code channel's setup guidance (blanking only the static failure copy) and pinned that in a unit test — but left the integration test asserting the old strip-everything contract. The contradiction was invisible on main because main's tip cannot compile the integration-test closure at all: #6618 renamed the RebornRuntime field to `_channel_host_assembly` but missed the test-support-gated accessor (`active_channel_preference_codec_ids_for_test`), so every integration-tier suite fails at compile and all five coverage lanes are red on main. This branch already carries the accessor fix from the catch-up merge; this commit carries the test-contract fix: telegram's web_generated_code guidance must remain model-visible with "IronClaw pairing panel" instructions and a blanked error_message, mirroring model_visible_extension_search_projects_generated_code_without_ui_failure_copy. [skip-regression-check] test-only contract alignment; the rewritten assertions are the regression coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1q1QYdRY3XRg6RnYLZdNw * review(memory): apply CodeRabbit/Gemini findings across the memory surface Verified each open review thread against current code; fixes for the still-valid ones: - Harness trust parity (security): the integration harness granted the ironclaw.memory provider the FULL builtin effect set (Network/SpawnProcess/ExecuteCode/...); production grants only dispatch + filesystem. Narrow core_builtin + qa_smoke profiles to the production ceiling so harness runs can't mask authority-ceiling denials production would enforce. - mem0 retrieve_context now filters the kind=profile record out of context snippets (profile JSON must not enter the prompt as a memory snippet; profile state has its own read path) + regression case. - Caller-level proof of the retrieve-before lane: drive the REAL LoopContextPort::load_loop_context twice with a recording MemoryPromptContextService — asserts the query is the latest user message, snippets surface on the bundle, and the fetch happens once per run (cache reuse). - [memory] manifest validation regression tests: non-first-party runtime, empty operations, and missing document_store all fail closed (+ a parsing baseline for the provider-only shape). - surface.rs: mirrored fail-closed test — a native-memory descriptor without an input schema ref is rejected like a builtin one. - Origin-gate ratchet now asserts BOTH host-bundled memory manifests are actually scanned (a moved manifest can no longer silently drop out). - document-read input schema mirrors write/tree's stricter path not-pattern (blank/absolute/traversal/backslash); tree output schema types its items as path strings. Verified-invalid threads (no change): the "duplicate [[tools]] headers" critical is a diff-context misread (one header per tool; the package parse is pinned by tests); the memory_provider_factory CapabilityProfileId silent-drop refers to code removed with the capability-profile vocabulary retirement. Deferred: mem0 unbounded list pagination (off-by-default provider; needs a mem0 API paging design). [skip-regression-check] review-driven test hardening; each behavioral fix above carries its regression case in the same commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1q1QYdRY3XRg6RnYLZdNw --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Robert Yan <46699230+think-in-universe@users.noreply.github.com>
Summary
Implements the mem0 host-managed memory flow on top of #5205 (
reborn/memory-lift-followups). Memory now reaches the model proactively (the loop previously hardcodedmemory_snippets: Vec::new()), and each completed turn is recorded back so the short-term lane is populated next time.The retrieval + safety logic is self-contained in the memory service: two provider-agnostic methods own both lane-scoping and per-snippet sanitization, so no provider can return unsafe prompt context and the host is a thin adapter.
Architecture
Retrieval — on
MemoryService(provider-agnostic defaults)read_long_termclears the thread sub-scope (without_thread_and_mission) → the user's general memory;read_threadkeeps it → the active thread's scratch. Both are default trait methods that own the per-snippet safety: scope-check (drop cross-tenant/user/agent/project), control-char strip, size-cap, and the untrusted-memory envelope. Providers implement only the rawretrieve_context; they inherit safe lane reads, so a swapped/third-party provider cannot return unsafe context. (ironclaw_memorygains a leaf dep onironclaw_prompt_envelope; no cycle.)retrieve_contextdoes the FTS + thethreads/<thread_id>/lane filter, over-fetching before the filter so general hits in the global top-N can't starve the thread lane.ProductionMemoryPromptContextServiceis now a thin adapter: it makes the two scoped reads concurrently (tokio::join!), concatenates long-term then short-term (this conversation nearest the current message), and maps each safe snippet onto aLoopContextSnippet. The only logic left host-side is what depends on loop-layer types: the model-visiblememory-snippet:*reference and the loop's prompt-content denylist (LoopSafeSummary) applied as a drop-filter, plus the combined 512 B/snippet + 4 KiB-aggregate budget.OnceCell); the query is the latest non-blank user message; a retrieval failure degrades to empty and is cached so a down/slow provider isn't re-hit every model step. Memory never breaks a turn.Recording —
record_interaction(the mem0addseam; "provider decides")MemoryService::record_interaction(invocation, { messages, turn_run_id, metadata })with a default no-op so providers opt in. The host passes the DATA — the full ordered turn transcript (every user / finalized-assistant / tool message),turn_run_idas provenance (not the mem0 session id — that'sscope.thread_id), and opaque metadata — and the provider decides verbatim-store vs extraction vs nothing.threads/<thread_id>/<turn_run_id>.mdwith overwrite semantics (idempotent — a scheduler re-run of aCompletedrun overwrites, no duplication/unbounded growth). Thethreads/namespace is reserved and write-rejected for tool/caller writes (a stray write there would be a silent retrieval black hole); only the recorder writes there, via a private bypass.AfterTurnMemoryRecorderfires atturn_run_executor::apply_exit, gated onCompleted, reading the exchange with the owner-rewritten thread scope. It's timeout-bounded so a slow/hung provider can't occupy the scheduler worker, and post-terminal best-effort — every error isdebug!-logged and never fails the already-completed run.Safety boundary
memory_search/read/write/tree) and the profile read are separate surfaces with their own paths and are unchanged.Testing
Unit + caller-level across
ironclaw_memory{,_native},host_runtime,loop_support,turns,reborn:read_long_term/read_threadlane split + envelope (native facade); per-snippet sanitizer (control-strip / truncate / envelope) + scope-drop unit tests;threads/write-rejection + reserved-namespace bypass; short-term over-fetch survives the lane filter; idempotent per-run record.memory_snippets.build_default_planned_runtimeso the writer wiring can't silently regress.cargo fmt+cargo clippyclean on the touched crates; downstream crates (mem0 / reborn / reborn_composition) compile; root test harness compiles. (3sandbox_processtests fail locally only without Docker.)Known items / follow-ups
None— reads + writes resolve on the local-dev runtime path only; the production graph degrades to no memory (issue Reborn: wire production-graph composition for optional context sources (identity + profile) #5013, same asuser_profile_source).read_profileis intentionally left asprofile_read(raw doc → host parses into the loop-shapedUserProfileContext); folding it into a parsedread_profilewould require moving the timezone/locale validation down and a memory-owned profile type.on_run_enddurable summary) intentionally deferred.Design doc:
docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md🤖 Generated with Claude Code