Skip to content

fix(reborn): [PRODUCTION CHANGE] #5605 — wire memory prompt-context source; pin the untrusted-memory envelope at int tier - #5742

Closed
henrypark133 wants to merge 10 commits into
mainfrom
fix-5605-memctx
Closed

henrypark133 wants to merge 10 commits into
mainfrom
fix-5605-memctx

Conversation

@henrypark133

Copy link
Copy Markdown
Collaborator

Root cause

ProductionMemoryPromptContextService (crates/ironclaw_host_runtime/src/memory_context.rs) was fully implemented — including the sanitize_snippet_text / wrap_untrusted_with_limit prompt-injection-hardening envelope — but nothing in ironclaw_reborn_composition composed it. The only production LoopContextPort implementation, ThreadBackedLoopContextPort, hardcoded memory_snippets: Vec::new() (crates/ironclaw_loop_support/src/lib.rs). Unlike its skill_context_source/identity_context_source siblings, there was no with_memory_context_source setter at all, so every real Reborn turn's memory recall was silently inert and the Untrusted memory content: envelope never reached a real system prompt.

Wiring shape

Mirrors the skill_context_source/identity_context_source sibling pattern exactly, at every layer:

  • ThreadBackedLoopContextPort (ironclaw_loop_support): new memory_context_source: Option<Arc<dyn MemoryPromptContextService>> field + with_memory_context_source setter. load_loop_context derives the search query from the most recent user-authored message in the loaded context window (per MemoryPromptContextRequest's own doc contract) and skips the lookup entirely when there's no user message — same graceful-degrade shape as the skill/identity None branches.
  • RebornLoopDriverHostFactory (ironclaw_reborn): same Option field/setter, wired into ThreadBackedLoopContextPort construction right next to the existing skill/identity conditionals.
  • DefaultPlannedRuntimeParts (ironclaw_reborn::runtime): mandatory Arc<dyn MemoryPromptContextService> field (mirrors identity_context_source's required-with-Empty-fallback shape), unconditionally threaded through build_default_planned_runtime_inner.
  • build_reborn_runtime (ironclaw_reborn_composition::runtime): match local_runtime { Some(rt) => real, None => Empty }, identical in shape to the adjacent identity_context_source/user_profile_source block. The real branch composes ProductionMemoryPromptContextService over a fresh NativeMemoryService backed by the same local_runtime.extension_filesystem MemoryBackedUserProfileSource already reads from — so a builtin.memory_write and this recall path see the same backing store. The production-graph path (local_runtime: None) stays Empty, matching the pre-existing, already-commented precedent that identity/profile sources are deferred there too — not a new gap this PR introduces.

Profile preservation

Every existing composition profile keeps its exact current behavior: Empty in local_runtime: None case is unchanged, and the Some case only adds new (previously-absent) behavior. ~8 existing DefaultPlannedRuntimeParts test-literal call sites were updated to explicitly supply Arc::new(EmptyMemoryPromptContextService), forced by the compiler — mechanical, behavior-preserving.

Test — W4-MEMCTX-ENVELOPE (int tier)

New tests/integration/memory_prompt_context.rs, driven through a new memory_context_tools() harness group (mirrors profile_tools(), since it needs the same real local-dev filesystem local_dev_profile_filesystem_for_test() exposes):

  • Seeds a memory doc via builtin.memory_write on one thread, then on a fresh thread (no tool call) asserts the captured system prompt contains Untrusted memory content: and the seeded marker — proving the envelope reaches the model on every turn, not only when a memory tool is explicitly called.
  • Non-vacuity guard: an unwritten marker must be absent.
  • Sanitization: a document containing a real instruction-hijack marker (ignore previous instructions) is dropped entirely by sanitize_snippet_text rather than escaped — asserted by confirming its content never surfaces, enveloped or otherwise.
  • Mutation-verified: temporarily forced the wiring back to EmptyMemoryPromptContextService — confirmed RED for the right reason (empty snippets) — then restored, confirmed GREEN.

Gates: cargo fmt clean; cargo clippy --all --benches --tests --examples --all-features — 0 warnings; cargo test -p ironclaw_loop_support --lib — 352 passed; cargo test -p ironclaw_reborn --lib — 301 passed; cargo test -p ironclaw_reborn_composition --lib — 908 passed; touched int suites (reborn_integration_memory_prompt_context, reborn_integration_wiring_parity, reborn_integration_profile, reborn_direct_chat_user_scope_isolation_parity, reborn_response_order_parity) all green; crate-tier test binaries at every touched call site (ironclaw_product_workflow::inbound_turn_contract, ironclaw_reborn::loop_driver_host, ironclaw_reborn_composition::product_live_adapters) all green.

Closes #5605

🤖 Generated with Claude Code

henrypark133 and others added 2 commits July 6, 2026 15:14
ProductionMemoryPromptContextService existed but nothing composed it;
ThreadBackedLoopContextPort hardcoded memory_snippets: Vec::new(), so the
Untrusted memory content: envelope never reached a real system prompt.

Mirrors the skill_context_source/identity_context_source sibling shape at
every layer: Option<Arc<dyn MemoryPromptContextService>> + setter on
ThreadBackedLoopContextPort and RebornLoopDriverHostFactory, mandatory
Arc<dyn MemoryPromptContextService> (Empty-fallback) on
DefaultPlannedRuntimeParts, and a Some(local_runtime)/None match in
build_reborn_runtime identical in shape to the adjacent identity/profile
sources — production-graph path stays Empty for the same reason those do.

Adds the W4-MEMCTX-ENVELOPE integration test: seeds memory via
builtin.memory_write, then asserts a later turn's captured system prompt
contains the Untrusted memory content: envelope and the seeded marker;
asserts a snippet containing an instruction-hijack marker is dropped by
sanitize_snippet_text instead of surfaced. New memory_context_tools()
harness profile mirrors profile_tools() (real filesystem-backed
MemoryPromptContextService instead of Empty).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 6, 2026 22:17
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@ironloopai

ironloopai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

⏳ IronLoop Review Status

Head: 467a12963f9980c26dba4838ef8915e0f2896682
Result: No reviewer jobs are scheduled yet.
Next: Run @ironloopai review to start reviewers.
Updated: 2026-07-07T22:37:34.327Z

Current reviewers:

Reviewer State Verdict Findings Last update
none Queued N/A No reviewer jobs scheduled yet. N/A
Reviewer summaries
Reviewer Detail
none No reviewer jobs scheduled yet.
Recent activity
Time Reviewer State Detail
N/A N/A Waiting No progress events recorded yet.
Available commands
  • @ironloopai agents
  • @ironloopai review
  • @ironloopai review --agent <agent-id-or-alias>
  • @ironloopai status
Run metadata

Admission: webhook accepted the request and IronLoop persisted review 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.

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5742 July 6, 2026 22:18 Destroyed
@github-actions github-actions Bot added scope: dependencies Dependency updates size: M 50-199 changed lines risk: medium Business logic, config, or moderate-risk modules contributor: core 20+ merged PRs labels Jul 6, 2026
@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added memory-aware context recall so saved memory snippets can be injected into prompts during relevant conversations.
    • Expanded end-to-end/integration coverage with new memory-context tooling and scenarios for memory write/read-back behavior.
  • Bug Fixes
    • Improved prompt safety by preventing hijack-marker style instruction snippets from being surfaced.
    • Made memory recall more resilient by falling back gracefully when memory backends fail.
    • Enhanced full-text literal-phrase search so quoted/special-character phrases are handled predictably (and empty phrases match nothing).

Walkthrough

Wires memory_context_source through loop-context loading, Reborn runtime construction, production composition, and integration harnesses. Adds memory-recall tests that verify recalled snippets reach the system prompt with sanitization, plus parity updates and existing test call-site fixes for the new runtime field.

Changes

Memory prompt-context injection

Layer / File(s) Summary
Loop context memory recall
crates/ironclaw_loop_support/src/lib.rs, crates/ironclaw_loop_support/src/memory_context.rs, crates/ironclaw_loop_support/tests/thread_loop_support_contract.rs
Adds memory_context_source, bounded query construction from the latest user message, snippet loading into LoopContextBundle, and unit/contract tests for actor selection, error fallback, and query handling.
Host factory and runtime plumbing
crates/ironclaw_reborn/src/loop_driver_host.rs, crates/ironclaw_reborn/src/runtime.rs
Adds memory_context_source to the Reborn host factory and planned runtime parts, then wires it into ThreadBackedLoopContextPort.
Production memory services and FTS phrase filters
crates/ironclaw_reborn_composition/Cargo.toml, crates/ironclaw_reborn_composition/src/runtime.rs, crates/ironclaw_reborn_composition/src/test_support/*, crates/ironclaw_filesystem/src/*, crates/ironclaw_memory_native/src/*
Adds memory crate dependencies, composes production/empty memory context services, exposes a test-support helper, and adds literal-phrase full-text search support across memory storage and retrieval.
Integration harness memory tools
tests/integration/support/harness/profiles/memory_context.rs, tests/integration/support/harness/profiles/mod.rs, tests/integration/support/group_constructors.rs, tests/integration/support/group.rs, tests/integration/support/harness/recorder.rs, tests/integration/wiring_parity.rs, tests/integration/support/planned_runtime_parts_shape.rs, crates/ironclaw_product_workflow/tests/*, crates/ironclaw_reborn/tests/loop_driver_host.rs, crates/ironclaw_reborn_composition/tests/product_live_adapters.rs, tests/support/reborn_parity_qa/binary_e2e.rs
Adds the memory-context profile/group wiring, shared runtime hookup, recorder filesystem accessor rename, shape/parity assertions, and existing runtime call-site updates for the new field.
End-to-end memory prompt-context test
Cargo.toml, tests/integration/memory_prompt_context.rs, crates/ironclaw_reborn_composition/tests/runtime.rs
Adds the integration test target and runtime-level coverage that writes memory, captures the reader system prompt, and verifies recalled snippets appear with sanitization.

Estimated code review effort: 4 (Complex) | ~50 minutes

Possibly related issues

Possibly related PRs

  • nearai/ironclaw#5163: Directly related memory_context_source wiring and MemoryPromptContextService usage in loop-context loading.
  • nearai/ironclaw#5642: Updates the same planned-runtime shape/parity tripwires for the new field.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commits style and accurately summarizes the Reborn memory prompt-context wiring change.
Description check ✅ Passed The description covers the root cause, implementation shape, validation, and linked issue closure; missing template sections are non-critical.
Linked Issues check ✅ Passed The PR wires the memory context source through Reborn runtime and adds envelope-focused integration coverage, satisfying #5605.
Out of Scope Changes check ✅ Passed The added FtsPhrase/search plumbing and harness updates support the memory-context feature and do not appear unrelated.

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.

@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: a0f21ddf3cc5e9de2d20061990ce85ae04824c13

Run details

Status: Current
Needs human: no
Needs validation: no

**Inline candidates:** 0

Summary

No concrete, actionable regressions found in the memory prompt-context wiring or its integration 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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0f21ddf3c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/ironclaw_loop_support/src/lib.rs Outdated
Comment on lines +404 to +406
let actor = self.run_context.actor().cloned().unwrap_or_else(|| {
TurnActor::new(self.run_context.scope.to_resource_scope().user_id)
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use the explicit thread owner for memory context lookups

For explicit-owner turns such as shared Slack/team routes, capability execution scopes memory to scope.explicit_owner_user_id() before falling back to the actor, but this lookup always prefers run_context.actor(). When the authenticated actor differs from the explicit thread owner, prompt-context recall searches the actor's memory rather than the conversation owner's memory, so the turn can miss the owner's saved memories and inject another user's memory into this system prompt.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed: memory recall now prefers scope.explicit_owner_user_id() over run_context.actor(), falling back to the actor only when there's no explicit owner. Added thread_context_port_accepts_explicit_owner_with_distinct_actor's new assertion (a spy source capturing the scoped actor) — mutation-verified RED without the fix.

@railway-app

railway-app Bot commented Jul 6, 2026 •

Copy link
Copy Markdown

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

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 7, 2026 at 10:46 pm

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review (multi-agent)

Intent: Wire Reborn memory prompt-context into production runtime and add integration coverage proving untrusted memory envelope reaches system prompts.

Stats: 5 findings (from 8 raw, 5 after dedupe/filter) across 4 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0.

Duplicate suppression: the explicit-owner memory lookup leak is already covered by the live unresolved thread on crates/ironclaw_loop_support/src/lib.rs:406, so I did not repost it.

Security

  1. Medium User message is used as an unbounded memory search query (crates/ironclaw_loop_support/src/lib.rs:1885-1891, confidence 75) — anchor: crates/ironclaw_loop_support/src/lib.rs:1890
    latest_user_message_query returns the full trimmed user message and the new prompt-memory path sends it to FTS on every turn. A large or pathological message can force expensive backend work or backend errors before the model runs.

Tests

  1. Medium Memory context lookup errors are not tested (crates/ironclaw_loop_support/src/lib.rs:407-419, confidence 100) — anchor: crates/ironclaw_loop_support/src/lib.rs:407
    The new .await? path propagates MemoryPromptContextService failures, but current tests only cover Empty or successful sources. Also flagged by: performance/Medium.

  2. Medium Production memory-context composition is not directly tested (crates/ironclaw_reborn_composition/src/runtime.rs:3499-3508, confidence 75) — anchor: crates/ironclaw_reborn_composition/src/runtime.rs:3499
    The new integration test wires memory through the harness helper, not through build_reborn_runtime's local_runtime: Some branch, so a production-composition regression to Empty would not be caught.

Conventions

  1. Medium Required memory context is still optional in host factory (crates/ironclaw_reborn/src/loop_driver_host.rs:966-966, confidence 100) — anchor: .claude/rules/architecture.md:64
    The factory adds an Option<Arc<dyn MemoryPromptContextService>> plus a production-invoked builder while the runtime parts type already treats the dependency as required. The architecture rule says production-wired runtime dependencies should not stay optional unless they are genuinely optional.

Local Patterns

  1. Nit Memory wiring hides behind a profile-named filesystem accessor (tests/integration/support/group.rs:803-805, confidence 50) — anchor: tests/integration/support/harness/recorder.rs:45
    The memory-context harness now uses capability_recorder.profile_filesystem(), whose name/docs describe the E-PROFILE seam, making this memory path harder to discover.

Comment thread crates/ironclaw_loop_support/src/lib.rs Outdated
.iter()
.rev()
.find(|message| message.kind == MessageKind::User)
.map(|message| message.content.trim().to_string())

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — User message is used as an unbounded memory search query.

latest_user_message_query returns the entire attacker-controlled user message and the new prompt-memory path sends it into memory search on every turn. MemorySearchRequest only rejects empty queries, so a very large or pathological message can force expensive FTS work or backend errors before the model runs.

Fix: Normalize and cap the recall query to a small byte/token budget before invoking memory search, and treat memory lookup failures as no snippets rather than failing the turn.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed: memory_search_query_from_message bounds the message to MEMORY_QUERY_MAX_CHARS (512) chars and quotes it as a single literal FTS5 phrase so query-syntax metacharacters can't be parsed as backend filters. New unit tests cover quoting, embedded-quote escaping, and length bound — mutation-verified RED.

Comment thread crates/ironclaw_loop_support/src/lib.rs Outdated
.context_profile_id
.clone(),
})
.await?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Memory context lookup errors are not tested.

This new .await? means a MemoryPromptContextService::load_memory_snippets failure aborts context loading, but the adjacent loop-support tests and the new integration test only cover Empty or successful memory context. That leaves the intended error behavior unpinned.

Fix: Add a ThreadBackedLoopContextPort test with a memory source that returns AgentLoopHostError::Unavailable, and assert the intended behavior, preferably graceful no-snippets fallback for prompt-context recall.

Also flagged by: performance/Medium

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed, and confirmed live in CI: memory recall now degrades to no snippets on a backend error instead of .await? aborting the whole turn — this was the actual root cause of several CI failures (group/root/composition tests, the WebUI v2 E2E smoke, QA recorded-behavior replay all died driver_unavailable under real backend contention before this fix). Added thread_context_port_degrades_to_no_snippets_when_memory_context_source_errors with an always-erroring source; mutation-verified RED against the old fail-closed behavior.

as Arc<dyn ironclaw_filesystem::RootFilesystem>,
None,
));
Arc::new(ProductionMemoryPromptContextService::new(memory_service))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Production memory-context composition is not directly tested.

The new integration test drives RebornIntegrationGroup, whose harness wires memory_context_source through build_memory_context_source_for_test, not through this build_reborn_runtime local_runtime: Some branch. A regression that leaves production composition on EmptyMemoryPromptContextService would not be caught.

Fix: Add a composition/runtime test that builds through build_reborn_runtime(local_dev) and proves the composed memory source reads from the same local runtime memory backing store used by memory_write.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added build_reborn_runtime_wires_memory_context_source_when_local_dev in crates/ironclaw_reborn_composition/tests/runtime.rs: drives build_reborn_runtime's own Some(local_runtime) match arm directly (real builtin.memory_write on one conversation, recall asserted via the captured system prompt on a second) rather than the harness's hand-mirrored test-support source. Mutation-verified RED by temporarily forcing that arm to Empty.

event_subscription: Option<EventTriggeredHookSubscription>,
safety_context: InstructionSafetyContext,
identity_context_source: Option<Arc<dyn HostIdentityContextSource>>,
memory_context_source: Option<Arc<dyn MemoryPromptContextService>>,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Required memory context is still optional in host factory.

This adds memory_context_source: Option<Arc<dyn MemoryPromptContextService>> plus a with_memory_context_source builder, while DefaultPlannedRuntimeParts makes the same dependency required and production composition always calls the builder. .claude/rules/architecture.md says Option<Arc<...>> on a runtime struct is only allowed for genuinely optional dependencies.

Fix: Make memory_context_source required on the factory/context adapter, default helper tests to EmptyMemoryPromptContextService, and remove the dead no-memory branch, or split the type if a true no-memory factory remains legitimate.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed: memory_context_source on RebornLoopDriverHostFactory is now Arc<dyn MemoryPromptContextService> (mandatory), defaulting to EmptyMemoryPromptContextService in new(), matching user_profile_source's shape; the dead None branch in build is gone.

Comment thread tests/integration/support/group.rs Outdated
// other backends fall back to `EmptyMemoryPromptContextService`.
memory_context_source:
ironclaw_reborn_composition::test_support::build_memory_context_source_for_test(
capability_recorder.profile_filesystem(),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Nit — Memory wiring hides behind a profile-named filesystem accessor.

The memory-context harness now consumes capability_recorder.profile_filesystem(), but that accessor name/docs describe the E-PROFILE user-profile seam. Reusing it for memory recall makes this new path harder to find now that the same raw memory filesystem backs more than profile tests.

Fix: Add or rename to a neutral local-dev memory filesystem accessor, update the docs for both profile and memory-context consumers, and keep the profile-specific alias only where it helps profile tests.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Renamed the recorder's accessor from profile_filesystem() to local_dev_filesystem() and updated its doc to describe both the E-PROFILE and memory-context consumers that now share the one backing store.

henrypark133 and others added 3 commits July 6, 2026 16:12
…ery bound, fail-open recall, production coverage

Codex P1: memory recall now keys off the thread's explicit owner before
falling back to the run actor, so shared/explicit-owner threads no longer
risk recalling another user's memory.

Bounds the recall query derived from the raw user message to
MEMORY_QUERY_MAX_CHARS and quotes it as a single literal FTS5 phrase, so
conversational query-syntax metacharacters can't be parsed as backend
filters and an oversized message can't force unbounded FTS work.

Memory recall is best-effort prompt enrichment, unlike the skill/identity
sources it sits next to: a `MemoryPromptContextService` error now degrades
to no snippets instead of aborting the whole turn via `.await?`. This was
live in CI, not just theoretical — every local-dev-composed test now runs
the real memory backend on each turn with a user message, and any backend
hiccup was failing turns with `driver_unavailable` (group/root/composition
tests, the WebUI v2 E2E smoke, and the QA recorded-behavior replay). Fixed
the failing/errored-arm test to assert the correct degrade behavior.

Adds a composition-tier test that drives build_reborn_runtime's own
`Some(local_runtime)` match arm directly (a real memory_write on one
conversation, recall asserted in a second), since the existing int-tier
harness test only drove a hand-mirrored test-support source.

Makes the host-factory's memory_context_source field mandatory
(Arc<dyn MemoryPromptContextService>, defaulting to
EmptyMemoryPromptContextService) instead of Option, matching the
user_profile_source shape and removing a dead always-Some-in-production
branch.

Renames the harness's profile_filesystem() accessor to local_dev_filesystem()
now that both the E-PROFILE and memory-context test consumers share it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5742 July 7, 2026 02:40 Destroyed
@ironloopai

ironloopai Bot commented Jul 7, 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 ❌ Changes requested
Findings 1 blocking / 0 notes
Reviewed head a9a43b59912b
Archived summary

Found one blocking regression in the new memory-context query construction: it quotes every user message as an FTS5 phrase, which is not compatible with the in-memory filesystem FTS implementation used by no-libsql/local test compositions and prevents recalled snippets from matching there.

Archived findings
  1. ❌ [MEDIUM] Quoted memory queries do not match the in-memory FTS backend - crates/ironclaw_loop_support/src/lib.rs:1935

@github-actions github-actions Bot added size: L 200-499 changed lines and removed size: M 50-199 changed lines labels Jul 7, 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: a9a43b59912b2346a0cdf82e3906316d48c7f20e

Run details

Status: Current
Needs human: no
Needs validation: no

**Inline candidates:** 1

Summary

Found one blocking regression in the new memory-context query construction: it quotes every user message as an FTS5 phrase, which is not compatible with the in-memory filesystem FTS implementation used by no-libsql/local test compositions and prevents recalled snippets from matching there.

Findings

1. ❌ [MEDIUM] Quoted memory queries do not match the in-memory FTS backend

Location: crates/ironclaw_loop_support/src/lib.rs:1935
memory_search_query_from_message wraps the entire user message in quotes before dispatching to the memory backend. That is FTS5-specific syntax, but the in-memory RootFilesystem backend implements Filter::Fts by splitting the query on whitespace and requiring each token to appear literally. A query like "shipment tracking id" becomes tokens such as "shipment and id", so it will not match stored text without quotes. This breaks memory recall in no-libsql/local in-memory compositions, including the new production-wiring test when ironclaw_reborn_composition is tested without the libsql feature. Keep the backend-neutral query unquoted for backends that do not parse FTS5, or move escaping into the libSQL translator/backend layer.

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.

Comment thread crates/ironclaw_loop_support/src/lib.rs Outdated
@github-actions

github-actions Bot commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

Coverage ratchet

Ratchet mode: ENFORCING

RATCHET PASS: global
  observed: 85.36% (279440 / 327383 lines)
  floor:    85.3% (tolerance 0.5pp -> effective floor 84.8%)
  denominator: 327383 lines now vs 320188 at floor capture (+7195 lines, +2.25%) — not a material change

⚠️ 2 Reborn crate(s) have 0 int-tier coverage (target: 0) — ironclaw_scripts, ironclaw_skill_learning

Reborn integration-tier coverage

Line coverage (Reborn crates): 85.36% — 279440 / 327383 lines

Per-crate breakdown (65 crates, lowest-covered first)
Crate Line % Covered / Total
ironclaw_scripts 0% 0 / 347
ironclaw_skill_learning 0% 0 / 61
ironclaw_wasm_sandbox_core 7.37% 7 / 95
ironclaw_runtime_policy 33.2% 80 / 241
ironclaw_event_projections 43.34% 673 / 1553
ironclaw_run_state 52.36% 222 / 424
ironclaw_authorization 53.54% 461 / 861
ironclaw_triggers 60.32% 1736 / 2878
ironclaw_prompt_envelope 61.36% 54 / 88
ironclaw_observability 61.54% 16 / 26
ironclaw_mcp 63.15% 581 / 920
ironclaw_reborn_cli 64.58% 3988 / 6175
ironclaw_webui_v2 66.3% 2439 / 3679
ironclaw_filesystem 66.38% 3678 / 5541
ironclaw_reborn_migration 67.01% 1172 / 1749
ironclaw_dispatcher 67.15% 92 / 137
ironclaw_memory 70.89% 789 / 1113
ironclaw_trust 72.88% 661 / 907
ironclaw_reborn_event_store 73.01% 944 / 1293
ironclaw_capabilities 74.08% 1658 / 2238
ironclaw_wasm_limiter 74.6% 47 / 63
ironclaw_extractors 74.72% 538 / 720
ironclaw_first_party_extensions 77.62% 5410 / 6970
ironclaw_llm 77.88% 19479 / 25013
ironclaw_product_context 78.57% 11 / 14
ironclaw_wasm_product_adapters 80.58% 1510 / 1874
ironclaw_process_sandbox 80.65% 671 / 832
ironclaw_reborn_openai_compat 80.95% 956 / 1181
ironclaw_memory_native 81.89% 3234 / 3949
ironclaw_secrets 82.33% 2716 / 3299
ironclaw_wasm 82.54% 950 / 1151
ironclaw_events 83.47% 1762 / 2111
ironclaw_processes 84.06% 965 / 1148
ironclaw_turns 84.14% 13061 / 15523
ironclaw_host_api 84.8% 3131 / 3692
ironclaw_threads 85.16% 3278 / 3849
ironclaw_projects 85.92% 659 / 767
ironclaw_network 86.12% 670 / 778
ironclaw_auth 86.32% 2727 / 3159
ironclaw_product_workflow 86.33% 10709 / 12405
ironclaw_common 86.59% 1472 / 1700
ironclaw_slack_v2_adapter 86.79% 1806 / 2081
ironclaw_reborn_config 86.98% 1730 / 1989
ironclaw_product_adapters 87.29% 3207 / 3674
ironclaw_skills 87.34% 4334 / 4962
ironclaw_reborn_traces 87.35% 10325 / 11820
ironclaw_hooks 87.84% 9916 / 11289
ironclaw_product_adapter_registry 87.96% 526 / 598
ironclaw_extensions 88.26% 2631 / 2981
ironclaw_reborn_identity 88.43% 344 / 389
ironclaw_reborn_composition 89.11% 69044 / 77479
ironclaw_host_runtime 89.13% 17302 / 19413
ironclaw_conversations 90% 2924 / 3249
ironclaw_approvals 90.51% 1507 / 1665
ironclaw_reborn 91.19% 17488 / 19177
ironclaw_event_streams 91.48% 1009 / 1103
ironclaw_reborn_webui_ingress 91.68% 2094 / 2284
ironclaw_loop_support 92.25% 14292 / 15493
ironclaw_attachments 93.06% 630 / 677
ironclaw_telegram_v2_adapter 94.01% 2447 / 2603
ironclaw_resources 94.2% 3605 / 3827
ironclaw_agent_loop 94.54% 8753 / 9259
ironclaw_safety 94.81% 3669 / 3870
ironclaw_first_party_extension_ports 95% 3094 / 3257
ironclaw_outbound 95.59% 3556 / 3720

This table itself is informational and never gates the PR on its own — not the percentage, not the per-crate holes, not the 0-coverage callout. A separate coverage ratchet (dry-run until enforce=true; see tests/integration/coverage-floor.toml) can fail the build on specific configured floors.

Exemptions (4 entry/entries excluded from the accounting above)
Module / Crate Reason Issue
crate: ironclaw_embeddings v1-only: consumed only by root ironclaw (src/app.rs, src/tools/builtin/memory.rs, src/workspace/mod.rs, src/config/{mod,embeddings}.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_gateway v1-only: consumed only by root ironclaw (src/channels/web/platform/static_files.rs, src/channels/web/handlers/frontend.rs); no crates/* dependents. Covered by "Tests (Legacy)". #5657
crate: ironclaw_oauth v1-only: consumed only by root ironclaw (src/auth/oauth.rs); no crates/* dependents. Crate's own doc comment confirms v1-only. Covered by "Tests (Legacy)". #5657
crate: ironclaw_tui v1-only: consumed only by root ironclaw (src/main.rs, src/channels/tui.rs); no crates/* dependents. Crate's own doc comment confirms it bridges INTO v1, not Reborn. Covered by "Tests (Legacy)". #5657

henrypark133 and others added 2 commits July 6, 2026 20:34
… matcher

The bounded, quoted memory-search query loop-support sends now wraps
content in a literal `"..."` phrase. The in-memory backend's naive FTS
approximation was whitespace-splitting that unconditionally, so the
leading/trailing quote characters landed on the first/last token and
almost never matched stored text — breaking memory recall for local-dev
deployments. Recognize a whole-query double-quoted phrase and match it
as literal (unescaped) substring content instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 7, 2026 03:55
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5742 July 7, 2026 03:55 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 temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5742 July 7, 2026 04:39 Destroyed

@henrypark133 henrypark133 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review (multi-agent)

Intent: Wire Reborn memory prompt-context into production composition and add integration coverage for untrusted-memory envelope recall.

Stats: 6 findings (from 11 raw, 6 after dedup/filter) across 2 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, approach. Reviewers failed: none. Body-only: 0. Suppressed duplicate live thread: 1 existing composition coverage thread already had an author reply.

bugs

  1. Low Empty quoted FTS query matches every in-memory row (crates/ironclaw_filesystem/src/in_memory.rs:577-578, confidence 75) — anchor: crates/ironclaw_filesystem/src/in_memory.rs:577
    fts5_literal_phrase documents that an empty quoted phrase should return None, but "" is accepted and unescaped to an empty string. fts_naive_matches then calls stored_lower.contains(""), which is true for every stored text, so an empty quoted FTS query returns all records in the in-memory backend instead of falling back/rejecting like the comment and real FTS behavior imply.

approach

  1. Medium Loop context now owns an FTS5 query dialect (crates/ironclaw_loop_support/src/lib.rs:1927-1935, confidence 75) — anchor: crates/ironclaw_filesystem/src/index.rs:292; crates/ironclaw_memory_native/AGENTS.md:18
    ThreadBackedLoopContextPort now turns attacker-controlled message text into an FTS5 double-quoted phrase before calling the memory service, and the PR updates the in-memory filesystem to understand that FTS5 phrase shape. That puts backend query syntax in the loop adapter and generic reference backend even though Filter::Fts is documented as a free-form query each backend translates to its native language, while memory_native owns memory search semantics.

conventions

  1. Medium Do not make always-wired memory source optional (crates/ironclaw_loop_support/src/lib.rs:216-216, confidence 100) — anchor: .claude/rules/architecture.md:64
    The diff adds memory_context_source: Option<Arc<dyn MemoryPromptContextService>> plus a with_memory_context_source builder, but production wiring now owns a required Arc defaulting to EmptyMemoryPromptContextService and always calls the setter before constructing the loop context. That matches the optional-Arc pattern the architecture rule rejects because the None branch is not a real production mode.

maintainability

  1. Medium Move memory context assembly out of lib.rs (crates/ironclaw_loop_support/src/lib.rs:404-453, confidence 75) — anchor: crates/ironclaw_loop_support/CLAUDE.md:29
    The new memory-recall path adds source selection, actor derivation, backend-query shaping, and best-effort error policy directly inside the already-large thread context adapter. This crate already keeps skill and identity context builders in focused sibling modules, and the crate guide says to add one file per host adapter or context source, so future memory prompt changes now require editing the central loop-support file instead of a narrow owner.

tests

  1. Medium Memory recall query selection lacks context-window edge tests (crates/ironclaw_loop_support/src/lib.rs:405-405, confidence 75) — anchor: crates/ironclaw_loop_support/src/lib.rs:405
    The new memory lookup path derives a query from a collection of context messages, but the adjacent tests only exercise a single user message. There is no test proving the source is skipped for summary/assistant-only or whitespace-user windows, or that a multi-message window uses the latest user-authored message rather than an older one.

performance

  1. Low Memory prompt recall over-fetches candidates on every turn (crates/ironclaw_loop_support/src/lib.rs:426-431, confidence 68) — anchor: crates/ironclaw_memory_native/src/service.rs:331
    This new per-turn prompt path asks for 5 memory snippets, but the wired NativeMemoryService::retrieve_context path builds a MemorySearchRequest with the default pre-fusion limit of 50 before returning at most 5 snippets. That makes every model turn fetch and rank 10x more FTS candidates than the loop can admit.

Comment thread crates/ironclaw_loop_support/src/lib.rs Outdated
max_messages: usize,
skill_context_source: Option<Arc<dyn HostSkillContextSource>>,
identity_context_source: Option<Arc<dyn HostIdentityContextSource>>,
memory_context_source: Option<Arc<dyn MemoryPromptContextService>>,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Do not make always-wired memory source optional.

The diff adds memory_context_source: Option<Arc<dyn MemoryPromptContextService>> plus a with_memory_context_source builder, but production wiring now owns a required Arc defaulting to EmptyMemoryPromptContextService and always calls the setter before constructing the loop context. That matches the optional-Arc pattern the architecture rule rejects because the None branch is not a real production mode.

Fix: Store an Arc<dyn MemoryPromptContextService> on ThreadBackedLoopContextPort, default it to EmptyMemoryPromptContextService or pass it through the constructor, and remove the None branch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 57a3852: ThreadBackedLoopContextPort.memory_context_source is now a required Arc<dyn MemoryPromptContextService> defaulting to EmptyMemoryPromptContextService in new(); with_memory_context_source is a plain setter and the dead None branch is gone, matching user_profile_source's shape.

Comment thread crates/ironclaw_loop_support/src/lib.rs Outdated
/// metacharacters in ordinary conversational text — hyphens, colons, commas —
/// can never be parsed as column filters or boolean operators by the
/// backend's full-text index; they land as literal phrase content instead.
fn memory_search_query_from_message(content: &str) -> String {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Loop context now owns an FTS5 query dialect.

ThreadBackedLoopContextPort now turns attacker-controlled message text into an FTS5 double-quoted phrase before calling the memory service, and the PR updates the in-memory filesystem to understand that FTS5 phrase shape. That puts backend query syntax in the loop adapter and generic reference backend even though Filter::Fts is documented as a free-form query each backend translates to its native language, while memory_native owns memory search semantics.

Fix: Pass bounded raw text, or a memory-owned literal query type, into the memory service and keep FTS5/libSQL/PostgreSQL escaping in the memory-native or filesystem backend translators.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Confirmed and restructured in 467a129 — you were right that the loop adapter owned backend dialect. Rather than moving the escaping to another string-shaping layer, literalness is now typed: new Filter::FtsPhrase { key, phrase } variant in ironclaw_filesystem, rendered natively per backend — libSQL owns the FTS5 double-quoted-phrase escaping in its translator, PostgreSQL uses phraseto_tsquery, in-memory does plain substring match (its hand-rolled fts5_literal_phrase parser is deleted entirely). MemorySearchRequest gained literal_phrase; retrieve_context sets it, search() keeps free-form Fts semantics untouched. memory_search_query_from_message is now bounding-only with zero dialect knowledge. Contract tests extended on real libSQL FTS5 + live Postgres with metacharacter/embedded-quote/empty-phrase cases; a facade test pins retrieve-vs-search divergence (reordered tokens excluded by literal phrase, matched by free-form).

Comment thread crates/ironclaw_loop_support/src/lib.rs Outdated
}
None => Vec::new(),
};
let memory_snippets = match self.memory_context_source.as_deref() {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Move memory context assembly out of lib.rs.

The new memory-recall path adds source selection, actor derivation, backend-query shaping, and best-effort error policy directly inside the already-large thread context adapter. This crate already keeps skill and identity context builders in focused sibling modules, and the crate guide says to add one file per host adapter or context source, so future memory prompt changes now require editing the central loop-support file instead of a narrow owner.

Fix: Create a focused memory_context.rs in ironclaw_loop_support for latest-user query selection, actor selection, query shaping, and the MemoryPromptContextRequest call; have load_loop_context call one helper that returns snippets.

Also flagged by: conventions/Medium

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 467a129: extracted crates/ironclaw_loop_support/src/memory_context.rs owning the whole memory-snippets step — latest-user query derivation, explicit-owner actor resolution, best-effort degrade-to-empty error policy, the query bound constants, and their unit tests. lib.rs call site is 6 lines; file went 2282 → 2175 lines.

Comment thread crates/ironclaw_loop_support/src/lib.rs Outdated
None => Vec::new(),
};
let memory_snippets = match self.memory_context_source.as_deref() {
Some(source) => match latest_user_message_query(&context.messages) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Medium — Memory recall query selection lacks context-window edge tests.

The new memory lookup path derives a query from a collection of context messages, but the adjacent tests only exercise a single user message. There is no test proving the source is skipped for summary/assistant-only or whitespace-user windows, or that a multi-message window uses the latest user-authored message rather than an older one.

Fix: Add thread_context_port_memory_query_uses_latest_user_and_skips_without_user covering multi-message latest-user selection and no-user/blank-user context windows.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 57a3852: one consolidated test thread_context_port_memory_query_uses_latest_user_and_skips_without_user (thread_loop_support_contract.rs) with a query-capturing spy source covers latest-user selection across a [user, assistant, user] window, an assistant-only window skipping recall, and a whitespace-only user message skipping recall. Production latest_user_message_query was already correct — the red run exposed a test-setup error, not a code bug, so only the test scenario was adjusted.

/// literal content (embedded `""` collapsed to `"`). `None` for anything
/// else, including an empty or unterminated quote.
fn fts5_literal_phrase(query: &str) -> Option<String> {
let inner = query.strip_prefix('"')?.strip_suffix('"')?;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Low — Empty quoted FTS query matches every in-memory row.

fts5_literal_phrase documents that an empty quoted phrase should return None, but "" is accepted and unescaped to an empty string. fts_naive_matches then calls stored_lower.contains(""), which is true for every stored text, so an empty quoted FTS query returns all records in the in-memory backend instead of falling back/rejecting like the comment and real FTS behavior imply.

Fix: Reject empty phrase contents before returning from fts5_literal_phrase and add the adjacent empty/unterminated quoted-phrase test.

Also flagged by: tests/Medium, local-patterns/Low

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 57a3852 (empty phrase returns None, red→green), then the whole parser was deleted in 467a129 — the FTS5-phrase shape no longer reaches the in-memory backend at all (typed Filter::FtsPhrase instead). The empty-phrase-matches-nothing invariant is re-pinned at the typed seam in fts_phrase_filter_matches_literal_content plus the cross-backend contract tests.

Comment thread crates/ironclaw_loop_support/src/lib.rs Outdated
// a backend hiccup (contention, transient unavailability)
// degrades to no snippets rather than failing the turn.
match source
.load_memory_snippets(MemoryPromptContextRequest {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Low — Memory prompt recall over-fetches candidates on every turn.

This new per-turn prompt path asks for 5 memory snippets, but the wired NativeMemoryService::retrieve_context path builds a MemorySearchRequest with the default pre-fusion limit of 50 before returning at most 5 snippets. That makes every model turn fetch and rank 10x more FTS candidates than the loop can admit.

Fix: Set pre_fusion_limit for retrieve_context to a small bounded value derived from max_snippets, mirroring the search path's explicit cap, before disabling vector search.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 57a3852: retrieve_context now sets pre_fusion_limit to max_snippets.saturating_mul(2) (10 for the default 5-snippet prompt path) instead of inheriting the 50-candidate default, mirroring search()'s explicit cap. Pinned by native_context_retrieve_bounds_pre_fusion_limit_relative_to_max_snippets with a spy backend capturing the request limit (red showed 50, green shows 10).

…ion edge tests, empty-phrase FTS guard, bounded pre-fusion limit

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 7, 2026 22:11
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5742 July 7, 2026 22:11 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.

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

♻️ Duplicate comments (1)
crates/ironclaw_loop_support/src/lib.rs (1)

405-452: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Two outstanding architecture comments from prior rounds still apply here.

  1. Memory-recall assembly (query derivation, actor precedence, service call, error policy) stays inline in load_loop_context, while skill/identity context building already delegate to skill_context::/identity_context:: modules. Per coding guidelines for crates/ironclaw_loop_support/src/**/*.rs: "Add one file per host adapter or context source." This block should move to a dedicated memory_context module mirroring the existing pattern.
  2. memory_search_query_from_message still bakes FTS5 double-quote phrase escaping into this generic loop-support adapter and hands it straight to MemoryPromptContextRequest.query, coupling the host-agnostic loop port to one backend's query dialect (the in-memory backend was subsequently made FTS5-aware to compensate, rather than moving the dialect ownership to memory-native/backend translators).

Both were raised in earlier review rounds without a recorded resolution, and the shipped code still matches the originally reported shape.

🤖 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 405 - 452, The
memory-recall logic in load_loop_context should be extracted out of the inline
match block into a dedicated memory_context module, following the same
host-adapter split already used for skill_context and identity_context. Move the
query derivation, actor precedence, load_memory_snippets call, and
fallback/error handling into that module, and keep the loop-support adapter
focused on orchestration. Also remove the FTS5-specific quote escaping from
memory_search_query_from_message and push any backend/query-dialect handling
into memory-native/backend translators so MemoryPromptContextRequest.query stays
backend-agnostic.

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.

Duplicate comments:
In `@crates/ironclaw_loop_support/src/lib.rs`:
- Around line 405-452: The memory-recall logic in load_loop_context should be
extracted out of the inline match block into a dedicated memory_context module,
following the same host-adapter split already used for skill_context and
identity_context. Move the query derivation, actor precedence,
load_memory_snippets call, and fallback/error handling into that module, and
keep the loop-support adapter focused on orchestration. Also remove the
FTS5-specific quote escaping from memory_search_query_from_message and push any
backend/query-dialect handling into memory-native/backend translators so
MemoryPromptContextRequest.query stays backend-agnostic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9ad80ce2-2b5a-4520-8019-e342a06f6305

📥 Commits

Reviewing files that changed from the base of the PR and between 4bad35b and 57a3852.

📒 Files selected for processing (11)
  • Cargo.toml
  • crates/ironclaw_filesystem/src/in_memory.rs
  • crates/ironclaw_loop_support/src/lib.rs
  • crates/ironclaw_loop_support/tests/thread_loop_support_contract.rs
  • crates/ironclaw_memory_native/src/service.rs
  • crates/ironclaw_memory_native/tests/memory_service_facade.rs
  • crates/ironclaw_reborn/src/runtime.rs
  • crates/ironclaw_reborn_composition/src/runtime.rs
  • tests/integration/support/group.rs
  • tests/integration/support/group_constructors.rs
  • tests/integration/wiring_parity.rs

…ntext module

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5742 July 7, 2026 22:37 Destroyed

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/ironclaw_loop_support/src/lib.rs (1)

365-403: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider parallelizing the three independent context lookups.

skill_context, identity_context, and now memory_context::load_memory_snippets_for_run are three independent async calls awaited sequentially on every turn. Memory recall doesn't depend on skill/identity output (or vice versa), so this is a straightforward tokio::join! candidate to cut per-turn latency added by this new backend round-trip.

⚡ Sketch using tokio::join!
-        let instruction_snippets = match self.skill_context_source.as_deref() {
-            Some(source) => {
-                skill_context::build_skill_instruction_snippets(source, &self.run_context).await?
-            }
-            None => Vec::new(),
-        };
-        let identity_messages = match self.identity_context_source.as_deref() {
-            Some(source) => { ... }
-            None => Vec::new(),
-        };
-        let memory_snippets = memory_context::load_memory_snippets_for_run(
-            &context.messages,
-            &self.run_context,
-            self.memory_context_source.as_ref(),
-        )
-        .await;
+        let skill_fut = async {
+            match self.skill_context_source.as_deref() {
+                Some(source) => {
+                    skill_context::build_skill_instruction_snippets(source, &self.run_context).await
+                }
+                None => Ok(Vec::new()),
+            }
+        };
+        let identity_fut = async { /* wrap existing identity branch, returning Result */ };
+        let memory_fut = memory_context::load_memory_snippets_for_run(
+            &context.messages,
+            &self.run_context,
+            self.memory_context_source.as_ref(),
+        );
+        let (instruction_snippets, identity_result, memory_snippets) =
+            tokio::join!(skill_fut, identity_fut, memory_fut);
+        let instruction_snippets = instruction_snippets?;
+        let identity_messages = identity_result?;
🤖 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 365 - 403, The three
independent context fetches in the run flow are still awaited sequentially,
which adds avoidable latency. Update the logic in the main turn-building path
around skill_context::build_skill_instruction_snippets,
identity_context::build_identity_messages_for_run_detailed, and
memory_context::load_memory_snippets_for_run to run in parallel with
tokio::join! (or equivalent), while preserving the existing per-branch behavior
for None cases and the identity_candidates
initialization/publish_personal_context_admitted flow.
🤖 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_filesystem/src/libsql.rs`:
- Around line 1805-1821: The libsql FTS phrase handling in the Filter::FtsPhrase
arm currently forwards empty phrases to FTS5 without an explicit check, unlike
in_memory.rs. Add a direct phrase.is_empty() guard before calling
fts5_quote_phrase and building the MATCH clause so empty phrases return
FilesystemError::Unsupported or a no-match path consistent with the other
backend behavior. Use the same FtsPhrase handling in libsql.rs (including the
related arm noted in the review) to keep the empty-phrase contract explicit and
aligned with db_root_filesystem_contract.rs.

In `@crates/ironclaw_filesystem/src/postgres.rs`:
- Around line 1623-1634: The FtsPhrase arm in postgres.rs relies on
phraseto_tsquery handling an empty string implicitly, instead of enforcing the
“empty phrase matches nothing” contract in Rust like in_memory.rs does. Add an
explicit guard in the Filter::FtsPhrase branch before building the SQL so empty
phrases short-circuit to a non-matching predicate, and keep the existing
phraseto_tsquery-based SQL path only for non-empty phrases to match libsql.rs
and in_memory.rs behavior consistently.

---

Outside diff comments:
In `@crates/ironclaw_loop_support/src/lib.rs`:
- Around line 365-403: The three independent context fetches in the run flow are
still awaited sequentially, which adds avoidable latency. Update the logic in
the main turn-building path around
skill_context::build_skill_instruction_snippets,
identity_context::build_identity_messages_for_run_detailed, and
memory_context::load_memory_snippets_for_run to run in parallel with
tokio::join! (or equivalent), while preserving the existing per-branch behavior
for None cases and the identity_candidates
initialization/publish_personal_context_admitted flow.
🪄 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: e2faae5f-855e-4acc-b569-367e36992d3c

📥 Commits

Reviewing files that changed from the base of the PR and between 57a3852 and 467a129.

📒 Files selected for processing (11)
  • crates/ironclaw_filesystem/src/in_memory.rs
  • crates/ironclaw_filesystem/src/index.rs
  • crates/ironclaw_filesystem/src/libsql.rs
  • crates/ironclaw_filesystem/src/postgres.rs
  • crates/ironclaw_filesystem/tests/db_root_filesystem_contract.rs
  • crates/ironclaw_loop_support/src/lib.rs
  • crates/ironclaw_loop_support/src/memory_context.rs
  • crates/ironclaw_memory_native/src/repo/filesystem.rs
  • crates/ironclaw_memory_native/src/search.rs
  • crates/ironclaw_memory_native/src/service.rs
  • crates/ironclaw_memory_native/tests/memory_service_facade.rs

Comment on lines +1805 to +1821
Filter::FtsPhrase { key, phrase } => {
let Some(fts_table) = fts_tables.get(key.as_str()) else {
return Err(FilesystemError::Unsupported {
path: path.clone(),
operation: FilesystemOperation::Query,
});
};
// The dialect boundary: free-form caller text becomes FTS5 query
// syntax only here, quoted as a single literal phrase so it can
// never be parsed as column filters or boolean operators.
params.push(libsql::Value::Text(fts5_quote_phrase(phrase)));
out.push_str(&format!(
"(path IN (SELECT path FROM {fts_table} WHERE {fts_table} MATCH ?{}))",
params.len()
));
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.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Empty-phrase "no match" guarantee relies on FTS5 engine behavior, not an explicit guard.

in_memory.rs's FtsPhrase arm explicitly rejects phrase.is_empty() (fixing a prior flagged bug where contains("") matched everything). This libsql arm has no equivalent guard — it always quotes and forwards the phrase to MATCH, relying on SQLite FTS5 to naturally return zero rows for a MATCH '""' empty phrase rather than erroring or matching broadly. The empty-phrase contract test in db_root_filesystem_contract.rs presumably validates current behavior, but making the "empty phrase → no match" contract explicit in Rust (rather than delegated to FTS5's undocumented-for-this-edge-case behavior) would keep the three backends' semantics visibly aligned and immune to any future libsql/SQLite behavior change.

♻️ Proposed defensive guard
         Filter::FtsPhrase { key, phrase } => {
+            if phrase.is_empty() {
+                out.push_str("FALSE");
+                return Ok(());
+            }
             let Some(fts_table) = fts_tables.get(key.as_str()) else {

Since this touches SQLite FTS5 dialect behavior for an edge case, please confirm MATCH '""' reliably returns zero rows (rather than erroring) across the libsql/SQLite version(s) this crate targets.

Also applies to: 1886-1893

🤖 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_filesystem/src/libsql.rs` around lines 1805 - 1821, The
libsql FTS phrase handling in the Filter::FtsPhrase arm currently forwards empty
phrases to FTS5 without an explicit check, unlike in_memory.rs. Add a direct
phrase.is_empty() guard before calling fts5_quote_phrase and building the MATCH
clause so empty phrases return FilesystemError::Unsupported or a no-match path
consistent with the other backend behavior. Use the same FtsPhrase handling in
libsql.rs (including the related arm noted in the review) to keep the
empty-phrase contract explicit and aligned with db_root_filesystem_contract.rs.

Comment on lines +1623 to +1634
Filter::FtsPhrase { key, phrase } => {
// `phraseto_tsquery` requires the lexemes it finds to appear as
// an exact adjacent phrase, giving literal-phrase semantics
// instead of `plainto_tsquery`'s implicit AND-of-terms.
params.push(Box::new(phrase.clone()));
out.push_str(&format!(
"(to_tsvector('english', COALESCE(indexed->>'{}', '')) @@ phraseto_tsquery('english', ${}))",
key.as_str(),
params.len()
));
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.

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Same empty-phrase reliance as libsql — consider an explicit guard for parity.

Mirrors the concern raised on libsql.rs's FtsPhrase arm: this relies on phraseto_tsquery('english', '') reducing to an empty tsquery that never matches via @@, rather than an explicit Rust-level guard. Since in_memory.rs enforces "empty phrase matches nothing" explicitly, doing the same here keeps all three backends' contracts visibly identical instead of two of them depending on FTS-engine-specific empty-input behavior.

🤖 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_filesystem/src/postgres.rs` around lines 1623 - 1634, The
FtsPhrase arm in postgres.rs relies on phraseto_tsquery handling an empty string
implicitly, instead of enforcing the “empty phrase matches nothing” contract in
Rust like in_memory.rs does. Add an explicit guard in the Filter::FtsPhrase
branch before building the SQL so empty phrases short-circuit to a non-matching
predicate, and keep the existing phraseto_tsquery-based SQL path only for
non-empty phrases to match libsql.rs and in_memory.rs behavior consistently.

@henrypark133

Copy link
Copy Markdown
Collaborator Author

Closing as stale — no activity in over three weeks. The branch is untouched; reopen if this is still needed.

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-5742 — 467a1296 Deployed Jul 7, 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 size: L 200-499 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

reborn: memory prompt-context injection is unwired — ProductionMemoryPromptContextService never composed, memory_snippets always empty

2 participants