Skip to content

feat(memory): AMA-Agent causality-graph MemoryService provider - #6694

Open
pranavraja99 wants to merge 5 commits into
mainfrom
feat/ama-agent-memory-provider
Open

pranavraja99 wants to merge 5 commits into
mainfrom
feat/ama-agent-memory-provider

Conversation

@pranavraja99

Copy link
Copy Markdown
Contributor

What

Adds ironclaw_memory_ama_agent — a causality-graph MemoryService provider — and wires it into the compose-time [memory] binding lane opened by #6345. [memory].provider = "ama-agent.local.memory" now resolves to a real provider instead of failing closed as an unknown id.

It's a Rust port of the memory system from AMA-Bench: Evaluating Long-Horizon Memory for Agentic Applications (arXiv:2602.22769) and its reference implementation.

Why

To make ironclaw's memory subsystem measurable against alternatives. We now have three arms selectable by config — native, mem0 (#6345), and this — so they can be run on the same suites with the same model.

Early data motivating it, on nearai-bench's reborn_memory suite (152 scenarios, deepseek-v4-flash, all runs 0 errors):

backend score
reborn + mem0 0.480
reborn + native 0.443

Swapping the backend moves the number by ~3.7 points, so the abstraction #6345 introduced is doing real work. This adds a third, architecturally different arm.

The reference implementation is Python with no service mode, so a faithful in-ironclaw comparison needs a real native provider rather than a subprocess wrapper.

Design

Two stages, per the paper:

  1. Construction — each recorded interaction is LLM-abstracted into environment-state / task-state nodes plus causal and association edges, merged into a per-scope graph.
  2. Retrieval — embed the query, take top-K nearest nodes, then ask the model whether that evidence suffices before expanding through the graph or running a structured aggregation.

Mapping fidelity

Following ironclaw_memory_mem0's convention:

IronClaw op mapping fidelity
record_interaction LLM extraction → nodes/edges/turn primary
retrieve_context embed → top-K → sufficiency → graph | aggregate primary
search stage-1 similarity, reshaped good
write / read / tree unsupported — no addressable-document model none
profile_set / profile_read unsupported — no profile concept upstream none

Unsupported ops are not overridden, so they inherit the trait's fail-closed unavailable default rather than returning something plausible but wrong. The manifest likewise declares only context_retrieval + interaction_log, not document_store.

Deliberate divergences from upstream

All documented in the crate docs rather than hidden:

  • Both NEED_GRAPH strategies ship, selectable via graph_retrieval_mode. The paper describes walking the causality graph; the reference implementation's retrieve.py never consults causal_graph at all and instead returns neighbouring turns by index. They disagree, so both are implemented to measure the difference instead of picking one and asserting it.
  • NEED_AGGREGATE replaces NEED_CODE. Upstream generates Python and executes it in a subprocess that inherits the parent environment. The same class of count/list/pattern queries is answered here by a fixed native menu, so a memory provider adds no arbitrary-code-execution surface. Bounded, documented fidelity loss against a paper-reported ~23.5% of queries.
  • Incremental construction. Upstream builds memory in one batch pass over a finished trajectory; ironclaw delivers turns one at a time through record_interaction.
  • Prompts adapted to strict JSON, matching this codebase's typed-contract convention, with upstream's loose-substring parsing retained as a fallback.

Two things reviewers should look at

1. The provider carries its own LLM + embedding clients. The composed model gateway lives in ironclaw_operator (layer products), which a substrates crate may not depend on, and third-party providers are constructed synchronously before any async provider factory runs. So chat.rs is a small OpenAI-compatible LlmProvider sharing the embedder's SSRF gate. ironclaw_llm's retry/cost instrumentation is deliberately not reused — both calls are best-effort and already degrade — and that's documented at the call site rather than left implicit.

2. A dedicated graph mount, not the composed root filesystem. Third-party providers are built at factory.rs ~3247, but the composed root filesystem isn't assembled until ~3625 — which is exactly why for_third_party passes filesystem: None. mem0 doesn't notice (pure HTTP); this provider needs durable storage. Rather than reorder the build, it gets a narrow /memory mount over <local-dev root>/ama-agent-memory. Open to a different approach if reordering is preferable.

Fail-closed behavior

Every missing prerequisite (graph filesystem, embedding endpoint/model, chat endpoint/model) logs its own specific reason rather than one generic failure, and an unrecognized graph_retrieval_mode is rejected instead of defaulted.

That precision is deliberate: a memory provider that silently retrieves nothing is indistinguishable from genuinely poor recall in a benchmark. That's the one failure mode that would quietly corrupt the comparison this crate exists to run.

Architecture test generalized (please note)

reborn_dependency_boundaries's provider-neutrality test hardcoded "ironclaw_memory_mem0", so it would have silently failed to enforce the rule for this new crate. It's now table-driven over every concrete provider crate, so the next provider is covered automatically.

docs/plans/composition-pubuse.snapshot is updated for the intentional facade addition (AmaAgentConnectionConfig).

Config

[memory]
provider = "ama-agent.local.memory"

[memory.ama_agent]
embedding_base_url = "https://openrouter.ai/api/v1"
embedding_model = "openai/text-embedding-3-small"
llm_base_url = "https://openrouter.ai/api/v1"
llm_model = "deepseek/deepseek-v4-flash"
graph_retrieval_mode = "edge_traversal"   # or "turn_window"

Secrets come from env only: MEMORY_AMA_AGENT_EMBEDDING_API_KEY, MEMORY_AMA_AGENT_LLM_API_KEY.

Off by default — compiled only under --features memory-ama-agent, same as mem0.

Verification

  • 51 crate unit tests; cargo clippy clean on both touched crates; cargo fmt --check clean
  • All ironclaw_architecture tests green, including the generalized boundary test and the manifest ratchet
  • 33 ironclaw_host_runtime memory tests green
  • cargo check with memory-ama-agent enabled

Two bugs my own tests caught, worth mentioning because both would have skewed results rather than crashed: hallucinated edge endpoints created dangling node ids that traversal followed into nothing, and search returned zero-similarity nodes as if relevant.

Status

Not yet run end-to-end on a full suite — that's next, on reborn_memory and AMA-Bench. Opening now for review of the wiring and the two design calls above.

🤖 Generated with Claude Code

A Rust port of the memory system from "AMA-Bench: Evaluating Long-Horizon
Memory for Agentic Applications" (arXiv:2602.22769) and its reference
implementation, plugged into the third-party provider lane PR #6345 opened.
Third arm of a memory-backend comparison (ironclaw native vs mem0 vs this),
measured on the same suites with the same model. The reference impl is Python
with no service mode, so a faithful in-ironclaw comparison needs a real native
provider rather than a subprocess wrapper.

Modules (48 unit tests, clippy-clean):
- graph.rs     pure, zero-I/O causality graph + every retrieval primitive
- embedding.rs crate-local trait; real OpenAI-compatible client + offline stub
- url_check.rs SSRF gate adapted from ironclaw_memory_mem0
- store.rs     per-scope JSON persistence over ironclaw_filesystem
- llm.rs       extraction + sufficiency-judgment prompts and parsing
- service.rs   the MemoryService impl
- config.rs    knobs defaulted to upstream's configs/ama_agent.yaml

Deliberate divergences, all documented in the crate docs rather than hidden:

* BOTH NEED_GRAPH strategies ship, selectable via `graph_retrieval_mode`. The
  paper describes walking the causality graph; the reference implementation's
  retrieve.py never consults causal_graph at all and instead returns turns by
  index. They disagree, so both are implemented to MEASURE the difference
  instead of assuming one.
* NEED_AGGREGATE replaces NEED_CODE. Upstream generates Python and executes it
  in a subprocess inheriting the parent environment. The same class of
  count/list/pattern queries is answered by a fixed native menu, so a memory
  provider adds no arbitrary-code-execution surface. Bounded fidelity loss
  against a paper-reported ~23.5% of queries.
* Construction is incremental (ironclaw delivers turns via record_interaction)
  rather than upstream's one batch pass over a finished trajectory.
* Prompts are adapted to strict JSON, not transcribed, matching this codebase's
  typed-contract convention; the loose-substring fallback upstream relies on is
  retained as a second chance.

Behavior worth noting:
- Unsupported ops (write/read/tree/profile_*) are deliberately NOT overridden,
  inheriting the trait's fail-closed `unavailable` default instead of returning
  something plausible but wrong. Recorded in the mapping-fidelity table.
- Extraction and the sufficiency judgment both DEGRADE rather than fail:
  memory must never break a turn. Bad extraction still records the raw turn;
  a bad judgment falls back to plain similarity retrieval.
- Edges whose endpoints were not extracted are dropped, so a hallucinated
  endpoint cannot create a dangling node id that traversal follows into nothing.
- search() filters zero-similarity hits. top_k returns the k NEAREST regardless
  of distance, which suits context retrieval but makes a model-facing search
  tool emit noise the model may then reason from.
- Corrupt stored graph is an error, not a silent reset — a silent reset is
  indistinguishable from a genuine recall failure in a benchmark.
Wires the causality-graph provider into the same compose-time selection path
mem0 uses, so `[memory].provider = "ama-agent.local.memory"` now resolves to a
real MemoryService instead of failing closed as an unknown id.

- assets/memory_ama_agent/manifest.toml: identity-only manifest (declares no
  tools of its own). Declares only context_retrieval + interaction_log, NOT
  document_store — a causality graph has no addressable-document model, matching
  the crate's mapping-fidelity table.
- memory_native_extension.rs: extension-id / service / manifest constants.
- memory_provider_factory.rs: AmaAgentConnectionConfig + a feature-gated arm and
  create_ama_agent_provider(). Fails closed with a SPECIFIC logged reason for each
  missing prerequisite (graph filesystem, embedding endpoint/model, chat
  endpoint/model) rather than one generic failure — a provider that silently
  retrieves nothing is indistinguishable from genuinely poor recall in a benchmark.
  An unrecognized graph_retrieval_mode is likewise rejected, since quietly
  defaulting would publish a mislabelled comparison arm.
- input.rs: `with_ama_agent_memory_connection` builder, mirroring the mem0 pair.
- factory.rs: build_ama_agent_graph_filesystem() mounts a dedicated local dir at
  /memory over `<local-dev root>/ama-agent-memory`.

  Why a dedicated mount rather than the composed root filesystem: third-party
  providers are constructed ONCE at startup (line ~3247), but the composed root
  filesystem is not assembled until much later (~3625) — which is exactly why
  `for_third_party` passes `filesystem: None`. mem0 doesn't notice (it is pure
  HTTP); this provider needs durable storage, so it gets its own narrow mount
  instead of reordering the build.

- chat.rs (new, in the provider crate): a small OpenAI-compatible LlmProvider.
  The composed model gateway lives in ironclaw_operator (layer `products`), which
  a `substrates` crate may not depend on, and the provider is built synchronously
  before any async provider factory runs. So it owns its own client, sharing the
  embedder's SSRF gate. Documented that ironclaw_llm's retry/cost instrumentation
  is deliberately not reused: both calls are best-effort and already degrade.

- ironclaw_reborn_config: `[memory.ama_agent]` section (endpoints, top_k,
  graph_retrieval_mode, graph_depth), secrets via env only.

- ARCHITECTURE TEST GENERALIZED: reborn_dependency_boundaries' provider-neutrality
  test hardcoded "ironclaw_memory_mem0", so it would have silently failed to
  enforce the rule for this new crate. Now table-driven over every concrete
  provider crate.

- Updated docs/plans/composition-pubuse.snapshot for the intentional facade
  addition (AmaAgentConnectionConfig).

Verified: cargo check with the feature on; 51 crate tests; all ironclaw_architecture
tests green (incl. the generalized boundary test + the manifest ratchet); 33
host_runtime memory tests; clippy clean on both crates.
Copilot AI review requested due to automatic review settings July 27, 2026 05:02
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@ironloopai

ironloopai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown
Contributor

🔎 IronLoop Review Status

Head: 7eae6ab994348bb915407d172dd191aab94a517b
Result: One or more review results were superseded by a newer PR head.
Next: Run @ironloopai review on the latest PR head.
Updated: 2026-07-27T05:38:30.009Z

Current reviewers:

Reviewer State Verdict Findings Last update
ironloop/common-reviewer (reviewer) Superseded N/A N/A 2026-07-27T05:03:18.310Z
Reviewer summaries
Reviewer Detail
ironloop/common-reviewer (reviewer) Superseded by a newer PR head. New head: a46b5fc. Previous verdict: Review declined.
Recent activity
Time Reviewer State Detail
2026-07-27T05:02:05.760Z ironloop/common-reviewer (reviewer) Queued Accepted review request for head 464d9c8.
2026-07-27T05:02:05.760Z ironloop/common-reviewer (reviewer) Queued Waiting for this reviewer lane to become available.
2026-07-27T05:02:06.360Z ironloop/common-reviewer (reviewer) Started Reviewer worker started.
2026-07-27T05:02:08.616Z ironloop/common-reviewer (reviewer) Workspace ready Prepared isolated checkout (head_ref) at 464d9c8.
2026-07-27T05:02:40.814Z ironloop/common-reviewer (reviewer) Superseded A newer PR head replaced this review (a46b5fc).
2026-07-27T05:03:18.310Z ironloop/common-reviewer (reviewer) Result captured Skipped; 0 blocking findings.
2026-07-27T05:03:18.310Z ironloop/common-reviewer (reviewer) Superseded A newer PR head replaced this review (a46b5fc).
Available commands
  • @ironloopai help
  • @ironloopai agents
  • @ironloopai review
  • @ironloopai review --agent <agent>
Run metadata

Admission: webhook accepted the request and IronLoop persisted reviewer state before this projection.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added scope: docs Documentation scope: dependencies Dependency updates labels Jul 27, 2026
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6694 July 27, 2026 05:02 Destroyed
@github-actions github-actions Bot added size: XL 500+ changed lines risk: medium Business logic, config, or moderate-risk modules contributor: core 20+ merged PRs labels Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b1edc84a-a4b7-427b-aeee-362e27fab0f8

📥 Commits

Reviewing files that changed from the base of the PR and between 728e011 and 7eae6ab.

📒 Files selected for processing (1)
  • crates/ironclaw_reborn_composition/src/runtime.rs

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added the AMA-Agent causality-graph memory provider, optional via the memory-ama-agent feature, including host/runtime wiring and a provider manifest.
    • Supports interaction recording, context retrieval, similarity search, graph traversal, and turn-window retrieval with model-judged sufficiency and aggregate queries.
    • Added OpenAI-compatible chat completions and embeddings with optional API authentication, plus configurable retrieval behavior via [memory.ama_agent].
  • Tests
    • Updated dependency-boundary enforcement to be table-driven over memory providers.

Walkthrough

Adds an AMA-Agent causality-graph memory provider with OpenAI-compatible clients, graph extraction and retrieval, scoped persistence, configuration, composition wiring, extension registration, and dependency-boundary coverage.

Changes

AMA-Agent memory provider

Layer / File(s) Summary
Provider contracts and outbound clients
crates/ironclaw_memory_ama_agent/*
Adds configuration, errors, URL validation, OpenAI-compatible clients, deterministic test support, and the public crate API.
Causality graph and LLM reasoning
crates/ironclaw_memory_ama_agent/src/graph.rs, crates/ironclaw_memory_ama_agent/src/llm.rs
Adds graph entities, retrieval and aggregation operations, structured extraction, sufficiency judgments, and JSON parsing.
Graph persistence and memory service
crates/ironclaw_memory_ama_agent/src/store.rs, crates/ironclaw_memory_ama_agent/src/service.rs
Adds scoped JSON storage and MemoryService implementations for recording, context retrieval, similarity search, and fail-closed unsupported operations.
Configuration and provider composition
Cargo.toml, crates/ironclaw_reborn_config/..., crates/ironclaw_reborn_composition/*, docs/plans/composition-pubuse.snapshot
Adds workspace membership, configuration, optional feature wiring, graph filesystem setup, provider construction, binding input, runtime inspection, and exports.
Extension registration and dependency boundaries
crates/ironclaw_host_runtime/..., crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs
Registers the extension identity and manifest, and generalizes provider dependency-boundary checks.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RebornHostBindings
  participant MemoryProviderFactory
  participant AmaAgentMemoryService
  participant AmaLlm
  participant GraphStore
  RebornHostBindings->>MemoryProviderFactory: provide AMA-Agent connection
  MemoryProviderFactory->>AmaAgentMemoryService: construct provider with filesystem and clients
  AmaAgentMemoryService->>AmaLlm: extract interaction or judge evidence
  AmaAgentMemoryService->>GraphStore: load or update scoped graph
  GraphStore-->>AmaAgentMemoryService: graph state
Loading

Possibly related issues

  • nearai/ironclaw#3773 — Adds a concrete memory provider and extends dependency-boundary architecture tests.
  • nearai/ironclaw#4470 — Adds composition dependency-boundary coverage for the new provider.

Suggested reviewers: copilot, ilblackdragon, serrrfirat

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, but it omits required template sections like Change Type, Linked Issue, Test Strategy, and Security/Trust-boundary checklists. Rewrite it to follow the repo template, including every required section and the linked issue, validation, test strategy, security, rollback, and review-track fields.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is Conventional Commits style and accurately summarizes the new AMA-Agent memory provider.
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.
✨ Finishing Touches
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/ama-agent-memory-provider

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.

Copilot AI review requested due to automatic review settings July 27, 2026 05:02
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6694 July 27, 2026 05:02 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@railway-app

railway-app Bot commented Jul 27, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-6694 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 27, 2026 at 5:47 am

@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

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/input.rs (1)

380-398: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Misplaced doc comment: with_ama_agent_memory_connection inherited the wrong doc block.

The pre-existing doc (Lines 380-384, "Attach a resolved memory profile binding policy... reads the resolved policy via the memory_binding_policy field") describes with_memory_binding_policy, not the newly inserted with_ama_agent_memory_connection. Since Rust merges contiguous /// lines into one doc comment, with_memory_binding_policy (Line 395) is now left completely undocumented, and rustdoc for with_ama_agent_memory_connection misleadingly opens with a description of a different field.

📝 Proposed fix
-    /// Attach a resolved memory profile binding policy (issue `#3537`). The CLI
-    /// resolves this from the `[memory]` config section + deployment profile,
-    /// failing closed before composition is built. The factory reads the
-    /// resolved policy via the `memory_binding_policy` field when destructuring
-    /// the build input.
     /// Attach AMA-Agent memory settings. Only consulted when the binding policy
     /// selects that provider; inert otherwise.
     pub fn with_ama_agent_memory_connection(
         mut self,
         connection: crate::AmaAgentConnectionConfig,
     ) -> Self {
         self.ama_agent_memory_connection = Some(connection);
         self
     }

+    /// Attach a resolved memory profile binding policy (issue `#3537`). The CLI
+    /// resolves this from the `[memory]` config section + deployment profile,
+    /// failing closed before composition is built. The factory reads the
+    /// resolved policy via the `memory_binding_policy` field when destructuring
+    /// the build input.
     pub fn with_memory_binding_policy(mut self, policy: MemoryBindingPolicy) -> Self {
         self.memory_binding_policy = Some(policy);
         self
     }
🤖 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/input.rs` around lines 380 - 398, Move
the resolved memory profile binding policy documentation from
`with_ama_agent_memory_connection` to `with_memory_binding_policy`, keeping the
AMA-Agent memory settings documentation attached only to
`with_ama_agent_memory_connection`. Ensure both methods have accurate, separate
rustdoc comments.
🤖 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_ama_agent/src/graph.rs`:
- Around line 52-59: Update the persisted enums NodeKind and GraphEdge to add
serde’s snake_case rename convention alongside their existing Serialize and
Deserialize derives. Apply #[serde(rename_all = "snake_case")] to both enum
declarations so graph.json round-tripping uses stable snake_case wire names.
- Around line 79-84: Update upsert_edge to normalize Association endpoints into
a deterministic order before checking for or inserting the edge, so reversed a/b
inputs compare as the same logical association. Leave Causal endpoint direction
unchanged and apply the same normalization at the other upsert_edge location.

In `@crates/ironclaw_memory_ama_agent/src/store.rs`:
- Around line 95-110: Replace the process-local write_lock read-modify-write in
AmaAgentStore::update (crates/ironclaw_memory_ama_agent/src/store.rs:95-110)
with the shared bounded cas_update helper, preserving mutation and serialization
error handling while avoiding a mutex across I/O. In record_interaction
(crates/ironclaw_memory_ama_agent/src/service.rs:196-233), move
processed_turn_run_ids.contains and max turn_idx + 1 computation into the
store.update mutate closure so both use the freshest graph atomically; propagate
graph-load errors instead of converting Err(_) to 0, unless an inline silent-ok
justification documents the fallback.

In `@crates/ironclaw_reborn_composition/src/factory.rs`:
- Around line 792-813: Replace the silent `VirtualPath::new("/memory").ok()?` in
the filesystem setup with an explicit error branch that logs the
path-construction failure via `tracing::warn!` using the existing memory target
and then returns `None`, matching the `create_dir_all` and `mount_local`
branches in this function.
- Around line 3283-3301: Extend the ama_agent_filesystem match in the factory
wiring to also handle RebornStorageInput::HostedSingleTenantPostgres when
ama_agent_memory_connection is present, using its root with
build_ama_agent_graph_filesystem just like LocalDev. Preserve None for
unsupported storage profiles, and ensure any intentionally unsupported profile
is identified in the resulting warning rather than failing with a generic
message.

In `@crates/ironclaw_reborn_composition/src/memory_provider_factory.rs`:
- Around line 296-409: Add composition-layer tests in the existing #[cfg(test)]
module for create_ama_agent_provider, covering fail-closed results for missing
graph filesystem, missing embedding endpoint/model, missing chat endpoint/model,
and unknown graph_retrieval_mode, plus a valid configuration that constructs a
provider. Reuse the module’s existing dependency/configuration setup and assert
each invalid path returns None while the valid path returns Some.

---

Outside diff comments:
In `@crates/ironclaw_reborn_composition/src/input.rs`:
- Around line 380-398: Move the resolved memory profile binding policy
documentation from `with_ama_agent_memory_connection` to
`with_memory_binding_policy`, keeping the AMA-Agent memory settings
documentation attached only to `with_ama_agent_memory_connection`. Ensure both
methods have accurate, separate rustdoc comments.
🪄 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: ad0f2d6b-06d0-4802-bc69-d6784baef98e

📥 Commits

Reviewing files that changed from the base of the PR and between a091876 and a46b5fc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock, !**/Cargo.lock
📒 Files selected for processing (22)
  • Cargo.toml
  • crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs
  • crates/ironclaw_host_runtime/assets/memory_ama_agent/manifest.toml
  • crates/ironclaw_host_runtime/src/memory_native_extension.rs
  • crates/ironclaw_memory_ama_agent/Cargo.toml
  • crates/ironclaw_memory_ama_agent/src/chat.rs
  • crates/ironclaw_memory_ama_agent/src/config.rs
  • crates/ironclaw_memory_ama_agent/src/embedding.rs
  • crates/ironclaw_memory_ama_agent/src/error.rs
  • crates/ironclaw_memory_ama_agent/src/graph.rs
  • crates/ironclaw_memory_ama_agent/src/lib.rs
  • crates/ironclaw_memory_ama_agent/src/llm.rs
  • crates/ironclaw_memory_ama_agent/src/service.rs
  • crates/ironclaw_memory_ama_agent/src/store.rs
  • crates/ironclaw_memory_ama_agent/src/url_check.rs
  • crates/ironclaw_reborn_composition/Cargo.toml
  • crates/ironclaw_reborn_composition/src/factory.rs
  • crates/ironclaw_reborn_composition/src/input.rs
  • crates/ironclaw_reborn_composition/src/lib.rs
  • crates/ironclaw_reborn_composition/src/memory_provider_factory.rs
  • crates/ironclaw_reborn_config/src/config_file.rs
  • docs/plans/composition-pubuse.snapshot

Comment on lines +52 to +59
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum NodeKind {
/// Environment state — what the world looks like (objects, positions, files,
/// query results, error text).
EnvState,
/// Objective/task state — progress toward the goal, sub-goals, plans.
TaskState,
}

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

Persisted enums missing #[serde(rename_all = "snake_case")].

NodeKind and GraphEdge are round-tripped through graph.json (see store.rs's serde_json::to_vec/from_slice) but default-derive PascalCase wire names instead of an explicit convention.

🔧 Proposed fix
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
 pub enum NodeKind {
     EnvState,
     TaskState,
 }
...
 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "snake_case")]
 pub enum GraphEdge {
     Causal { from: NodeId, to: NodeId },
     Association { a: NodeId, b: NodeId },
 }
As per coding guidelines: "Enums serialized over the network or persisted to the database must derive Serialize and Deserialize and use `#[serde(rename_all = "snake_case")]`, or explicit wire renames where required."

Also applies to: 78-84

🤖 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_ama_agent/src/graph.rs` around lines 52 - 59, Update
the persisted enums NodeKind and GraphEdge to add serde’s snake_case rename
convention alongside their existing Serialize and Deserialize derives. Apply
#[serde(rename_all = "snake_case")] to both enum declarations so graph.json
round-tripping uses stable snake_case wire names.

Source: Coding guidelines

Comment on lines +79 to +84
pub enum GraphEdge {
/// `from` caused / was a precondition of `to`.
Causal { from: NodeId, to: NodeId },
/// `a` and `b` co-occur / are associated, no direction implied.
Association { a: NodeId, b: NodeId },
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

upsert_edge doesn't normalize Association endpoint order, so the "no direction implied" invariant can be violated by duplicate edges.

Two extractions of the same fact in reversed textual order produce Association{a,b} and Association{a:b,b:a}, which Vec::contains treats as distinct — silently duplicating a logically identical edge.

🔧 Proposed fix
 pub fn upsert_edge(&mut self, edge: GraphEdge) {
+    let edge = match edge {
+        GraphEdge::Association { a, b } if a > b => GraphEdge::Association { a: b, b: a },
+        other => other,
+    };
     if !self.edges.contains(&edge) {
         self.edges.push(edge);
     }
 }

Also applies to: 199-204

🤖 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_ama_agent/src/graph.rs` around lines 79 - 84, Update
upsert_edge to normalize Association endpoints into a deterministic order before
checking for or inserting the edge, so reversed a/b inputs compare as the same
logical association. Leave Causal endpoint direction unchanged and apply the
same normalization at the other upsert_edge location.

Comment on lines +95 to +110
pub async fn update<F>(&self, scope: &ResourceScope, mutate: F) -> Result<(), AmaAgentError>
where
F: FnOnce(&mut CausalityGraph) -> Result<(), AmaAgentError> + Send,
{
let _guard = self.write_lock.lock().await;
let mut graph = self.load(scope).await?;
mutate(&mut graph)?;
let bytes = serde_json::to_vec(&graph)
.map_err(|e| AmaAgentError::Storage(format!("serialize graph: {e}")))?;
let path = Self::graph_path(scope)?;
self.filesystem
.write_file(&path, &bytes)
.await
.map_err(|e| AmaAgentError::Storage(format!("write graph.json: {e}")))?;
Ok(())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Read-modify-write isn't CAS-backed, and callers compute derived state from stale unlocked reads — turns can be silently lost under concurrency.

The persistence layer holds a process-local mutex across filesystem I/O instead of using a versioned/CAS update, and its only caller derives next_idx and the idempotency verdict from separate unlocked reads taken before that lock — so two concurrent record_interaction calls on the same scope can compute the same next_idx and the second silently overwrites the first's turn via upsert_turn.

  • crates/ironclaw_memory_ama_agent/src/store.rs#L95-L110: Replace the write_lock: Mutex<()> read-then-write pattern with the repo's shared bounded CAS helper (cas_update) so writes are versioned against the filesystem's own state, not a process-local lock that only covers one instance.
  • crates/ironclaw_memory_ama_agent/src/service.rs#L196-L233: Move the idempotency check (processed_turn_run_ids.contains) and the next_idx computation (max turn_idx + 1) inside the mutate closure passed to store.update, where they see the freshest graph under the same atomic section as the write — don't pre-read the graph twice outside the lock. Also replace Err(_) => 0 with propagating the load error (?) or an explicit // silent-ok:-justified fallback, per the fail-loud guideline.

As per coding guidelines: "Every read-modify-write must use the shared bounded CAS update path; do not overwrite a previously read version without CAS," "Do not hold a process-local or per-record async mutex across filesystem or backend I/O," and "do not use ... warning-only error handling that continues with a potentially uninitialized value for DB, I/O, workspace, or settings reads... justified fallbacks must include an inline // silent-ok: <reason> comment."

📍 Affects 2 files
  • crates/ironclaw_memory_ama_agent/src/store.rs#L95-L110 (this comment)
  • crates/ironclaw_memory_ama_agent/src/service.rs#L196-L233
🤖 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_ama_agent/src/store.rs` around lines 95 - 110, Replace
the process-local write_lock read-modify-write in AmaAgentStore::update
(crates/ironclaw_memory_ama_agent/src/store.rs:95-110) with the shared bounded
cas_update helper, preserving mutation and serialization error handling while
avoiding a mutex across I/O. In record_interaction
(crates/ironclaw_memory_ama_agent/src/service.rs:196-233), move
processed_turn_run_ids.contains and max turn_idx + 1 computation into the
store.update mutate closure so both use the freshest graph atomically; propagate
graph-load errors instead of converting Err(_) to 0, unless an inline silent-ok
justification documents the fallback.

Source: Coding guidelines

Comment on lines +792 to +813
let host_root = root.join("ama-agent-memory");
if let Err(error) = std::fs::create_dir_all(&host_root) {
tracing::warn!(
target: "ironclaw_reborn::memory",
%error,
"could not create the ama-agent graph directory; failing the binding closed"
);
return None;
}
let virtual_root = VirtualPath::new("/memory").ok()?;
let host_path = HostPath::from_path_buf(host_root);
let mut filesystem = DiskFilesystem::new();
if let Err(error) = filesystem.mount_local(virtual_root, host_path) {
tracing::warn!(
target: "ironclaw_reborn::memory",
%error,
"could not mount the ama-agent graph directory; failing the binding closed"
);
return None;
}
Some(Arc::new(filesystem) as Arc<dyn RootFilesystem>)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Silent .ok()? breaks the function's own fail-loud pattern.

VirtualPath::new("/memory").ok()? (Line 801) swallows the error and returns None without logging, unlike the create_dir_all and mount_local failure branches immediately around it, which both call tracing::warn! with a specific reason before failing closed. If this ever fails (e.g., a future change to VirtualPath validation), an operator gets no diagnostic at all for why ama-agent memory silently stopped working.

🔧 Proposed fix
-    let virtual_root = VirtualPath::new("/memory").ok()?;
+    let virtual_root = match VirtualPath::new("/memory") {
+        Ok(path) => path,
+        Err(error) => {
+            tracing::warn!(
+                target: "ironclaw_reborn::memory",
+                %error,
+                "could not construct the ama-agent graph virtual mount path; failing the binding closed"
+            );
+            return None;
+        }
+    };

As per coding guidelines: "do not use ... .ok()? ... that continues with a potentially uninitialized value for DB, I/O, workspace, or settings reads. Fail loudly with ? by default; justified fallbacks must include an inline // silent-ok: <reason> comment naming the operation."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let host_root = root.join("ama-agent-memory");
if let Err(error) = std::fs::create_dir_all(&host_root) {
tracing::warn!(
target: "ironclaw_reborn::memory",
%error,
"could not create the ama-agent graph directory; failing the binding closed"
);
return None;
}
let virtual_root = VirtualPath::new("/memory").ok()?;
let host_path = HostPath::from_path_buf(host_root);
let mut filesystem = DiskFilesystem::new();
if let Err(error) = filesystem.mount_local(virtual_root, host_path) {
tracing::warn!(
target: "ironclaw_reborn::memory",
%error,
"could not mount the ama-agent graph directory; failing the binding closed"
);
return None;
}
Some(Arc::new(filesystem) as Arc<dyn RootFilesystem>)
}
let host_root = root.join("ama-agent-memory");
if let Err(error) = std::fs::create_dir_all(&host_root) {
tracing::warn!(
target: "ironclaw_reborn::memory",
%error,
"could not create the ama-agent graph directory; failing the binding closed"
);
return None;
}
let virtual_root = match VirtualPath::new("/memory") {
Ok(path) => path,
Err(error) => {
tracing::warn!(
target: "ironclaw_reborn::memory",
%error,
"could not construct the ama-agent graph virtual mount path; failing the binding closed"
);
return None;
}
};
let host_path = HostPath::from_path_buf(host_root);
let mut filesystem = DiskFilesystem::new();
if let Err(error) = filesystem.mount_local(virtual_root, host_path) {
tracing::warn!(
target: "ironclaw_reborn::memory",
%error,
"could not mount the ama-agent graph directory; failing the binding closed"
);
return None;
}
Some(Arc::new(filesystem) as Arc<dyn RootFilesystem>)
}
🤖 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/factory.rs` around lines 792 - 813,
Replace the silent `VirtualPath::new("/memory").ok()?` in the filesystem setup
with an explicit error branch that logs the path-construction failure via
`tracing::warn!` using the existing memory target and then returns `None`,
matching the `create_dir_all` and `mount_local` branches in this function.

Source: Coding guidelines

Comment on lines +3283 to +3301
// The AMA-Agent provider persists its causality graph, but the composed
// root filesystem does not exist yet at this point in the build (it is
// assembled further down), and a third-party provider is constructed once
// here rather than per-invocation. So give it a small dedicated local mount
// under the local-dev root instead of waiting for the composed filesystem.
// Non-local-dev storage yields `None`, which fails the arm closed.
let ama_agent_filesystem: Option<Arc<dyn RootFilesystem>> =
match (&storage, &ama_agent_memory_connection) {
(crate::input::RebornStorageInput::LocalDev { root, .. }, Some(_)) => {
build_ama_agent_graph_filesystem(root)
}
_ => None,
};
let deps = crate::MemoryProviderDeps::for_third_party(memory_provider_connection)
.with_ama_agent(
ama_agent_memory_connection.unwrap_or_default(),
ama_agent_filesystem,
);
crate::build_memory_service_resolver(memory_binding_policy, &deps)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^|/)factory\.rs$|(^|/)input\.rs$|CLAUDE\.md|AGENTS\.md|\.claude/rules' || true

echo
echo "== relevant factory snippet outline/lines =="
wc -l crates/ironclaw_reborn_composition/src/factory.rs crates/ironclaw_reborn_composition/src/input.rs
sed -n '3260,3315p' crates/ironclaw_reborn_composition/src/factory.rs

echo
echo "== RebornStorageInput definitions/usages =="
rg -n "enum RebornStorageInput|HostedSingleTenantPostgres|build_ama_agent_graph_filesystem|ama_agent_memory_connection|no graph filesystem|RootFilesystem" crates/ironclaw_reborn_composition/src/input.rs crates/ironclaw_reborn_composition/src/factory.rs -C 4

echo
echo "== AMa-agent graph filesystem implementation =="
rg -n "fn build_ama_agent_graph_filesystem|build_ama_agent_graph_filesystem|ama.agent|AMA-Agent|graph filesystem" crates -C 8

Repository: nearai/ironclaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== build_ama_agent_graph_filesystem implementation =="
sed -n '760,815p' crates/ironclaw_reborn_composition/src/factory.rs

echo
echo "== memory resolver error/warning paths =="
rg -n "no graph filesystem was supplied|Graph filesystem|with_ama_agent|No graph filesystem|Graph filesystem|ama-agent|MemoryProviderDeps|build_memory_service_resolver" crates/ironclaw_memory crates/ironclaw_reborn_composition crates -g '*.rs' -C 4

echo
echo "== input.rs structs around RebornStorageInput/build_hosted_single_tenant_postgres =="
sed -n '280,315p' crates/ironclaw_reborn_composition/src/input.rs
sed -n '450,535p' crates/ironclaw_reborn_composition/src/input.rs

echo
echo "== deterministic shape check: enum variants with root field and match pattern =="
python3 - <<'PY'
from pathlib import Path
p = Path('crates/ironclaw_reborn_composition/src/input.rs')
text = p.read_text()
print('RebornStorageInput contains LocalDev:', 'LocalDev {' in text and 'root: PathBuf,' in text[text.find('enum RebornStorageInput'):text.find('pub(crate) type BuildInput')])
print('RebornStorageInput contains HostedSingleTenantPostgres:', 'HostedSingleTenantPostgres {' in text and text.find('HostedSingleTenantPostgres {') < p.read_text().find('pub(crate) type BuildInput'))
s = text[text.find('enum RebornStorageInput'):text.find('pub(crate) type BuildInput')]
for name in ['LocalDev', 'HostedSingleTenantPostgres']:
    i = s.find(name)
print('LocalDev declares root before enum end:', 'LocalDev {' in s[s.find('LocalDev'):s.find('HostedSingleTenantPostgres')])
print('HostedSingleTenantPostgres declares root before enum end:', 'HostedSingleTenantPostgres {' in s[s.find('HostedSingleTenantPostgres'):s.find('}",', s.find('HostedSingleTenantPostgres'))+3])
PY

echo
echo "== exact factory match context =="
sed -n '3272,3305p' crates/ironclaw_reborn_composition/src/factory.rs

Repository: nearai/ironclaw

Length of output: 50372


Allow AMA-Agent memory graph mounts on HostedSingleTenantPostgres.

RebornStorageInput::HostedSingleTenantPostgres also carries root: PathBuf, but the match arm only builds ama_agent_filesystem for LocalDev; with ama-agent.local.memory selected, the provider fails closed with the generic “no graph filesystem was supplied” warning. If this profile is supported and root is available, mirror the local-dev case in crates/ironclaw_reborn_composition/src/factory.rs. If the profile is intentionally excluded, name the restriction in the warning.

Violates CLAUDE.md/AGENTS.md: changed Reborn crates must expose production profile support without silently failing closed, and product/wiring errors should name unsupported/incorrect configuration.

🤖 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/factory.rs` around lines 3283 - 3301,
Extend the ama_agent_filesystem match in the factory wiring to also handle
RebornStorageInput::HostedSingleTenantPostgres when ama_agent_memory_connection
is present, using its root with build_ama_agent_graph_filesystem just like
LocalDev. Preserve None for unsupported storage profiles, and ensure any
intentionally unsupported profile is identified in the resulting warning rather
than failing with a generic message.

Comment on lines +296 to +409
/// Build the AMA-Agent causality-graph provider, or `None` (fail-closed).
///
/// Fails closed — with a specific reason logged — when any of the four things it
/// cannot work without is absent: a graph filesystem, an embedding endpoint, a
/// chat endpoint, or a model id for either. Silently returning a provider that
/// retrieves nothing would look exactly like genuinely poor recall in a
/// benchmark, which is the failure mode this must not have.
#[cfg(feature = "memory-ama-agent")]
fn create_ama_agent_provider(deps: &MemoryProviderDeps) -> Option<Arc<dyn MemoryService>> {
let cfg = &deps.ama_agent;

let Some(filesystem) = deps.ama_agent_filesystem.clone() else {
tracing::warn!(
target: LOG_TARGET,
"ama-agent memory binding selected but no graph filesystem was supplied; failing closed"
);
return None;
};
let (Some(embed_url), Some(embed_model)) = (
cfg.embedding_base_url.as_deref(),
cfg.embedding_model.as_deref(),
) else {
tracing::warn!(
target: LOG_TARGET,
"ama-agent memory binding selected but the embedding endpoint/model is unset ([memory.ama_agent].embedding_base_url / .embedding_model); failing closed"
);
return None;
};
let (Some(llm_url), Some(llm_model)) = (cfg.llm_base_url.as_deref(), cfg.llm_model.as_deref())
else {
tracing::warn!(
target: LOG_TARGET,
"ama-agent memory binding selected but the chat endpoint/model is unset ([memory.ama_agent].llm_base_url / .llm_model); failing closed"
);
return None;
};

let embedder = match OpenAiCompatEmbedder::new(
embed_url,
cfg.embedding_api_key
.as_ref()
.map(|k| k.expose_secret().to_string()),
embed_model,
cfg.embedding_dimension.unwrap_or(1536),
) {
Ok(e) => Arc::new(e) as Arc<dyn AmaEmbeddingProvider>,
Err(error) => {
tracing::warn!(
target: LOG_TARGET,
%error,
"failed to build the ama-agent embedding client (rejected base URL); failing closed"
);
return None;
}
};

let chat = match OpenAiCompatChat::new(
llm_url,
cfg.llm_api_key
.as_ref()
.map(|k| k.expose_secret().to_string()),
llm_model,
) {
Ok(c) => Arc::new(c) as Arc<dyn ironclaw_llm::LlmProvider>,
Err(error) => {
tracing::warn!(
target: LOG_TARGET,
%error,
"failed to build the ama-agent chat client (rejected base URL); failing closed"
);
return None;
}
};

// An unrecognized mode is rejected rather than silently defaulted: the two
// modes are the thing being compared, so quietly running the wrong one would
// publish a mislabelled result.
let graph_retrieval_mode = match cfg.graph_retrieval_mode.as_deref() {
None => AmaAgentConfig::default().graph_retrieval_mode,
Some("edge_traversal") => GraphRetrievalMode::EdgeTraversal,
Some("turn_window") => GraphRetrievalMode::TurnWindow,
Some(other) => {
tracing::warn!(
target: LOG_TARGET,
mode = other,
"unknown [memory.ama_agent].graph_retrieval_mode (expected edge_traversal|turn_window); failing closed"
);
return None;
}
};

let defaults = AmaAgentConfig::default();
let config = AmaAgentConfig {
top_k: cfg.top_k.unwrap_or(defaults.top_k),
graph_retrieval_mode,
graph_depth: cfg.graph_depth.unwrap_or(defaults.graph_depth),
..defaults
};

tracing::info!(
target: LOG_TARGET,
top_k = config.top_k,
graph_depth = config.graph_depth,
mode = ?config.graph_retrieval_mode,
"built the ama-agent causality-graph memory provider"
);
Some(Arc::new(AmaAgentMemoryService::new(
GraphStore::new(filesystem),
embedder,
AmaLlm::new(chat),
config,
)) as Arc<dyn MemoryService>)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for composition-layer tests covering create_ama_agent_provider / build_ama_agent_graph_filesystem.
rg -n -C3 'create_ama_agent_provider|build_ama_agent_graph_filesystem' crates/ironclaw_reborn_composition

Repository: nearai/ironclaw

Length of output: 3328


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files 'crates/ironclaw_reborn_composition/**' | sed -n '1,120p'

echo
echo "== test/module markers =="
rg -n '#\[cfg\(.*test\)\]|mod tests|#\[cfg_attr|memory_provider_factory|ama_agent|MemoryProviderDeps|MemoryProviderBuilder|new_reborn_runtime|assemble_runtime' crates/ironclaw_reborn_composition/src -g '*.rs' | sed -n '1,240p'

echo
echo "== memory_provider_factory outline/snippet =="
wc -l crates/ironclaw_reborn_composition/src/memory_provider_factory.rs
sed -n '1,480p' crates/ironclaw_reborn_composition/src/memory_provider_factory.rs

Repository: nearai/ironclaw

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== memory_provider_factory tests =="
sed -n '444,536p' crates/ironclaw_reborn_composition/src/memory_provider_factory.rs

echo
echo "== factory ama-agent test region =="
sed -n '3207,3320p' crates/ironclaw_reborn_composition/src/factory.rs
sed -n '5700,5880p' crates/ironclaw_reborn_composition/src/factory/tests.rs | rg -n -C4 'ama_agent|memory_ama_agent|graph_retrieval|AMA_AGENT|memory-provider|provider'

echo
echo "== repo coverage rule references =="
sed -n '1,220p' crates/ironclaw_reborn_composition/AGENTS.md
sed -n '1,240p' crates/ironclaw_reborn_composition/CLAUDE.md
rg -n "New or changed production-wired behavior|caller-level test|fails closed|production-wired" crates/ironclaw_reborn_composition/AGENTS.md crates/ironclaw_reborn_composition/CLAUDE.md

Repository: nearai/ironclaw

Length of output: 9851


Add composition-layer tests for the AMA-Agent provider factory paths.

create_document_store_provider has cases for native/mem0/unknown third-party, but the AMA-Agent branches only return None; memory_provider_factory’s #[cfg(test)] module covers only mem0. Per the ironclaw_reborn_composition invariant—new production-wired behavior needs a caller-level test at the nearest seam—the fail-closed cases (missing filesystem/endpoint/model/unknown graph mode) and successful construction need tests.

🤖 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/memory_provider_factory.rs` around
lines 296 - 409, Add composition-layer tests in the existing #[cfg(test)] module
for create_ama_agent_provider, covering fail-closed results for missing graph
filesystem, missing embedding endpoint/model, missing chat endpoint/model, and
unknown graph_retrieval_mode, plus a valid configuration that constructs a
provider. Reuse the module’s existing dependency/configuration setup and assert
each invalid path returns None while the valid path returns Some.

Source: Path instructions

Adds `RebornRuntime::memory_document_store()`, mirroring the existing
`session_thread_service()` / `thread_scope()` accessors (same test-support gate).

Motivation: a memory benchmark needs to SEED memory before asking recall
questions. Without this, the only way to populate memory is to replay each
trajectory turn through the model so the after-turn writer fires — one LLM call
per step, which is prohibitive for a 100+ turn trajectory and also changes what
is being measured. This exposes the same `MemoryService` the prompt-context lane
and after-turn writer already use, so a harness can seed via record_interaction
directly.

It returns whichever provider the [memory] binding selected, so a harness can
compare native / mem0 / ama-agent without knowing which is bound.

Production is byte-identical: the field and accessor are both behind
`cfg(any(test, feature = "test-support"))`, and the clone that feeds it is taken
under the same gate before the value moves into after_turn_memory_writer.

Verified: cargo check both with and without test-support; ironclaw_architecture
tests green; fmt clean.
Copilot AI review requested due to automatic review settings July 27, 2026 05:26
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-6694 July 27, 2026 05:26 Destroyed

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

…eding

Companion to memory_document_store(): returns the ResourceScope built from the
same thread_scope + actor user id the runtime's own memory paths use.

Hand-building a scope outside the runtime is the exact failure this prevents — a
mismatched user/agent/project axis writes to a different document and retrieval
then finds nothing, which is indistinguishable from a memory backend that simply
forgot. thread_id/mission_id are None so seeded writes land in the durable
long-term lane rather than per-thread scratch.

Same test-support gate as the sibling accessors; production unchanged.

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

//! gateway, the provider owns a small client of its own — the same shape as
//! [`crate::embedding::OpenAiCompatEmbedder`], sharing its SSRF gate.
//!
//! Deliberately NOT reused here: `ironclaw_llm`'s retry/backoff and cost

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm not sure if this is the right way to do this. We are gonna create implementation drift if we dont use ironclaw_llm. I rather fix the problem there and use the crate

//!
//! | IronClaw op | AMA-Agent mapping | fidelity |
//! |----------------------|------------------------------------------------------|----------|
//! | `record_interaction` | LLM extraction -> nodes/edges/turn merged into graph | primary |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

hmm do we somehow have an abstraction in front of these? Who is the caller of this? They would need to know which memory is active and have match arms probably @BenKurrek how did we design the new memory structure wrt to that?

This branch was successfully deployed

1 active deployment
ironclaw-ci-preview / ironclaw-pr-6694 — 7eae6ab9 Deployed Jul 27, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-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.

3 participants