Skip to content

fix: audit follow-ups — LLM-provider binding, hint-mode deser, handler dedup, tc26 flake - #1144

Merged
dhyansraj merged 5 commits into
mainfrom
fix/1134-1136-1141-1142-audit-followups
Jun 4, 2026
Merged

fix: audit follow-ups — LLM-provider binding, hint-mode deser, handler dedup, tc26 flake#1144
dhyansraj merged 5 commits into
mainfrom
fix/1134-1136-1141-1142-audit-followups

Conversation

@dhyansraj

@dhyansraj dhyansraj commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Summary

Four independent audit follow-ups, each a clean per-issue commit, validated individually and by one comprehensive integration run.

  • Java: per-funcId LLM-provider proxy binding unreliable on agents with many @MeshLlm functions #1141 (fix · Rust shared core)process_llm_providers_changes inserted the resolved provider into self.topology.llm_providers before sending LLM_PROVIDER_AVAILABLE and discarded the send result. Once a function_id was recorded, the diff gate never re-emitted it — so a dropped send (first-heartbeat-burst back-pressure/race) left that @MeshLlm function's proxy permanently unbound (worsening with more @MeshLlm functions per agent). Reordered to send-before-insert (mirroring process_dependency_changes): the funcId is recorded only after a successful send, so a dropped send re-emits next heartbeat (self-healing); the let _ = …send() discard is now an Err warn + continue. Happy path unchanged; benefits all runtimes.
  • Java spring-ai handlers: hoist message-assembly / [Previous Response] prefix (+ optional Gemini tool-callback transform hook) #1136 (refactor · Java spring-ai handlers, behavior-preserving) — hoisted the duplicated user-content assembly, non-system-message extraction, and system-message replacement into LlmProviderHandler defaults with a previousResponsePrefix() hook (default [Previous Assistant Response]\n; Gemini overrides [Previous Response]\n); collapsed Gemini's two full createToolCallback(sForSchema) overrides into a single transformToolInputSchema(Map) hook (default identity; Gemini = convertSchemaTypesToUpperCase). Byte-identical per vendor.
  • Hint-mode structured output: lenient deserialization for loosely-shaped LLM responses (Java strict-deser throws; verify Python/TS parity) #1142 (fix · Python + TS + Java)hint mode embeds the schema in the prompt without vendor enforcement, so the LLM can return a scalar where the schema declares a list (e.g. "insights":"x" vs ["x"]); strict deserialization rejected it. Added scoped single-value-as-array coercion at each runtime's response-model boundary (Java: dedicated responseModelMapper with ACCEPT_SINGLE_VALUE_AS_ARRAY, shared mapper untouched; Python: ResponseParser._coerce_scalar_list_fields for list/Optional[List]; TS: coerceScalarArrayFields via zodToJsonSchema). No-op for well-shaped/strict output.
  • Flaky test: tc26_cancel_posts_synthetic_event_java — fixed 3s log-flush wait races producer stdout capture #1134 (test) — the three tc26_cancel_posts_synthetic_event variants (Python/TS/Java) replaced a fixed 3 s producer-log-flush wait with a poll-until-marker loop (~30×1s, diagnostics to stderr, fail-loud on timeout). All registry-side + producer-log assertions retained. Fixes the observed FAIL,FAIL,FAIL,PASS flake on the Java variant.

Scope note

The two high-risk structural refactors were deliberately split out for their own focused PRs (per maintainer): #1131 (TS MeshRuntimeBase event-loop dedup, incl. the registry_disconnected behavioral reconciliation) and #1137 (MeshAutoConfiguration god-config split). Also deferred: the MeshAutoConfiguration funcId-source AOP hardening noted during #1141 (latent, not the #1141 cause).

Review Notes

Independent review: 0 blocker / 0 warning / 3 INFO (all non-issues). All four fixes VERIFIED; the four touch disjoint areas (core emitter / Java handlers / response parsers / test yaml) with no cross-fix coupling. #1141 reorder confirmed correct (insert only after a successful send; continue skips only the failed funcId; happy path behavior-preserving). #1136 byte-identical per vendor. #1142 confirmed a genuine no-op when the value already matches the schema (valid arrays/None/nested untouched).

Test plan

Closes #1134
Closes #1136
Closes #1141
Closes #1142

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Structured response parsing now gracefully handles scalar values where list fields are declared, automatically coercing them into single-element arrays across Java, Python, and TypeScript.
    • Enhanced error handling for LLM provider event emissions with improved resilience.
  • Tests

    • Integration tests updated with robust polling mechanisms for detecting asynchronous event markers, improving CI reliability.

dhyansraj and others added 4 commits June 3, 2026 18:31
 #1134)

The three tc26_cancel_posts_synthetic_event variants (Python/TS/Java) gated the
producer-log marker assertions on a fixed 3s wait, which races the async
stdout->log capture under CI load (observed FAIL,FAIL,FAIL,PASS on Java).
Replace the fixed wait with a poll-until-marker loop (~30x1s) that captures the
producer log only once the cancelled_gracefully marker is present; diagnostics
to stderr, fail loud on timeout. All registry-side + producer-log assertions
retained unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ansform hook (Closes #1136)

Two behavior-preserving dedups in the spring-ai vendor handlers:

- Message assembly: the three handlers duplicated the user-content loop, the
  non-system-message extraction, and the system-message replacement loop,
  differing only in the assistant-prefix literal. Hoisted into LlmProviderHandler
  defaults (buildUserContent / extractNonSystemMessages / replaceSystemMessage)
  + a previousResponsePrefix() hook (default "[Previous Assistant Response]\n";
  Gemini overrides to "[Previous Response]\n").
- Tool-callback transform: GeminiHandler re-implemented the whole base
  createToolCallback / createToolCallbacksForSchema just to upper-case the tool
  input schema. Added a transformToolInputSchema(Map) hook (default identity) to
  the base; Gemini now overrides only that hook (convertSchemaTypesToUpperCase),
  dropping both full overrides.

Byte-identical per vendor (only the code path is shared). Validated: mvn compile
BUILD SUCCESS; spring-ai 135 tests green (incl. GeminiHandler tool-callback +
BuildToolNoExecute).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…1141)

process_llm_providers_changes inserted the resolved provider into
self.topology.llm_providers BEFORE sending the event, and discarded the send
result. Once a function_id is recorded, the diff gate never re-emits it — so a
dropped send (back-pressure/first-heartbeat-burst race) permanently suppressed
re-emission, leaving that @Meshllm function's proxy unbound. The failure scaled
with the number of @Meshllm functions on an agent.

Reorder to send-before-insert, mirroring process_dependency_changes: the
function_id is recorded as "seen" only after a successful send, so a dropped
send leaves it un-inserted and the next heartbeat re-emits (self-healing,
<= one heartbeat-interval delay). Replaced the `let _ = ...send()` discard with
an explicit Err warn + continue. Happy path unchanged.

Shared-core fix (benefits Python/TS/Java). Validated: cargo build/test green
(420 passed, --no-default-features per CI), zero new clippy warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d output (Closes #1142)

Under output_mode=hint the provider embeds the schema in the prompt but does
not enforce it, so the LLM can return a scalar where the schema declares a list
(e.g. "insights": "x" instead of ["x"]). Strict deserialization rejected it
(Java threw MismatchedInputException → fallback). Add scoped single-value-as-
array coercion at each runtime's response-model deserialization boundary; no-op
for well-shaped (strict) output.

- Java: dedicated responseModelMapper with ACCEPT_SINGLE_VALUE_AS_ARRAY, used
  only by generate(Class) (shared objectMapper untouched).
- Python: ResponseParser._coerce_scalar_list_fields (list[...] / List[...] /
  Optional[List[...]]) before Pydantic validation.
- TypeScript: ResponseParser.coerceScalarArrayFields via zodToJsonSchema
  introspection before safeParse.

Coercion is top-level (Jackson also handles nesting); scoped to structured-
output parsing only. Validated: Java 13, Python 31, TS 843 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@dhyansraj, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 22 minutes and 23 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3ad8c365-24ee-4b51-85d2-2b8be983c0be

📥 Commits

Reviewing files that changed from the base of the PR and between 834bb42 and 5f5d50b.

📒 Files selected for processing (3)
  • tests/integration/suites/uc21_meshjob/tc26_cancel_posts_synthetic_event/test.yaml
  • tests/integration/suites/uc22_meshjob_ts/tc26_cancel_posts_synthetic_event_ts/test.yaml
  • tests/integration/suites/uc23_meshjob_java/tc26_cancel_posts_synthetic_event_java/test.yaml
📝 Walkthrough

Walkthrough

This PR addresses "hint-mode drift" by implementing scalar-to-list coercion across Rust, Java, Python, and TypeScript runtimes, refactoring shared message-handling in Java handlers, and improving integration test reliability with polling instead of fixed waits.

Changes

Scalar Coercion & Message Refactoring

Layer / File(s) Summary
Shared message-handling contract and schema transformation hook
src/runtime/java/mcp-mesh-spring-ai/src/main/java/io/mcpmesh/ai/handlers/LlmProviderHandler.java
LlmProviderHandler introduces shared default methods for message assembly (previousResponsePrefix, extractNonSystemMessages, buildUserContent, replaceSystemMessage) and a transformToolInputSchema hook for vendor-specific schema transformation; tool-callback schema serialization applies the transformer before encoding.
Gemini handler vendor-specific overrides
src/runtime/java/mcp-mesh-spring-ai/src/main/java/io/mcpmesh/ai/handlers/GeminiHandler.java
GeminiHandler adds previousResponsePrefix and transformToolInputSchema overrides (for JSON-schema type uppercasing); removes prior createToolCallbacksForSchema and createToolCallback overrides; delegates message building to shared helpers.
Anthropic and OpenAI handler message delegation
src/runtime/java/mcp-mesh-spring-ai/src/main/java/io/mcpmesh/ai/handlers/AnthropicHandler.java, src/runtime/java/mcp-mesh-spring-ai/src/main/java/io/mcpmesh/ai/handlers/OpenAiHandler.java
Both handlers replace inline message/prompt logic with buildUserContent and replaceSystemMessage calls; remove UserMessage imports as they no longer directly reference message types.
Java response-model lenient deserialization
src/runtime/java/mcp-mesh-spring-boot-starter/src/main/java/io/mcpmesh/spring/MeshLlmAgentProxy.java, src/runtime/java/mcp-mesh-spring-boot-starter/src/test/java/io/mcpmesh/spring/MeshLlmAgentProxyResponseModelLenientTest.java
MeshLlmAgentProxy adds responseModelMapper configured with ACCEPT_SINGLE_VALUE_AS_ARRAY for generate(Class) structured-response parsing; test class verifies both scalar-coerced and normal array deserialization.
Python scalar-to-list coercion helpers
src/runtime/python/_mcp_mesh/engine/response_parser.py, src/runtime/python/tests/unit/test_response_parser_scalar_list.py
ResponseParser adds _is_list_annotation and _coerce_scalar_list_fields helpers to detect list-typed fields and wrap scalar values into single-element lists during validation; comprehensive test coverage includes optional lists and preservation of non-list fields.
TypeScript scalar-to-list coercion helper
src/runtime/typescript/src/response-parser.ts, src/runtime/typescript/src/__tests__/response-parser.test.ts
ResponseParser.parse applies coerceScalarArrayFields to wrap scalar values into single-element arrays for array-typed top-level properties before Zod validation; test suite validates coercion behavior for arrays and optional arrays.
Rust event emission error handling
src/runtime/core/src/runtime.rs
process_llm_providers_changes reorders LLM provider tracking to emit MeshEvent::llm_provider_available first with explicit error handling; on send failure logs warning and skips topology recording (allowing re-emission on next heartbeat).
Integration test polling reliability improvements
tests/integration/suites/uc21_meshjob/tc26_cancel_posts_synthetic_event/test.yaml, tests/integration/suites/uc22_meshjob_ts/tc26_cancel_posts_synthetic_event_ts/test.yaml, tests/integration/suites/uc23_meshjob_java/tc26_cancel_posts_synthetic_event_java/test.yaml
Three test YAML files replace fixed "wait for log flush" delays with explicit polling loops (~30 seconds) that repeatedly check provider logs for the [run_until_cancel] cancelled_gracefully marker, capturing or dumping logs on success/timeout.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • dhyansraj/mcp-mesh#1134: Proposes the same polling-based fix for the flaky tc26_cancel_posts_synthetic_event integration test, addressing the fixed 3s log-flush wait that this PR replaces with explicit marker polling.

Possibly related PRs

  • dhyansraj/mcp-mesh#1043: Introduces the TypeScript MeshJob cancel-synthetic-event test and provider fixture that produces the [run_until_cancel] cancelled_gracefully marker that this PR's polling logic now waits for more reliably.

Poem

🐰 Scalars dancing where arrays should be,

We coerce them to lists with gentle decree,

Handlers now share their prompting grace,

Tests poll for markers at their own pace,

Hint-mode drift fades, stability's key! 🌟

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the four primary fixes: LLM-provider binding (#1141), hint-mode deserialization (#1142), handler deduplication (#1136), and tc26 test flake (#1134).
Docstring Coverage ✅ Passed Docstring coverage is 82.86% which is sufficient. The required threshold is 80.00%.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1134-1136-1141-1142-audit-followups

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@tests/integration/suites/uc21_meshjob/tc26_cancel_posts_synthetic_event/test.yaml`:
- Around line 103-106: The detection step greps the full output of "meshctl logs
long-task-provider" for "[run_until_cancel] cancelled_gracefully" but the
subsequent capture uses "tail -200" which can omit older markers; make the
capture use the same log source as the detection so they match (either remove
the "tail -200" and run the same "meshctl logs long-task-provider 2>&1 | grep
\"[run_until_cancel] cancelled_gracefully\"" or capture the full output into a
variable and grep that variable) and ensure the grep pattern uses the same
escaped marker string as in the detection.

In
`@tests/integration/suites/uc22_meshjob_ts/tc26_cancel_posts_synthetic_event_ts/test.yaml`:
- Around line 96-99: The test intermittently misses the marker because it calls
meshctl logs long-task-provider-ts twice (once for detection with grep -qF and
again with tail -200 for capture); instead capture the logs once into a variable
or temporary file (run meshctl logs long-task-provider-ts > /tmp/logs or assign
to LOGS="$(meshctl logs long-task-provider-ts)") and then run grep -qF
"[run_until_cancel] cancelled_gracefully" against that single snapshot and, on
success, print the relevant lines from the same snapshot (e.g., use echo "$LOGS"
| tail -200 | grep "\[run_until_cancel\] cancelled_gracefully") so both
detection and capture operate on identical log content.

In
`@tests/integration/suites/uc23_meshjob_java/tc26_cancel_posts_synthetic_event_java/test.yaml`:
- Around line 126-129: The test currently calls meshctl logs
long-task-provider-java twice (once with grep -qF and then again piped to tail
-200) which can miss the marker due to different reads; change the block to read
the logs once into a variable (or a temp file) and use that same captured output
for both the presence check and the tail extraction so both grep and the tail
operate on the identical log snapshot (reference: the commands using meshctl
logs long-task-provider-java, the grep -qF "[run_until_cancel]
cancelled_gracefully" check, and the tail -200 extraction).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 58cf4c6a-24b9-4cd7-be67-3fefd01ceba4

📥 Commits

Reviewing files that changed from the base of the PR and between 2fd11e6 and 834bb42.

📒 Files selected for processing (14)
  • src/runtime/core/src/runtime.rs
  • src/runtime/java/mcp-mesh-spring-ai/src/main/java/io/mcpmesh/ai/handlers/AnthropicHandler.java
  • src/runtime/java/mcp-mesh-spring-ai/src/main/java/io/mcpmesh/ai/handlers/GeminiHandler.java
  • src/runtime/java/mcp-mesh-spring-ai/src/main/java/io/mcpmesh/ai/handlers/LlmProviderHandler.java
  • src/runtime/java/mcp-mesh-spring-ai/src/main/java/io/mcpmesh/ai/handlers/OpenAiHandler.java
  • src/runtime/java/mcp-mesh-spring-boot-starter/src/main/java/io/mcpmesh/spring/MeshLlmAgentProxy.java
  • src/runtime/java/mcp-mesh-spring-boot-starter/src/test/java/io/mcpmesh/spring/MeshLlmAgentProxyResponseModelLenientTest.java
  • src/runtime/python/_mcp_mesh/engine/response_parser.py
  • src/runtime/python/tests/unit/test_response_parser_scalar_list.py
  • src/runtime/typescript/src/__tests__/response-parser.test.ts
  • src/runtime/typescript/src/response-parser.ts
  • tests/integration/suites/uc21_meshjob/tc26_cancel_posts_synthetic_event/test.yaml
  • tests/integration/suites/uc22_meshjob_ts/tc26_cancel_posts_synthetic_event_ts/test.yaml
  • tests/integration/suites/uc23_meshjob_java/tc26_cancel_posts_synthetic_event_java/test.yaml

…1134)

The tc26 poll loop read `meshctl logs <provider>` twice per iteration: full-log
detection (grep -qF) vs a second read piped through `tail -200` for capture. If
>200 log lines followed the marker, detection passed but the tail-200 capture
missed it -> empty producer_log -> assertion failure. Snapshot the log once into
$LOGS and use that single snapshot for both detection and extraction (drop
tail -200; grep -F emits only the marker line, which carries the events_seen
payload). Same fixed-string pattern for both; timeout diagnostics reuse the last
snapshot. Assertions unchanged.

Validated: all three tc26 variants GREEN (Java x2); events_seen content confirmed
on the captured marker line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dhyansraj
dhyansraj merged commit 2a56894 into main Jun 4, 2026
20 checks passed
@dhyansraj
dhyansraj deleted the fix/1134-1136-1141-1142-audit-followups branch June 4, 2026 01:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment