Skip to content

feat(memory): host-managed memory lifecycle — two-lane retrieval + after-turn record (mem0 flow) - #5327

Merged
BenKurrek merged 13 commits into
stack/memory/01-userland-extensionfrom
reborn/memory-lifecycle
Jul 20, 2026
Merged

BenKurrek merged 13 commits into
stack/memory/01-userland-extensionfrom
reborn/memory-lifecycle

Conversation

@BenKurrek

@BenKurrek BenKurrek commented Jun 26, 2026 •

Copy link
Copy Markdown
Collaborator

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 hardcoded memory_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.

on_run_start:   long_term  = MemoryService::read_long_term(user scope)   // clears thread
                short_term = MemoryService::read_thread(thread scope)    // keeps thread
before_model:   prompt = system + long_term + short_term + conversation
after_turn:     MemoryService::record_interaction(full transcript, turn_run_id, metadata)

Architecture

Retrieval — on MemoryService (provider-agnostic defaults)

  • read_long_term clears the thread sub-scope (without_thread_and_mission) → the user's general memory; read_thread keeps 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 raw retrieve_context; they inherit safe lane reads, so a swapped/third-party provider cannot return unsafe context. (ironclaw_memory gains a leaf dep on ironclaw_prompt_envelope; no cycle.)
  • Native retrieve_context does the FTS + the threads/<thread_id>/ lane filter, over-fetching before the filter so general hits in the global top-N can't starve the thread lane.
  • ProductionMemoryPromptContextService is 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 a LoopContextSnippet. The only logic left host-side is what depends on loop-layer types: the model-visible memory-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.
  • Fetched once per run (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 mem0 add seam; "provider decides")

  • Low-level 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_id as provenance (not the mem0 session id — that's scope.thread_id), and opaque metadata — and the provider decides verbatim-store vs extraction vs nothing.
  • The native provider writes the transcript to a per-run file threads/<thread_id>/<turn_run_id>.md with overwrite semantics (idempotent — a scheduler re-run of a Completed run overwrites, no duplication/unbounded growth). The threads/ 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.
  • AfterTurnMemoryRecorder fires at turn_run_executor::apply_exit, gated on Completed, 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 is debug!-logged and never fails the already-completed run.

Safety boundary

  • Prompt-context sanitization is provider-agnostic and always-on, living on the read methods. The model-visible output and drop-logic were independently audited as a no-regression move from the previous host-side implementation (envelope, denylist, reference hash, scope-check, and budgets all byte-identical).
  • The agent's memory tools (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_thread lane 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.
  • Host two-lane fetch (long-term-first) + per-lane degradation + combined budget; once-per-run cache; latest-non-blank-user query; render-boundary lane-ordering; prompt rendering from memory_snippets.
  • After-turn: full-ordered-transcript capture (incl. final assistant), verbatim content (no trim), threadless/no-run-id no-ops, a full-turn record through the executor, and a caller-level test driving build_default_planned_runtime so the writer wiring can't silently regress.
  • cargo fmt + cargo clippy clean on the touched crates; downstream crates (mem0 / reborn / reborn_composition) compile; root test harness compiles. (3 sandbox_process tests fail locally only without Docker.)

Known items / follow-ups

  • Production graph wires 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 as user_profile_source).
  • Full-composition e2e (record in one run → surface in a later run's model request) not yet written; the individual links are unit + caller tested.
  • Budget priority: the combined block is now long-term-first, so under aggregate-budget pressure the long-term lane wins (was short-term-first before the consolidation). Per-lane sub-budgets are a later refinement.
  • read_profile is intentionally left as profile_read (raw doc → host parses into the loop-shaped UserProfileContext); folding it into a parsed read_profile would require moving the timezone/locale validation down and a memory-owned profile type.
  • Phase 3 (on_run_end durable summary) intentionally deferred.

Design doc: docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md

🤖 Generated with Claude Code

…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>
@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • staging
  • reborn-integration

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: adc13583-826f-46fd-bb4e-e23e55f159d5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This 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.

Changes

Reborn memory lifecycle

Layer / File(s) Summary
Memory contract and native store
crates/ironclaw_memory/..., crates/ironclaw_memory_native/..., crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs, crates/ironclaw_memory_native/tests/memory_service_facade.rs
Adds interaction-recording types and record_interaction, splits memory reads into read_long_term/read_thread, and updates native storage for thread-scoped retrieval, reserved threads/ writes, and per-run transcript persistence.
Prompt cache and memory section
crates/ironclaw_host_runtime/..., crates/ironclaw_loop_support/..., crates/ironclaw_turns/..., crates/ironclaw_reborn/tests/llm_gateway.rs
Switches host memory loading to concurrent lane reads with loop denylist admission, caches prompt memory once per run from the latest user message, and preserves host insertion order when rendering memory snippets.
After-turn recorder
crates/ironclaw_reborn/src/after_turn_memory.rs, crates/ironclaw_reborn/src/turn_run_executor.rs, crates/ironclaw_reborn/tests/loop_driver_host.rs
Adds post-completion transcript building and best-effort recording under timeout, then verifies the recorded per-run document is written and discoverable.
Runtime wiring and defaults
crates/ironclaw_reborn/src/loop_driver_host.rs, crates/ironclaw_reborn/src/runtime.rs, crates/ironclaw_reborn_composition/src/runtime.rs, tests/integration/*, tests/support/*, crates/*/tests/*
Adds optional memory wiring fields to planned runtime parts, resolves them from composition, and updates parity and harness tests to account for the new runtime shape.
Lifecycle design spec
docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md
Adds the memory lifecycle design/spec and its implementation progress log.

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
Loading
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
Loading

Possibly related issues

Possibly related PRs

  • nearai/ironclaw#5163: Prior memory-service facade work this PR extends in both host and native paths.
  • nearai/ironclaw#5642: Introduced the wiring-parity surface that this PR updates for the new runtime fields.
  • nearai/ironclaw#4795: Also touches InstructionBundleBuilder::build, which this PR changes for memory ordering.

Suggested reviewers: serrrfirat, ilblackdragon

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning Core content is present, but the required template sections for change type, linked issue, validation, security, and runtime checklist are missing. Add the missing template sections and fill in the approved issue link, validation checklist, security impact, trust-boundary, database, blast radius, rollback, and review-track fields.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Conventional-commits title matches the PR’s memory lifecycle, two-lane retrieval, and after-turn recording changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


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.

❤️ Share

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread crates/ironclaw_loop_support/src/lib.rs Outdated
Comment thread crates/ironclaw_reborn/src/after_turn_memory.rs Outdated
Comment thread crates/ironclaw_host_runtime/src/memory_context.rs Outdated
@github-actions github-actions Bot added scope: docs Documentation scope: dependencies Dependency updates size: XL 500+ changed lines risk: low Changes to docs, tests, or low-risk modules contributor: experienced 6-19 merged PRs labels Jun 26, 2026
@BenKurrek

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 | 🔴 Critical

Missing required field after_turn_memory_writer in this DefaultPlannedRuntimeParts literal.

The struct DefaultPlannedRuntimeParts requires an after_turn_memory_writer field, which is present in other initializations within the codebase but omitted in the literal at tests/support/reborn/harness.rs (lines 861-903). Adding memory_context_service: None without including after_turn_memory_writer results 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 | 🔵 Trivial

Optional: 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_text skips memory if the most recent user message is blank, even when an earlier user message has text.

find returns the last User message and the trailing .filter only inspects that one. If the latest user turn is whitespace-only (e.g. an attachment-only message), the query resolves to None and 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 win

Consider 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 an Option<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

📥 Commits

Reviewing files that changed from the base of the PR and between f8377b9 and ff95137.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (24)
  • crates/ironclaw_host_runtime/src/memory_context.rs
  • crates/ironclaw_host_runtime/tests/memory_prompt_context.rs
  • crates/ironclaw_loop_support/src/lib.rs
  • crates/ironclaw_memory/Cargo.toml
  • crates/ironclaw_memory/src/lib.rs
  • crates/ironclaw_memory/src/service.rs
  • crates/ironclaw_memory_native/src/lib.rs
  • crates/ironclaw_memory_native/src/service.rs
  • crates/ironclaw_memory_native/tests/memory_service_facade.rs
  • crates/ironclaw_product_workflow/tests/inbound_turn_contract.rs
  • crates/ironclaw_product_workflow/tests/support/planned_agent_loop.rs
  • crates/ironclaw_reborn/Cargo.toml
  • crates/ironclaw_reborn/src/after_turn_memory.rs
  • crates/ironclaw_reborn/src/lib.rs
  • crates/ironclaw_reborn/src/loop_driver_host.rs
  • crates/ironclaw_reborn/src/runtime.rs
  • crates/ironclaw_reborn/src/turn_run_executor.rs
  • crates/ironclaw_reborn/tests/llm_gateway.rs
  • crates/ironclaw_reborn/tests/loop_driver_host.rs
  • crates/ironclaw_reborn_composition/src/runtime.rs
  • crates/ironclaw_reborn_composition/tests/product_live_adapters.rs
  • crates/ironclaw_turns/tests/agent_loop_host_contract.rs
  • docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md
  • tests/support/reborn/harness.rs

Comment thread crates/ironclaw_memory_native/src/service.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>
@BenKurrek

Copy link
Copy Markdown
Collaborator Author

Thanks @coderabbitai — both findings are addressed in 41a2574b8, alongside an independent adversarial audit and a mem0 data-parity audit run in parallel.

Your findings

  • Idempotency on run_id — record_interaction now writes a per-run file threads/<thread_id>/<turn_run_id>.md with append: false (overwrite). A scheduler re-run of a Completed run overwrites idempotently (no duplicate snippets), and growth is bounded to one file per run (no unbounded log.md).
  • Missing after_turn_memory_writer (E0063) — added to tests/support/reborn/harness.rs. The root crate's reborn tests now compile (verified locally with cargo test -p ironclaw --no-run --test reborn_agent_scope_isolation_parity).

Also fixed this pass (audit + mem0 data parity)

  • Multi-step runs recorded the first assistant, not the final answer → build_exchange → build_transcript now captures the full ordered run transcript (every user/assistant/tool message; every finalized assistant including the final one).
  • run_id-vs-session conflation → renamed MemoryServiceRecordRequest.run_id → turn_run_id (per-turn provenance); mem0's session id maps to scope.thread_id (documented on the contract so a provider can't mis-map and silently miss short-term recall).
  • Added MemoryInteractionMessage.name (per-message actor → mem0 actor_id) + provenance metadata; fixed a per-run memory cache that could freeze to empty when the first prompt build had no user message; added arch-exempt: optional_arc annotations; doc corrections (long-term full-tuple scope, the threads/ advisory reservation, the v1 no-mid-run-invalidation note).

Requesting a full re-review.

@BenKurrek

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

@BenKurrek Acknowledged. I’ll re-run a full review on #5327 with the updated contract in mind — especially the per-run overwrite/idempotency path, thread-vs-user scope separation, and the turn_run_id provenance rename.


✏️ Learnings added
Learnt from: BenKurrek
URL: https://github.com/nearai/ironclaw/pull/5327

Timestamp: 2026-06-26T14:17:32.808Z
Learning: In this repository's mem0 memory recording flow, `MemoryServiceRecordRequest.turn_run_id` is the per-turn provenance identifier; mem0 session identity maps to `MemoryInvocation.scope.thread_id`, not to `turn_run_id`.

Learnt from: BenKurrek
URL: https://github.com/nearai/ironclaw/pull/5327

Timestamp: 2026-06-26T14:17:32.809Z
Learning: For the native memory provider's after-turn recording path, `record_interaction` writes one file per completed run at `threads/<thread_id>/<turn_run_id>.md` with overwrite semantics (`append: false`) so scheduler replays of a completed run remain idempotent and do not append to an unbounded shared `log.md`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between f8377b9 and 41a2574.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (24)
  • crates/ironclaw_host_runtime/src/memory_context.rs
  • crates/ironclaw_host_runtime/tests/memory_prompt_context.rs
  • crates/ironclaw_loop_support/src/lib.rs
  • crates/ironclaw_memory/Cargo.toml
  • crates/ironclaw_memory/src/lib.rs
  • crates/ironclaw_memory/src/service.rs
  • crates/ironclaw_memory_native/src/lib.rs
  • crates/ironclaw_memory_native/src/service.rs
  • crates/ironclaw_memory_native/tests/memory_service_facade.rs
  • crates/ironclaw_product_workflow/tests/inbound_turn_contract.rs
  • crates/ironclaw_product_workflow/tests/support/planned_agent_loop.rs
  • crates/ironclaw_reborn/Cargo.toml
  • crates/ironclaw_reborn/src/after_turn_memory.rs
  • crates/ironclaw_reborn/src/lib.rs
  • crates/ironclaw_reborn/src/loop_driver_host.rs
  • crates/ironclaw_reborn/src/runtime.rs
  • crates/ironclaw_reborn/src/turn_run_executor.rs
  • crates/ironclaw_reborn/tests/llm_gateway.rs
  • crates/ironclaw_reborn/tests/loop_driver_host.rs
  • crates/ironclaw_reborn_composition/src/runtime.rs
  • crates/ironclaw_reborn_composition/tests/product_live_adapters.rs
  • crates/ironclaw_turns/tests/agent_loop_host_contract.rs
  • docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md
  • tests/support/reborn/harness.rs

Comment thread crates/ironclaw_host_runtime/tests/memory_prompt_context.rs
Comment thread crates/ironclaw_memory_native/src/service.rs
Comment thread crates/ironclaw_memory_native/src/service.rs
Comment thread crates/ironclaw_memory_native/tests/memory_service_facade.rs
Comment thread crates/ironclaw_reborn/src/after_turn_memory.rs
Comment thread docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

📥 Commits

Reviewing files that changed from the base of the PR and between f8377b9 and 41a2574.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (24)
  • crates/ironclaw_host_runtime/src/memory_context.rs
  • crates/ironclaw_host_runtime/tests/memory_prompt_context.rs
  • crates/ironclaw_loop_support/src/lib.rs
  • crates/ironclaw_memory/Cargo.toml
  • crates/ironclaw_memory/src/lib.rs
  • crates/ironclaw_memory/src/service.rs
  • crates/ironclaw_memory_native/src/lib.rs
  • crates/ironclaw_memory_native/src/service.rs
  • crates/ironclaw_memory_native/tests/memory_service_facade.rs
  • crates/ironclaw_product_workflow/tests/inbound_turn_contract.rs
  • crates/ironclaw_product_workflow/tests/support/planned_agent_loop.rs
  • crates/ironclaw_reborn/Cargo.toml
  • crates/ironclaw_reborn/src/after_turn_memory.rs
  • crates/ironclaw_reborn/src/lib.rs
  • crates/ironclaw_reborn/src/loop_driver_host.rs
  • crates/ironclaw_reborn/src/runtime.rs
  • crates/ironclaw_reborn/src/turn_run_executor.rs
  • crates/ironclaw_reborn/tests/llm_gateway.rs
  • crates/ironclaw_reborn/tests/loop_driver_host.rs
  • crates/ironclaw_reborn_composition/src/runtime.rs
  • crates/ironclaw_reborn_composition/tests/product_live_adapters.rs
  • crates/ironclaw_turns/tests/agent_loop_host_contract.rs
  • docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md
  • tests/support/reborn/harness.rs

Comment thread crates/ironclaw_reborn/src/runtime.rs
Comment thread crates/ironclaw_reborn/tests/loop_driver_host.rs
Comment thread crates/ironclaw_turns/tests/agent_loop_host_contract.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>
@BenKurrek

Copy link
Copy Markdown
Collaborator Author

Re-review round addressed — dfce4cadc

All 10 open review threads (7 CodeRabbit + 3 Gemini) are fixed and resolved. Behavioral changes are TDD'd (red test shown to fail first, then made green).

CodeRabbit

  • Short-term lane starvation (service.rs retrieve_context) — over-fetch (max_snippets*8, min 64) before the scope+lane retains, truncate to max_snippets only after the lane filter. Test: native_short_term_retrieval_over_fetches_before_thread_lane_filter.
  • Reserved threads/ writes (service.rs write) — now rejected (fail loud); record_interaction writes there via a private write_reserved_document bypass. Test: native_write_rejects_reserved_thread_namespace. (Un-defers audit L1.)
  • No transcript trim (after_turn_memory.rs build_transcript) — filter blank-only rows, pass content verbatim. Test: build_transcript_preserves_content_verbatim_and_filters_blanks.
  • Render-boundary lane priority (instruction_bundle.rs) — dropped the by-ref re-sort so the host's short-term-first order survives. Test: instruction_bundle_renders_memory_section_from_memory_snippets (+ repurposed the old model_content-tiebreak test).
  • Caller-level wiring test — build_default_planned_runtime_wires_after_turn_memory_writer drives the real composition factory and asserts the per-run doc is written.
  • Test rigor + comment — threadless record_interaction test now sets a real turn_run_id; softened the after-turn writer contract comments to match full-transcript behavior.

Gemini

  • get_or_try_init retry-storm (loop_support load_memory_snippets_once) — switched to get_or_init caching empty on failure = true fetch-once-per-run (M1 early-return preserved). Behavior change — a transient first-fetch failure now degrades memory to empty for the rest of that run rather than retrying each model step.
  • Final assistant via .find() — already addressed on this branch: build_transcript records the full ordered transcript (every finalized assistant + the final answer), so it no longer depends on .find(). Resolved as addressed.
  • Shared correlation id — both retrieval lanes now share one correlation_id.

Also: strengthened the aggregate-budget test (non-empty assertion so the all-short-term check isn't vacuous); aligned the design doc with shipped behavior (no mid-run invalidation; full transcript; threads/ enforced).

cargo fmt + clippy clean (zero warnings); per-crate tests green across ironclaw_memory_native, ironclaw_host_runtime, ironclaw_turns, ironclaw_loop_support, ironclaw_reborn.

@BenKurrek

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (2)
crates/ironclaw_reborn_composition/src/runtime.rs (1)

3117-3122: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Soften the after-turn recording contract here too.

Line 3120 still says the executor records a strict [user, assistant] exchange, but AfterTurnMemoryRecorder::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 in crates/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 win

Collapse this spec onto the shipped memory contract.

These sections still describe the superseded design: long_term before short_term, thread_id metadata/filter-based retrieval, after-turn writes feeding both lanes, and threads/<thread_id>/log.md. The implemented contract in this PR is short-term-first ordering, scope-based lane selection, and per-run threads/<thread_id>/<turn_run_id>.md that feeds only the short-term lane. As per coding guidelines, **/*.md: "After a refactor that relocates or renames code, grep for .md and CLAUDE.md references 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

📥 Commits

Reviewing files that changed from the base of the PR and between f8377b9 and dfce4ca.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (25)
  • crates/ironclaw_host_runtime/src/memory_context.rs
  • crates/ironclaw_host_runtime/tests/memory_prompt_context.rs
  • crates/ironclaw_loop_support/src/lib.rs
  • crates/ironclaw_memory/Cargo.toml
  • crates/ironclaw_memory/src/lib.rs
  • crates/ironclaw_memory/src/service.rs
  • crates/ironclaw_memory_native/src/lib.rs
  • crates/ironclaw_memory_native/src/service.rs
  • crates/ironclaw_memory_native/tests/memory_service_facade.rs
  • crates/ironclaw_product_workflow/tests/inbound_turn_contract.rs
  • crates/ironclaw_product_workflow/tests/support/planned_agent_loop.rs
  • crates/ironclaw_reborn/Cargo.toml
  • crates/ironclaw_reborn/src/after_turn_memory.rs
  • crates/ironclaw_reborn/src/lib.rs
  • crates/ironclaw_reborn/src/loop_driver_host.rs
  • crates/ironclaw_reborn/src/runtime.rs
  • crates/ironclaw_reborn/src/turn_run_executor.rs
  • crates/ironclaw_reborn/tests/llm_gateway.rs
  • crates/ironclaw_reborn/tests/loop_driver_host.rs
  • crates/ironclaw_reborn_composition/src/runtime.rs
  • crates/ironclaw_reborn_composition/tests/product_live_adapters.rs
  • crates/ironclaw_turns/src/run_profile/instruction_bundle.rs
  • crates/ironclaw_turns/tests/agent_loop_host_contract.rs
  • docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md
  • tests/support/reborn/harness.rs

Comment thread crates/ironclaw_loop_support/src/lib.rs Outdated
Comment thread crates/ironclaw_memory_native/src/service.rs
Comment thread crates/ironclaw_reborn/src/after_turn_memory.rs
Comment thread crates/ironclaw_reborn/src/turn_run_executor.rs Outdated
Comment thread crates/ironclaw_reborn/tests/loop_driver_host.rs Outdated
BenKurrek and others added 2 commits June 26, 2026 12:01
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>
@BenKurrek

Copy link
Copy Markdown
Collaborator Author

Round 3 addressed + base merged — fb7447762

Merged reborn/memory-lift-followups (#5205) into the branch — resolved the single crates/ironclaw_memory/src/service.rs conflict by combining both independently-added mod tests blocks. Then fixed all 5 new findings + the duplicate from the last full review:

  • Latest non-blank user message (loop_support) — latest_user_message_text uses find_map to skip a blank trailing user turn, so proactive memory isn't dropped when an earlier turn has content (+ unit test).
  • Reserved-write guard (memory_native) — write_reserved_document now rejects any non-threads/ resolved path, enforcing the bypass contract so the public write guard can't be circumvented.
  • silent-ok annotations (after_turn_memory) — both post-terminal fallbacks (history read + provider write) annotated per the fail-loud convention.
  • Bounded recorder (turn_run_executor) — the inline after-turn await is wrapped in a 30s timeout, so a slow/hung provider can't occupy the scheduler worker.
  • Test race (loop_driver_host) — the sibling test now shuts the scheduler down after the memory read/assertions.
  • Duplicate (reborn_composition) — softened the [user, assistant] contract comment to match full-transcript behavior (same fix as runtime.rs).

All round-3 threads resolved. fmt + clippy clean; tests green (loop_support 336, memory_native 20, reborn 258 + loop_driver_host 109); full workspace compiles post-merge.

@BenKurrek

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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_store is now resolved three times over the same filesystem.

user_profile_source (Line 3097), memory_context_service (Line 3120), and after_turn_memory_writer (Line 3139) each independently call local_runtime.memory_service_resolver.resolve_document_store(Arc::clone(&extension_filesystem), None), building three separate provider instances for one runtime. Resolve once into a local Option<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 win

Spec 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-run threads/<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

📥 Commits

Reviewing files that changed from the base of the PR and between 502f676 and fb74477.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (25)
  • crates/ironclaw_host_runtime/src/memory_context.rs
  • crates/ironclaw_host_runtime/tests/memory_prompt_context.rs
  • crates/ironclaw_loop_support/src/lib.rs
  • crates/ironclaw_memory/Cargo.toml
  • crates/ironclaw_memory/src/lib.rs
  • crates/ironclaw_memory/src/service.rs
  • crates/ironclaw_memory_native/src/lib.rs
  • crates/ironclaw_memory_native/src/service.rs
  • crates/ironclaw_memory_native/tests/memory_service_facade.rs
  • crates/ironclaw_product_workflow/tests/inbound_turn_contract.rs
  • crates/ironclaw_product_workflow/tests/support/planned_agent_loop.rs
  • crates/ironclaw_reborn/Cargo.toml
  • crates/ironclaw_reborn/src/after_turn_memory.rs
  • crates/ironclaw_reborn/src/lib.rs
  • crates/ironclaw_reborn/src/loop_driver_host.rs
  • crates/ironclaw_reborn/src/runtime.rs
  • crates/ironclaw_reborn/src/turn_run_executor.rs
  • crates/ironclaw_reborn/tests/llm_gateway.rs
  • crates/ironclaw_reborn/tests/loop_driver_host.rs
  • crates/ironclaw_reborn_composition/src/runtime.rs
  • crates/ironclaw_reborn_composition/tests/product_live_adapters.rs
  • crates/ironclaw_turns/src/run_profile/instruction_bundle.rs
  • crates/ironclaw_turns/tests/agent_loop_host_contract.rs
  • docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md
  • tests/support/reborn/harness.rs

Comment thread crates/ironclaw_host_runtime/src/memory_context.rs Outdated
ironloopai[bot]
ironloopai Bot previously requested changes Jul 6, 2026

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❌ 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

**Inline candidates:** 1

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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloop review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloop review when the fix may affect multiple areas.
  4. Use @ironloop status to 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
crates/ironclaw_reborn_composition/src/runtime.rs (1)

3458-3466: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Still resolving the document-store provider three times — same concern flagged in a prior review round.

memory_service_resolver.resolve_document_store(...) is called separately for user_profile_source, memory_context_service, and after_turn_memory_writer, with identical arguments each time. The in-code comments explicitly promise these three consumers share "the SAME document-store provider" / "the SAME memory_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 win

Still unresolved: threads/ prefix checks remain case-sensitive.

is_thread_scoped_path/thread_memory_prefix (and every call site: the write reservation guard, and both branches of the lane split in retrieve_context) still use plain starts_with. A previously-flagged concern on this exact code (case-insensitive threads/ 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.md aliases the reserved threads/foo.md path on disk but slips past both the public write guard 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 via is_thread_scoped_path at 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

📥 Commits

Reviewing files that changed from the base of the PR and between a0310d6 and 404760c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (29)
  • crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs
  • crates/ironclaw_host_runtime/src/memory_context.rs
  • crates/ironclaw_host_runtime/tests/memory_prompt_context.rs
  • crates/ironclaw_loop_support/src/lib.rs
  • crates/ironclaw_memory/Cargo.toml
  • crates/ironclaw_memory/src/lib.rs
  • crates/ironclaw_memory/src/service.rs
  • crates/ironclaw_memory_native/src/lib.rs
  • crates/ironclaw_memory_native/src/service.rs
  • crates/ironclaw_memory_native/tests/memory_service_facade.rs
  • crates/ironclaw_product_workflow/tests/inbound_turn_contract.rs
  • crates/ironclaw_product_workflow/tests/support/planned_agent_loop.rs
  • crates/ironclaw_reborn/Cargo.toml
  • crates/ironclaw_reborn/src/after_turn_memory.rs
  • crates/ironclaw_reborn/src/lib.rs
  • crates/ironclaw_reborn/src/loop_driver_host.rs
  • crates/ironclaw_reborn/src/runtime.rs
  • crates/ironclaw_reborn/src/turn_run_executor.rs
  • crates/ironclaw_reborn/tests/llm_gateway.rs
  • crates/ironclaw_reborn/tests/loop_driver_host.rs
  • crates/ironclaw_reborn_composition/src/runtime.rs
  • crates/ironclaw_reborn_composition/tests/product_live_adapters.rs
  • crates/ironclaw_turns/src/run_profile/instruction_bundle.rs
  • crates/ironclaw_turns/tests/agent_loop_host_contract.rs
  • docs/superpowers/specs/2026-06-25-reborn-memory-host-lifecycle-design.md
  • tests/integration/support/group.rs
  • tests/integration/support/planned_runtime_parts_shape.rs
  • tests/integration/wiring_parity.rs
  • tests/support/reborn_parity_qa/binary_e2e.rs

Comment on lines +219 to +230
/// 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>>>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 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

Comment thread crates/ironclaw_reborn/tests/loop_driver_host.rs Outdated
@ironloopai

ironloopai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

🗂️ Archived IronLoop Review: reviewer

This result is from an older PR head and is no longer the active review.

Field Value
Status Superseded
Verdict ✅ Approved
Findings 0 blocking / 0 notes
Reviewed head 8522b3512d34
Archived 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.

@BenKurrek

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@BenKurrek

Copy link
Copy Markdown
Collaborator Author

/canary

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Started Reborn WebUI v2 live canary for reborn/memory-lifecycle at 8522b3512d with cases all: https://github.com/nearai/ironclaw/actions/runs/28803586262

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ 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

**Inline candidates:** 0

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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloop review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloop review when the fix may affect multiple areas.
  4. Use @ironloop status to check queued/running/completed/stale/stalled state while reviewers run.

@ironloopai
ironloopai Bot dismissed their stale review July 6, 2026 15:39

Superseded by a later IronLoop approved review for this reviewer.

@ironloopai ironloopai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ 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

**Inline candidates:** 0

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:

  1. Push the fix to this PR branch.
  2. Re-run this reviewer with @ironloop review --agent reviewer if you only changed this reviewer's findings.
  3. Re-run all reviewers with @ironloop review when the fix may affect multiple areas.
  4. Use @ironloop status to check queued/running/completed/stale/stalled state while reviewers run.

Copy link
Copy Markdown
Collaborator Author

/canary

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Started Reborn WebUI v2 live canary for reborn/memory-lifecycle at d1f873e49e with cases all: https://github.com/nearai/ironclaw/actions/runs/28807333885

@BenKurrek
BenKurrek merged commit 565b0d8 into stack/memory/01-userland-extension Jul 20, 2026
18 checks passed
BenKurrek added a commit that referenced this pull request Jul 20, 2026
…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>
BenKurrek added a commit that referenced this pull request Jul 24, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: experienced 6-19 merged PRs risk: low Changes to docs, tests, or low-risk modules scope: dependencies Dependency updates scope: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant