Skip to content

Fix multi-turn history and hand dependency versions#26

Open
aegntic wants to merge 2 commits into
mainfrom
aegntic/e2e-multi-turn-fix
Open

Fix multi-turn history and hand dependency versions#26
aegntic wants to merge 2 commits into
mainfrom
aegntic/e2e-multi-turn-fix

Conversation

@aegntic

@aegntic aegntic commented Mar 20, 2026

Copy link
Copy Markdown
Owner

Preserves repaired multi-turn session history in both streaming and non-streaming agent loops by replacing the old 20-message trim with repair logging and a higher emergency cap. Adds regression tests to ensure earlier turns survive past twenty messages while still trimming only in true overflow scenarios. Also adds optional minimum-version metadata for binary hand requirements, enforces those checks in the hands registry, and exposes the requirement/version status through the API. Updates the hands setup UI to show required versions and success messaging when the installed tool satisfies the minimum.

Summary by CodeRabbit

Release Notes

  • New Features

    • Hand requirements now support minimum version specification with automatic version validation.
    • Requirement detail displays enhanced to show version constraints and check results.
  • Improvements

    • Agent history management improved with emergency overflow controls and detailed repair statistics logging.

@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds minimum version checking for hand requirements. It introduces a min_version field to requirement specifications, implements version extraction and comparison logic, exposes version requirements through the API, updates the UI to display versions, and refactors the runtime's history management with improved repair statistics tracking.

Changes

Cohort / File(s) Summary
Requirement Version Schema
crates/clawreform-hands/src/lib.rs
Added min_version: Option<String> and version_args: Vec<String> fields to HandRequirement struct with serde defaults. Updated TOML parsing tests to validate min_version handling and backward compatibility.
Version Checking Implementation
crates/clawreform-hands/src/registry.rs
Implemented binary version checking via check_requirement, including version argument invocation, semver extraction, and lexicographic version comparison. Added helper functions for version querying and comparison with comprehensive unit tests.
API & Frontend Exposure
crates/clawreform-api/src/routes.rs, crates/clawreform-api/static/index_body.html, crates/clawreform-api/static/js/pages/hands.js
Exposed min_version field in API JSON responses. Updated HTML to call new getRequirementCheckLabel(req) and getRequirementMetLabel(req) helper functions for dynamic requirement display with version information.
Runtime History Management
crates/clawreform-runtime/src/agent_loop.rs
Replaced fixed MAX_HISTORY_MESSAGES constant with higher EMERGENCY_HISTORY_MESSAGE_LIMIT. Integrated validate_and_repair_with_stats() for detailed repair tracking. Added apply_emergency_history_limit() to trim oldest messages after overflow recovery with updated tests for multi-message history scenarios.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A version to check, a history to keep,
With semver so neat, comparing runs deep,
The registry hops with precision so fine,
While messages trim to the freshest in line!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Fix multi-turn history and hand dependency versions' accurately captures the two main changes: multi-turn history fixes in agent loops and hand dependency version enforcement.
Docstring Coverage ✅ Passed Docstring coverage is 88.57% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch aegntic/e2e-multi-turn-fix
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Tip

CodeRabbit can suggest fixes for GitHub Check annotations.

Configure the reviews.tools.github-checks setting to adjust the time to wait for GitHub Checks to complete.

@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

🧹 Nitpick comments (1)
crates/clawreform-api/static/js/pages/hands.js (1)

212-222: Consider template literals for improved readability.

Both helper functions use string concatenation. Template literals would be more readable and maintainable.

♻️ Optional refactor using template literals
 getRequirementCheckLabel(req) {
   if (!req) return '';
-  if (req.min_version) return req.check_value + ' >= ' + req.min_version;
+  if (req.min_version) return `${req.check_value || ''} >= ${req.min_version}`;
   return req.check_value || '';
 },

 getRequirementMetLabel(req) {
   if (!req) return 'Detected on your system';
-  if (req.min_version) return 'Detected on your system and meets >= ' + req.min_version;
+  if (req.min_version) return `Detected on your system and meets >= ${req.min_version}`;
   return 'Detected on your system';
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/clawreform-api/static/js/pages/hands.js` around lines 212 - 222,
Replace string concatenation in getRequirementCheckLabel and
getRequirementMetLabel with template literals for readability: in
getRequirementCheckLabel use a template like `${req.check_value} >=
${req.min_version}` when req.min_version is present and return
`${req.check_value}` (or empty) otherwise; in getRequirementMetLabel use a
template like `Detected on your system and meets >= ${req.min_version}` when
req.min_version exists and keep the existing fallback string when it does not.
Ensure null/undefined guards (if (!req)) remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@crates/clawreform-api/static/js/pages/hands.js`:
- Around line 212-216: The getRequirementCheckLabel function may emit "undefined
>= x" if req.min_version is set but req.check_value is missing; update
getRequirementCheckLabel to defensively check that req.check_value (or
req.checkValue) is a valid non-null/undefined string before concatenating with
req.min_version and fall back to an empty string or a safe placeholder when
check_value is absent; specifically modify the logic in getRequirementCheckLabel
to test req.check_value (or use String(req.check_value) only when not
null/undefined) before returning "req.check_value + ' >= ' + req.min_version",
otherwise return '' or req.min_version alone as appropriate.

In `@crates/clawreform-hands/src/registry.rs`:
- Around line 317-330: query_binary_version currently uses
std::process::Command::output() and parses stdout/stderr even when the child
exited non-zero and runs synchronously inside async callers like
check_requirements()/list_hands(); update query_binary_version to validate the
child's exit status (return None or an Err when status.success() is false)
before using stdout/stderr, and protect blocking/spawn operations by running the
process under Tokio: either switch to tokio::process::Command and call it inside
tokio::time::timeout(...) with an appropriate duration in the async callers, or
wrap the existing std::process::Command invocation in
tokio::task::spawn_blocking and apply tokio::time::timeout around the spawned
task to avoid wedging the runtime.

---

Nitpick comments:
In `@crates/clawreform-api/static/js/pages/hands.js`:
- Around line 212-222: Replace string concatenation in getRequirementCheckLabel
and getRequirementMetLabel with template literals for readability: in
getRequirementCheckLabel use a template like `${req.check_value} >=
${req.min_version}` when req.min_version is present and return
`${req.check_value}` (or empty) otherwise; in getRequirementMetLabel use a
template like `Detected on your system and meets >= ${req.min_version}` when
req.min_version exists and keep the existing fallback string when it does not.
Ensure null/undefined guards (if (!req)) remain unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dab5c69d-52b8-4076-bbc3-6df461ac7e8c

📥 Commits

Reviewing files that changed from the base of the PR and between 85697b9 and 972c1b3.

📒 Files selected for processing (6)
  • crates/clawreform-api/src/routes.rs
  • crates/clawreform-api/static/index_body.html
  • crates/clawreform-api/static/js/pages/hands.js
  • crates/clawreform-hands/src/lib.rs
  • crates/clawreform-hands/src/registry.rs
  • crates/clawreform-runtime/src/agent_loop.rs

Comment on lines +212 to +216
getRequirementCheckLabel(req) {
if (!req) return '';
if (req.min_version) return req.check_value + ' >= ' + req.min_version;
return req.check_value || '';
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against undefined check_value in version display.

Line 214 concatenates req.check_value without verifying it exists. If req.min_version is truthy but req.check_value is undefined or null, the UI would display "undefined >= 1.2.3" or "null >= 1.2.3".

🛡️ Proposed fix to add defensive check
 getRequirementCheckLabel(req) {
   if (!req) return '';
-  if (req.min_version) return req.check_value + ' >= ' + req.min_version;
+  if (req.min_version) return (req.check_value || '') + ' >= ' + req.min_version;
   return req.check_value || '';
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/clawreform-api/static/js/pages/hands.js` around lines 212 - 216, The
getRequirementCheckLabel function may emit "undefined >= x" if req.min_version
is set but req.check_value is missing; update getRequirementCheckLabel to
defensively check that req.check_value (or req.checkValue) is a valid
non-null/undefined string before concatenating with req.min_version and fall
back to an empty string or a safe placeholder when check_value is absent;
specifically modify the logic in getRequirementCheckLabel to test
req.check_value (or use String(req.check_value) only when not null/undefined)
before returning "req.check_value + ' >= ' + req.min_version", otherwise return
'' or req.min_version alone as appropriate.

Comment on lines +317 to +330
let output = std::process::Command::new(name).args(&args).output().ok()?;
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();

if stdout.is_empty() && stderr.is_empty() {
return None;
}

Some(match (stdout.is_empty(), stderr.is_empty()) {
(false, true) => stdout,
(true, false) => stderr,
(false, false) => format!("{stdout}\n{stderr}"),
(true, true) => return None,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and get its size
wc -l crates/clawreform-hands/src/registry.rs

Repository: aegntic/clawreform

Length of output: 105


🏁 Script executed:

# Read the specific lines mentioned in the review plus surrounding context
sed -n '300,340p' crates/clawreform-hands/src/registry.rs

Repository: aegntic/clawreform

Length of output: 1444


🏁 Script executed:

# Search for the function containing these lines and understand the context
cd crates/clawreform-hands && grep -n "min_version\|fn.*version" src/registry.rs | head -20

Repository: aegntic/clawreform

Length of output: 978


🏁 Script executed:

# Read the extract_version function to understand how it parses version strings
sed -n '333,360p' crates/clawreform-hands/src/registry.rs

Repository: aegntic/clawreform

Length of output: 765


🏁 Script executed:

# Search for tests related to version handling and min_version
grep -n "test\|#\[test\]" crates/clawreform-hands/src/registry.rs | tail -20

Repository: aegntic/clawreform

Length of output: 606


🏁 Script executed:

# Look for any timeout or threading code in the file
grep -n "timeout\|thread\|spawn\|await" crates/clawreform-hands/src/registry.rs

Repository: aegntic/clawreform

Length of output: 231


🏁 Script executed:

# Read the version-related tests to understand the expected behavior
sed -n '583,630p' crates/clawreform-hands/src/registry.rs

Repository: aegntic/clawreform

Length of output: 1267


🏁 Script executed:

# Search for how binary_meets_min_version is used in the codebase
grep -rn "binary_meets_min_version" crates/clawreform-hands/src/

Repository: aegntic/clawreform

Length of output: 293


🏁 Script executed:

# Check if there are any async/await patterns or threading in the broader codebase
grep -n "async\|tokio\|thread\|spawn" crates/clawreform-hands/src/*.rs | head -20

Repository: aegntic/clawreform

Length of output: 605


🏁 Script executed:

# Look at the file to understand the broader context where binary_meets_min_version is called
sed -n '255,275p' crates/clawreform-hands/src/registry.rs

Repository: aegntic/clawreform

Length of output: 840


🏁 Script executed:

# Read more context around check_requirement to understand the usage
sed -n '200,280p' crates/clawreform-hands/src/registry.rs

Repository: aegntic/clawreform

Length of output: 2767


🏁 Script executed:

# Look for where check_requirement is called to understand if there are timeout/threading concerns
grep -rn "check_requirement" crates/clawreform-hands/src/ | head -15

Repository: aegntic/clawreform

Length of output: 1045


🏁 Script executed:

# Run cargo build for the workspace
cd crates/clawreform-hands && cargo build --workspace --lib 2>&1 | tail -50

Repository: aegntic/clawreform

Length of output: 662


🏁 Script executed:

# Run cargo test
cd crates/clawreform-hands && cargo test --workspace 2>&1 | tail -100

Repository: aegntic/clawreform

Length of output: 148


🏁 Script executed:

# Try a simpler cargo build without syncing the full toolchain
cd crates/clawreform-hands && cargo build --lib 2>&1 | head -50

Repository: aegntic/clawreform

Length of output: 7251


🏁 Script executed:

# Try cargo test with a timeout to see if there's a hang issue
timeout 30 cargo test --lib binary_meets_min_version 2>&1 || echo "Exit code: $?"

Repository: aegntic/clawreform

Length of output: 122


🏁 Script executed:

# Let me examine what happens when a command exits with non-zero status
# Create a test scenario to understand the extract_version behavior
cat > /tmp/test_version.txt << 'EOF'
Example error outputs that might contain version-like patterns:
"Error: option requires argument 1"
"Usage: mybin --version
Version 2.0.0"
"Failed at line 5"
"invalid version format 3.14"
EOF
cat /tmp/test_version.txt

Repository: aegntic/clawreform

Length of output: 249


🏁 Script executed:

# Check if there are any error handling tests or examples that show the concern
grep -A 10 -B 5 "output.status\|exit\|non.zero\|failed" crates/clawreform-hands/src/registry.rs

Repository: aegntic/clawreform

Length of output: 44


🏁 Script executed:

# Look at the public API of the Hand to understand if this runs on the main thread
sed -n '176,190p' crates/clawreform-hands/src/registry.rs

Repository: aegntic/clawreform

Length of output: 565


🏁 Script executed:

# Search for "readiness" to understand the timeout concern mentioned in review
grep -rn "readiness\|timeout\|stall\|wedge" crates/clawreform-hands/src/

Repository: aegntic/clawreform

Length of output: 3646


🏁 Script executed:

# Check if there are comments about the check_requirements being called from async/API context
grep -B 5 -A 5 "pub fn check_requirements" crates/clawreform-hands/src/registry.rs

Repository: aegntic/clawreform

Length of output: 513


🏁 Script executed:

# Look at the HandResult error types to understand error handling
grep -A 10 "enum HandError\|type HandResult" crates/clawreform-hands/src/lib.rs | head -30

Repository: aegntic/clawreform

Length of output: 741


🏁 Script executed:

# Let me check if there's any caller or context that would indicate async/API usage
grep -rn "check_requirements\|readiness" crates/ --include="*.rs" | grep -v "test\|crates/clawreform-hands"

Repository: aegntic/clawreform

Length of output: 832


🏁 Script executed:

# Check the broader project structure to understand if this is used in an async context
ls -la crates/

Repository: aegntic/clawreform

Length of output: 1042


🏁 Script executed:

# Look for kernel or server code that might call into this
find crates -name "*.rs" -type f | head -20 | xargs grep -l "check_requirements" 2>/dev/null

Repository: aegntic/clawreform

Length of output: 141


🏁 Script executed:

# Look at the API routes to see if they're async and how check_requirements is used
sed -n '3045,3065p' crates/clawreform-api/src/routes.rs

Repository: aegntic/clawreform

Length of output: 852


🏁 Script executed:

# Check the async context
grep -B 5 "check_requirements" crates/clawreform-api/src/routes.rs | head -40

Repository: aegntic/clawreform

Length of output: 928


🏁 Script executed:

# Check if there's any timeout handling in the API layer
grep -n "timeout\|tokio\|blocking" crates/clawreform-api/src/routes.rs | head -20

Repository: aegntic/clawreform

Length of output: 1349


🏁 Script executed:

# Look at channel_bridge to understand the readiness check context
sed -n '235,250p' crates/clawreform-api/src/channel_bridge.rs

Repository: aegntic/clawreform

Length of output: 704


🏁 Script executed:

# Let's see if there's an async wrapper or blocking implementation already in the codebase
grep -A 10 "tokio::task::spawn_blocking\|blocking" crates/clawreform-api/src/routes.rs | head -30

Repository: aegntic/clawreform

Length of output: 44


🏁 Script executed:

# Check how other binaries are invoked with timeout protection in the API
sed -n '3300,3330p' crates/clawreform-api/src/routes.rs

Repository: aegntic/clawreform

Length of output: 1234


🏁 Script executed:

# Verify the context: are these API handlers marked as async?
grep -B 3 "check_requirements(&d.id)" crates/clawreform-api/src/routes.rs | head -15

Repository: aegntic/clawreform

Length of output: 188


🏁 Script executed:

# Let's see the full context of how check_requirements is called in the async handlers
sed -n '3040,3075p' crates/clawreform-api/src/routes.rs

Repository: aegntic/clawreform

Length of output: 1394


🏁 Script executed:

# Check if this is indeed an async handler
grep -B 15 "check_requirements(&d.id)" crates/clawreform-api/src/routes.rs | grep -E "async|fn |#\[" | head -20

Repository: aegntic/clawreform

Length of output: 144


🏁 Script executed:

# Verify the function signature to understand if it's an async route handler
sed -n '3000,3050p' crates/clawreform-api/src/routes.rs | head -60

Repository: aegntic/clawreform

Length of output: 1781


Check exit status before parsing process output, and protect from hung binaries in async context.

output().ok()? only filters spawn failures. A child that exits non-zero still has its stdout/stderr parsed, so error/usage text with digits can incorrectly satisfy min_version. Additionally, check_requirements() is called from async handlers (e.g., list_hands()) but runs synchronously, so a wedged or slow binary blocks the tokio runtime. Needs both exit code validation and timeout protection.

Required fixes

Exit status check in query_binary_version:

-    let output = std::process::Command::new(name).args(&args).output().ok()?;
+    let output = std::process::Command::new(name).args(&args).output().ok()?;
+    if !output.status.success() {
+        return None;
+    }

Timeout protection: Use tokio::time::timeout() with tokio::process::Command in the async callers, similar to the installation handler pattern at line 3302, or move to tokio::task::spawn_blocking().

📝 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 output = std::process::Command::new(name).args(&args).output().ok()?;
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
if stdout.is_empty() && stderr.is_empty() {
return None;
}
Some(match (stdout.is_empty(), stderr.is_empty()) {
(false, true) => stdout,
(true, false) => stderr,
(false, false) => format!("{stdout}\n{stderr}"),
(true, true) => return None,
})
let output = std::process::Command::new(name).args(&args).output().ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
if stdout.is_empty() && stderr.is_empty() {
return None;
}
Some(match (stdout.is_empty(), stderr.is_empty()) {
(false, true) => stdout,
(true, false) => stderr,
(false, false) => format!("{stdout}\n{stderr}"),
(true, true) => return None,
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/clawreform-hands/src/registry.rs` around lines 317 - 330,
query_binary_version currently uses std::process::Command::output() and parses
stdout/stderr even when the child exited non-zero and runs synchronously inside
async callers like check_requirements()/list_hands(); update
query_binary_version to validate the child's exit status (return None or an Err
when status.success() is false) before using stdout/stderr, and protect
blocking/spawn operations by running the process under Tokio: either switch to
tokio::process::Command and call it inside tokio::time::timeout(...) with an
appropriate duration in the async callers, or wrap the existing
std::process::Command invocation in tokio::task::spawn_blocking and apply
tokio::time::timeout around the spawned task to avoid wedging the runtime.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant