Skip to content

fix(prompts): tighten prompt tuning output guidance - #136

Merged
yilu331 merged 2 commits into
mainfrom
feature/swe-bench-prompt-tuning-output-guidance
Jun 8, 2026
Merged

fix(prompts): tighten prompt tuning output guidance#136
yilu331 merged 2 commits into
mainfrom
feature/swe-bench-prompt-tuning-output-guidance

Conversation

@yilu331

@yilu331 yilu331 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add prompt-bank versions that replace output examples with stricter format guidance
  • tighten consolidation id handling and compact unified-skill guidance
  • preserve caller request IDs and add a hard client-side LiteLLM timeout for tuning-loop stability

Verification

  • uv run pytest tests/server/services/test_generation_service.py tests/server/services/test_prompt_model_mapping.py tests/server/services/prompt/test_prompt_manager.py tests/server/llm/test_litellm_client_unit.py::TestLitellmIntegration -q --no-cov

Summary by CodeRabbit

  • New Features

    • User interaction requests now accept optional request identifiers for improved request tracking and correlation
    • Client-side hard timeout support added for LLM calls to enforce strict timeout boundaries
  • Updates

    • Playbook extraction and consolidation prompts updated with enhanced extraction and consolidation logic

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces three independent feature updates: client-supplied request ID support for tracking user interactions, executor-based hard timeout enforcement for LLM calls to prevent indefinite blocking, and significant updates to playbook consolidation and extraction prompts with stricter output constraints and refined decision logic.

Changes

Client Request ID Support

Layer / File(s) Summary
API contract: optional request_id field
reflexio/models/api_schema/domain/entities.py
PublishUserInteractionRequest now includes an optional request_id field for clients to provide request identifiers.
Service: honor client-provided request_id
reflexio/server/services/generation_service.py
GenerationService.run now uses the client-provided request_id when present, falling back to UUID generation otherwise.
Test: verify request ID flow
tests/server/services/test_generation_service.py
New test verifies that a client-supplied request_id is preserved through the generation service and storage layer.

LLM Hard Timeout Protection

Layer / File(s) Summary
Hard timeout exception and executor wrapper
reflexio/server/llm/litellm_client.py
Adds LLMHardTimeoutError exception, imports ThreadPoolExecutor and aliases TimeoutError as FuturesTimeoutError, and implements _completion_with_hard_timeout function that enforces wall-clock deadline on litellm.completion calls based on provider timeout plus configurable grace period.
Integration: use hard timeout wrapper in request path
reflexio/server/llm/litellm_client.py
Request execution in _make_request now calls _completion_with_hard_timeout instead of litellm.completion directly, ensuring hard timeout enforcement on all completion attempts.
Test: verify hard timeout behavior
tests/server/llm/test_litellm_client_unit.py
New test confirms that LLM calls are interrupted when hard timeout elapses, completing quickly without blocking indefinitely, even when the provider sleeps longer than the timeout window.

Prompt Library Updates

Layer / File(s) Summary
Playbook consolidation: v2.3.0 deactivation and v2.3.1–v2.3.2 addition
reflexio/server/prompt/prompt_bank/playbook_consolidation/v*.prompt.md
Deactivates v2.3.0; introduces v2.3.1 with decision-kind definitions, unification rules, and no-self-contradiction guard; advances to v2.3.2 with re-synthesis guidance for coherent composition, hard constraints on numeric ID fields, and strict JSON-only output requirements.
Playbook extraction: v4.2.0 deactivation and v4.2.2–v4.2.3 addition
reflexio/server/prompt/prompt_bank/playbook_extraction_context/v*.prompt.md
Deactivates v4.2.0; introduces v4.2.2 with resumable extraction mode, correction SOP and success recipe definitions, content grounding rules, and reasoning procedures; advances to v4.2.3 with refined tool-call discipline, explicit intermediate-tool behavior constraints, and exhaustive output format and cardinality rules.
Test: synchronize prompt version expectations
tests/server/services/test_prompt_model_mapping.py
Updates PROMPT_VERSION_MAP test expectations to pin playbook_consolidation to v2.3.2 and playbook_extraction_context to v4.2.3.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A request ID hops through the service with grace,
While timeouts stand guard at the LLM's place,
And prompts now consolidate rules without strife—
Three updates bring rigor to the rabbit's life! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: tightening prompt output guidance through new stricter prompt versions and format constraints.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/swe-bench-prompt-tuning-output-guidance

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

🧹 Nitpick comments (2)
tests/server/services/test_generation_service.py (1)

74-107: ⚡ Quick win

Consider extracting common test setup to reduce duplication.

The test setup (lines 75-91) is nearly identical to test_publish_request_with_session_id (lines 47-62). Extract the common setup into a fixture or helper function to improve maintainability:

♻️ Example refactor using a fixture
+@pytest.fixture
+def generation_service_setup(mock_llm_responses):
+    """Fixture providing configured generation service and sample interaction"""
+    org_id = "test_org"
+    with tempfile.TemporaryDirectory() as temp_dir:
+        llm_config = LiteLLMConfig(model="gpt-4o-mini")
+        llm_client = LiteLLMClient(llm_config)
+        generation_service = GenerationService(
+            llm_client=llm_client,
+            request_context=RequestContext(org_id=org_id, storage_base_dir=temp_dir),
+        )
+        interaction = InteractionData(
+            content="test interaction",
+            created_at=int(datetime.datetime.now(UTC).timestamp()),
+        )
+        yield generation_service, interaction
+
-def test_publish_request_honors_caller_request_id(mock_llm_responses):
+def test_publish_request_honors_caller_request_id(generation_service_setup):
-    user_id = "test_user_id"
-    org_id = "test_org"
+    generation_service, interaction = generation_service_setup
+    user_id = "test_user_id"
     session_id = "test_session_id"
     request_id = "caller-request-id"
-
-    with tempfile.TemporaryDirectory() as temp_dir:
-        llm_config = LiteLLMConfig(model="gpt-4o-mini")
-        llm_client = LiteLLMClient(llm_config)
-        generation_service = GenerationService(
-            llm_client=llm_client,
-            request_context=RequestContext(org_id=org_id, storage_base_dir=temp_dir),
-        )
-
-        interaction = InteractionData(
-            content="test interaction",
-            created_at=int(datetime.datetime.now(UTC).timestamp()),
-        )
-
-        request = PublishUserInteractionRequest(
+    
+    request = PublishUserInteractionRequest(
+        request_id=request_id,
+        user_id=user_id,
+        interaction_data_list=[interaction],
+        session_id=session_id,
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/server/services/test_generation_service.py` around lines 74 - 107, The
two tests test_publish_request_honors_caller_request_id and
test_publish_request_with_session_id duplicate setup for
LiteLLMConfig/LiteLLMClient, RequestContext, GenerationService, and
InteractionData; extract that repeated setup into a pytest fixture or helper
function (e.g., a fixture named generation_service_factory or common_setup) that
returns a configured GenerationService (or returns the tuple of
generation_service, temp_dir, request_context) and reuse it in both tests;
update both tests to call the fixture/helper and remove the duplicated lines
creating llm_config, llm_client, RequestContext, GenerationService, and
InteractionData while keeping assertions unchanged.
reflexio/server/services/generation_service.py (1)

175-177: 💤 Low value

Consider explicit validation for empty string request_id.

The expression publish_user_interaction_request.request_id or str(uuid.uuid4()) treats an empty string "" as falsy and silently replaces it with a UUID. While this is consistent with Python idioms, it may surprise callers who explicitly pass request_id="" (which passes Pydantic validation since the field is typed as str | None).

Consider either:

  • Adding a Pydantic validator to reject empty strings if they're invalid
  • Explicitly checking is None if you only want to replace None: if publish_user_interaction_request.request_id is None: ...

This ensures the API contract is clear about what constitutes a "provided" request_id.

🤖 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 `@reflexio/server/services/generation_service.py` around lines 175 - 177, The
current line using publish_user_interaction_request.request_id or
str(uuid.uuid4()) will treat an empty string as missing; change this to
explicitly distinguish None from empty string: either (A) replace the expression
with an explicit None check (e.g., if
publish_user_interaction_request.request_id is None: request_id =
str(uuid.uuid4()) else: request_id =
publish_user_interaction_request.request_id) so empty strings are preserved, or
(B) add a Pydantic validator on the request model that rejects empty strings for
the request_id field (e.g., in the model used to construct
publish_user_interaction_request) and then keep the current logic; update code
in generation_service.py around the request_id assignment and the model class
where request_id is defined accordingly.
🤖 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 `@reflexio/server/llm/litellm_client.py`:
- Around line 980-983: The code directly calls
float(os.environ.get("REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS", "5")) which will
raise on malformed env values; change this to parse with a safe try/except
(catch ValueError/TypeError), falling back to the default 5.0 if parsing fails,
then clamp with grace_seconds = max(0.0, parsed_value) before computing
hard_timeout = max(0.001, timeout_seconds) + grace_seconds; optionally emit a
warning/log when parsing fails. This affects the
REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS env read and the grace_seconds /
hard_timeout calculation in litellm_client.py.
- Around line 985-997: The current _completion_with_hard_timeout implementation
spawns a ThreadPoolExecutor per request which cannot kill a blocking
litellm.completion call, allowing hung provider calls to accumulate stuck
threads; replace the thread-based approach with process-based isolation: run
litellm.completion in a separate process (e.g., via multiprocessing.Process or
concurrent.futures.ProcessPoolExecutor) and communicate the result back over a
Pipe/Queue, enforce the hard_timeout by terminating the child process if it
exceeds the timeout, raise LLMHardTimeoutError on termination, and ensure you
properly join/terminate the child and clean up resources (replace usages of
future.cancel() / executor.shutdown(...) in _completion_with_hard_timeout with
process termination and safe result collection).

In `@reflexio/server/services/generation_service.py`:
- Around line 175-177: The code currently coerces empty strings into new UUIDs
causing duplicate client request_ids to overwrite Request rows but still append
Interactions; change the assignment so a provided empty string is treated as a
supplied id (generate a uuid only when
publish_user_interaction_request.request_id is None), i.e. treat None vs ""
distinctly for publish_user_interaction_request.request_id; then in
GenerationService.run use RequestMixin.get_request(request_id) (or equivalent
existence check) before calling RequestMixin.add_request() and
add_user_interactions_bulk()/ _insert_interaction so you either return/no-op or
raise on a repeated request_id to enforce idempotency (or explicitly dedupe
interactions) and adjust
GenerationService.get_interaction_from_publish_user_interaction_request/_insert_interaction
logic as needed to avoid always appending when interaction_id == 0.

---

Nitpick comments:
In `@reflexio/server/services/generation_service.py`:
- Around line 175-177: The current line using
publish_user_interaction_request.request_id or str(uuid.uuid4()) will treat an
empty string as missing; change this to explicitly distinguish None from empty
string: either (A) replace the expression with an explicit None check (e.g., if
publish_user_interaction_request.request_id is None: request_id =
str(uuid.uuid4()) else: request_id =
publish_user_interaction_request.request_id) so empty strings are preserved, or
(B) add a Pydantic validator on the request model that rejects empty strings for
the request_id field (e.g., in the model used to construct
publish_user_interaction_request) and then keep the current logic; update code
in generation_service.py around the request_id assignment and the model class
where request_id is defined accordingly.

In `@tests/server/services/test_generation_service.py`:
- Around line 74-107: The two tests
test_publish_request_honors_caller_request_id and
test_publish_request_with_session_id duplicate setup for
LiteLLMConfig/LiteLLMClient, RequestContext, GenerationService, and
InteractionData; extract that repeated setup into a pytest fixture or helper
function (e.g., a fixture named generation_service_factory or common_setup) that
returns a configured GenerationService (or returns the tuple of
generation_service, temp_dir, request_context) and reuse it in both tests;
update both tests to call the fixture/helper and remove the duplicated lines
creating llm_config, llm_client, RequestContext, GenerationService, and
InteractionData while keeping assertions unchanged.
🪄 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: CHILL

Plan: Pro Plus

Run ID: 6a9f911a-2e9c-4b83-bcde-d65bf7e8e712

📥 Commits

Reviewing files that changed from the base of the PR and between 92797d0 and 9f727b6.

📒 Files selected for processing (12)
  • reflexio/models/api_schema/domain/entities.py
  • reflexio/server/llm/litellm_client.py
  • reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.0.prompt.md
  • reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.1.prompt.md
  • reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md
  • reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.0.prompt.md
  • reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.2.prompt.md
  • reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md
  • reflexio/server/services/generation_service.py
  • tests/server/llm/test_litellm_client_unit.py
  • tests/server/services/test_generation_service.py
  • tests/server/services/test_prompt_model_mapping.py

Comment thread reflexio/server/llm/litellm_client.py Outdated
Comment on lines +980 to +983
grace_seconds = float(
os.environ.get("REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS", "5")
)
hard_timeout = max(0.001, timeout_seconds) + max(0.0, grace_seconds)

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 | ⚡ Quick win

Handle malformed hard-timeout grace env safely.

REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS is parsed with float(...) without a guard. A bad env value will make every LLM request fail before dispatch.

Proposed fix
-        grace_seconds = float(
-            os.environ.get("REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS", "5")
-        )
+        grace_raw = os.environ.get("REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS", "5")
+        try:
+            grace_seconds = float(grace_raw)
+        except (TypeError, ValueError):
+            self.logger.warning(
+                "Invalid REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS=%r; falling back to 5.0",
+                grace_raw,
+            )
+            grace_seconds = 5.0
🤖 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 `@reflexio/server/llm/litellm_client.py` around lines 980 - 983, The code
directly calls float(os.environ.get("REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS",
"5")) which will raise on malformed env values; change this to parse with a safe
try/except (catch ValueError/TypeError), falling back to the default 5.0 if
parsing fails, then clamp with grace_seconds = max(0.0, parsed_value) before
computing hard_timeout = max(0.001, timeout_seconds) + grace_seconds; optionally
emit a warning/log when parsing fails. This affects the
REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS env read and the grace_seconds /
hard_timeout calculation in litellm_client.py.

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 080b2a2. REFLEXIO_LLM_HARD_TIMEOUT_GRACE_SECONDS now parses through a guarded helper, falls back to 5.0 on malformed values, clamps at non-negative, and has a regression test for the invalid-env path.

Comment thread reflexio/server/llm/litellm_client.py Outdated
Comment on lines +985 to +997
executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="litellm")
future = executor.submit(litellm.completion, **params)
try:
return future.result(timeout=hard_timeout)
except FuturesTimeoutError as exc:
future.cancel()
raise LLMHardTimeoutError(
f"LLM request exceeded hard timeout of {hard_timeout:.3f}s "
f"(provider timeout={provider_timeout!r})"
) from exc
finally:
executor.shutdown(wait=False, cancel_futures=True)

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:

#!/bin/bash
python - <<'PY'
import time, threading
from concurrent.futures import ThreadPoolExecutor

started = threading.Event()

def slow():
    started.set()
    time.sleep(5)

ex = ThreadPoolExecutor(max_workers=1)
f = ex.submit(slow)
started.wait(timeout=1)
print("cancel_before_shutdown:", f.cancel())  # expected: False (already running)
ex.shutdown(wait=False, cancel_futures=True)
print("future_done_immediately:", f.done())    # expected: False right after shutdown
time.sleep(0.2)
print("future_still_running:", not f.done())   # expected: True (until sleep completes)
PY

Repository: ReflexioAI/reflexio

Length of output: 151


🏁 Script executed:

#!/bin/bash
set -euo pipefail

repo_root="."
file="reflexio/server/llm/litellm_client.py"
# Print around snippet lines 950-1035 with line numbers
nl -ba "$file" | sed -n '930,1040p'

Repository: ReflexioAI/reflexio

Length of output: 106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="reflexio/server/llm/litellm_client.py"
nl -ba "$file" | sed -n '930,1040p'

Repository: ReflexioAI/reflexio

Length of output: 106


🏁 Script executed:

#!/bin/bash
set -euo pipefail
file="reflexio/server/llm/litellm_client.py"

# Print lines 960-1025 with line numbers using awk (nl not available)
awk 'NR>=960 && NR<=1025 {printf "%d\t%s\n", NR, $0}' "$file"

Repository: ReflexioAI/reflexio

Length of output: 3059


🏁 Script executed:

#!/bin/bash
python - <<'PY'
import time, threading
from concurrent.futures import ThreadPoolExecutor

started = threading.Event()

def slow():
    started.set()
    time.sleep(5)

ex = ThreadPoolExecutor(max_workers=1)
f = ex.submit(slow)
started.wait(timeout=1)
print("cancel_before_shutdown:", f.cancel())  # already running -> False
ex.shutdown(wait=False, cancel_futures=True)
print("future_done_immediately:", f.done())    # still not done
time.sleep(0.2)
print("future_still_running:", not f.done())   # thread continues running
PY

Repository: ReflexioAI/reflexio

Length of output: 151


🏁 Script executed:

#!/bin/bash
python - <<'PY'
import time, threading
from concurrent.futures import ThreadPoolExecutor

started = threading.Event()

def slow():
    started.set()
    time.sleep(5)

ex = ThreadPoolExecutor(max_workers=1)
f = ex.submit(slow)
started.wait(timeout=1)
print("cancel_before_shutdown:", f.cancel())  # False (already running)
ex.shutdown(wait=False, cancel_futures=True)
print("future_done_immediately:", f.done())    # False
time.sleep(0.2)
print("future_still_running:", not f.done())   # True
PY

Repository: ReflexioAI/reflexio

Length of output: 151


🏁 Script executed:

#!/bin/bash
python - <<'PY'
import time, threading
from concurrent.futures import ThreadPoolExecutor

started = threading.Event()

def slow():
    started.set()
    time.sleep(5)

ex = ThreadPoolExecutor(max_workers=1)
f = ex.submit(slow)
started.wait(timeout=1)
print("cancel_before_shutdown:", f.cancel())  # False (already running)
ex.shutdown(wait=False, cancel_futures=True)
print("future_done_immediately:", f.done())    # False
time.sleep(0.2)
print("future_still_running:", not f.done())   # True
PY

Repository: ReflexioAI/reflexio

Length of output: 151


Hard-timeout can accumulate stuck worker threads on provider hangs

_completion_with_hard_timeout() spins up a new ThreadPoolExecutor(max_workers=1) per request and times out by catching FuturesTimeoutError, then calling future.cancel() and executor.shutdown(wait=False, cancel_futures=True). future.cancel() won’t stop an already-running blocking litellm.completion call, so a provider that hangs keeps its worker thread alive until the call returns—repeated incidents can accumulate stuck threads.

Use process-based isolation (run litellm.completion in a subprocess and terminate the process on timeout) or another mechanism that can actually kill the in-flight request.

🤖 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 `@reflexio/server/llm/litellm_client.py` around lines 985 - 997, The current
_completion_with_hard_timeout implementation spawns a ThreadPoolExecutor per
request which cannot kill a blocking litellm.completion call, allowing hung
provider calls to accumulate stuck threads; replace the thread-based approach
with process-based isolation: run litellm.completion in a separate process
(e.g., via multiprocessing.Process or concurrent.futures.ProcessPoolExecutor)
and communicate the result back over a Pipe/Queue, enforce the hard_timeout by
terminating the child process if it exceeds the timeout, raise
LLMHardTimeoutError on termination, and ensure you properly join/terminate the
child and clean up resources (replace usages of future.cancel() /
executor.shutdown(...) in _completion_with_hard_timeout with process termination
and safe result collection).

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 080b2a2. Real LiteLLM completions now run in a child process for the hard-timeout path; on timeout the process is terminated, then killed if needed, before raising LLMHardTimeoutError. The old per-request ThreadPoolExecutor path was removed.

Comment on lines +175 to +177
request_id = publish_user_interaction_request.request_id or str(
uuid.uuid4()
)

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:

#!/bin/bash
# Description: Search for existing tests or code that exercises duplicate request_id scenarios

# Check for tests that publish multiple times with the same request_id
rg -n -A5 -B5 'request_id.*=.*["\'].*["\']' tests/ | rg -A10 -B10 'PublishUserInteractionRequest'

# Check for any uniqueness constraints or validation on request_id in storage layer
rg -n 'UNIQUE.*request_id|request_id.*UNIQUE' reflexio/server/services/storage/

# Look for idempotency handling or duplicate detection
ast-grep --pattern 'if $_.request_id $$$'

Repository: ReflexioAI/reflexio

Length of output: 132


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate generation_service snippet =="
rg -n "request_id\s*=\s*publish_user_interaction_request\.request_id|publish_user_interaction_request\.request_id\s*or" -S reflexio/server/services/generation_service.py || true

echo
echo "== Inspect surrounding lines in generation_service.py (160-230) =="
nl -ba reflexio/server/services/generation_service.py | sed -n '150,240p'

echo
echo "== Find storage requests insert behavior =="
# locate files mentioning INSERT OR REPLACE and requests table
rg -n --hidden -S "INSERT\s+OR\s+REPLACE|insert\s+or\s+replace" reflexio/server/services/storage/ || true
rg -n --hidden -S "requests" reflexio/server/services/storage/ || true

echo
echo "== Find UNIQUE constraints or indexes for request_id in storage =="
rg -n --hidden -S "UNIQUE.*request_id|request_id.*UNIQUE|CREATE\s+UNIQUE|CREATE\s+INDEX.*request_id" reflexio/server/services/storage/ || true

echo
echo "== Search for PublishUserInteractionRequest usage in tests (duplicate scenarios) =="
rg -n "PublishUserInteractionRequest" tests/ || true

echo
echo "== Search for publish calls with explicit request_id in code/tests =="
rg -n -S "request_id\s*=\s*['\"][^'\"]+['\"]" tests/ reflexio/ || true

echo
echo "== Search for idempotency/duplicate handling keywords around request_id =="
rg -n -S "idempotent|idempotency|duplicate|conflict|already exists|REPLACE|ON CONFLICT" reflexio/server/services/ || true

echo
echo "== Check interactions table relation to request_id (foreign key / accumulation) =="
rg -n -S "CREATE\s+TABLE.*interactions|interactions.*request_id|request_id.*interactions" reflexio/server/services/storage/ || true

Repository: ReflexioAI/reflexio

Length of output: 313


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Basic sanity: show generation_service request_id assignment =="
sed -n '150,210p' reflexio/server/services/generation_service.py

echo
echo "== Dump exact lines around request_id assignment (170-190) with line numbers via awk =="
awk 'NR>=165 && NR<=195 {printf "%6d:%s\n", NR, $0}' reflexio/server/services/generation_service.py

echo
echo "== Locate sqlite storage requests implementation (_requests.py) =="
fd -a "_requests.py" reflexio/server/services/storage || true
fd -a "sqlite_storage" reflexio/server/services/storage || true

echo
echo "== Search storage for INSERT OR REPLACE and requests table =="
rg -n --hidden -S "INSERT\s+OR\s+REPLACE|insert\s+or\s+replace" reflexio/server/services/storage/ || true
rg -n --hidden -S "CREATE\s+TABLE.*requests|requests" reflexio/server/services/storage/ || true

echo
echo "== Search for UNIQUE constraints / indexes for request_id =="
rg -n --hidden -S "UNIQUE.*request_id|request_id.*UNIQUE|CREATE\s+UNIQUE|CREATE\s+INDEX.*request_id" reflexio/server/services/storage/ || true

echo
echo "== Find where Requests are written (look for add_request / insert into requests) =="
rg -n --hidden -S "add_request|insert.*into.*requests|upsert|ON CONFLICT" reflexio/server/services/storage/ || true

echo
echo "== Inspect sqlite_storage/_requests.py if present =="
REQ_FILE="$(fd -a "_requests.py" reflexio/server/services/storage | head -n 1 || true)"
if [ -n "${REQ_FILE:-}" ]; then
  echo "Using: $REQ_FILE"
  sed -n '1,220p' "$REQ_FILE"
fi

echo
echo "== Search for tests exercising duplicate request_id / retries =="
rg -n -S "PublishUserInteractionRequest|request_id\s*=" tests/ reflexio/ || true

echo
echo "== Search for idempotency/duplicate handling keywords near request_id =="
rg -n -S "idempotent|idempotency|duplicate|conflict|already exists|REPLACE|ON CONFLICT" reflexio/server/services/ || true

echo
echo "== Inspect interactions schema relation to request_id =="
rg -n --hidden -S "CREATE\s+TABLE.*interactions|interactions.*request_id|request_id.*interactions|FOREIGN\s+KEY.*request_id" reflexio/server/services/storage/ || true

Repository: ReflexioAI/reflexio

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== generation_service: show Request construction + persistence (around new_request / add_request) =="
rg -n "new_request\s*=\s*Request|add_request\(" reflexio/server/services/generation_service.py
sed -n '200,270p' reflexio/server/services/generation_service.py

echo
echo "== sqlite_storage schema: show CREATE TABLE requests + interactions =="
rg -n "CREATE TABLE IF NOT EXISTS requests|CREATE TABLE IF NOT EXISTS interactions" reflexio/server/services/storage/sqlite_storage/_base.py
sed -n '1580,1685p' reflexio/server/services/storage/sqlite_storage/_base.py

echo
echo "== storage insert path for interactions: look for INSERT/OR REPLACE into interactions =="
rg -n --hidden -S "INSERT\s+OR\s+REPLACE\s+INTO\s+interactions|INSERT\s+INTO\s+interactions|add_interaction|add_interactions" reflexio/server/services/storage/sqlite_storage | head -n 80

echo
echo "== Inspect RequestMixin + any interaction mixin file for add_interaction(s) =="
fd -a "*interaction*" reflexio/server/services/storage/sqlite_storage || true
fd -a "_interactions.py" reflexio/server/services/storage/sqlite_storage || true
fd -a "_interactions" reflexio/server/services/storage/sqlite_storage || true

# Try common filenames
for f in reflexio/server/services/storage/sqlite_storage/_interactions.py \
         reflexio/server/services/storage/sqlite_storage/_interactions_mixin.py \
         reflexio/server/services/storage/storage_base/_interactions.py; do
  if [ -f "$f" ]; then
    echo "== file exists: $f =="
    sed -n '1,220p' "$f"
  fi
done

echo
echo "== Search tests for behavior when same request_id is inserted twice =="
# Target the storage contract tests first (most likely to cover upserts/replaces)
rg -n "test_.*request_id|add_request|INSERT OR REPLACE|OR REPLACE INTO requests|requests.*request_id" tests/server/services/storage/test_storage_contract_requests.py tests/server/services/storage/test_sqlite_storage.py tests/server/services/storage/test_sqlite_storage_bc_extras.py || true

sed -n '1,220p' tests/server/services/storage/test_storage_contract_requests.py || true

echo
echo "== Check generation_service tests for publishing same request_id twice =="
sed -n '1,240p' tests/server/services/test_generation_service.py || true

Repository: ReflexioAI/reflexio

Length of output: 20387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find add_user_interactions_bulk implementation =="
rg -n --hidden -S "def add_user_interactions_bulk|add_user_interactions_bulk\(" reflexio/server/services/storage || true
for f in $(rg -l --hidden -S "add_user_interactions_bulk" reflexio/server/services/storage | head -n 20); do
  echo
  echo "---- $f ----"
  rg -n "add_user_interactions_bulk" "$f" -n
done

# Show relevant portions from the likely file
target_files=$(rg -l --hidden -S "def add_user_interactions_bulk" reflexio/server/services/storage | head -n 5 || true)
if [ -n "${target_files:-}" ]; then
  for f in $target_files; do
    echo
    echo "== Showing function area in $f =="
    # print around first match line
    line=$(rg -n "def add_user_interactions_bulk" "$f" | head -n 1 | cut -d: -f1)
    start=$((line-40)); end=$((line+140))
    sed -n "${start},${end}p" "$f"
  done
fi

echo
echo "== Confirm interactions INSERT/UPSERT behavior =="
rg -n --hidden -S "INSERT\s+INTO\s+interactions|INSERT\s+OR\s+REPLACE\s+INTO\s+interactions|ON\s+CONFLICT.*interactions" reflexio/server/services/storage/sqlite_storage || true

echo
echo "== Inspect publish tests for any duplicate request_id publishing =="
sed -n '1,260p' tests/server/services/test_generation_service.py
echo
echo "== Continue inspect further in test_generation_service.py (260-520) =="
sed -n '260,560p' tests/server/services/test_generation_service.py

echo
echo "== Search within test_generation_service.py for 'publish' calls with same request_id var =="
rg -n "request_id\s*=" tests/server/services/test_generation_service.py
rg -n -S "caller-request-id|test_request|req1|r1" tests/server/services/test_generation_service.py || true

Repository: ReflexioAI/reflexio

Length of output: 20456


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Show _insert_interaction / add_interaction section around INSERT OR REPLACE INTO interactions (sqlite_storage/_profiles.py:360-430) =="
sed -n '330,460p' reflexio/server/services/storage/sqlite_storage/_profiles.py

echo
echo "== Search generation_service.py for any existing-request/idempotency check (get_request/duplicate handling) =="
rg -n --hidden -S "get_request\(|request_id.*get|existing.*request|duplicate|idempotent|already.*request" reflexio/server/services/generation_service.py || true

echo
echo "== Search for any early return when request_id exists =="
rg -n --hidden -S "if .*request_id.*(exists|present|get_request)" reflexio/server/services/generation_service.py || true

echo
echo "== Inspect PublishUserInteractionRequest schema for request_id constraints/typing (if in models) =="
# find definition
REQ_FILE="$(fd -a "service_schemas.py" reflexio/models/api_schema/service_schemas | head -n 1 || true)"
echo "service_schemas file: $REQ_FILE"
# If not found, just search for class name
rg -n "class\s+PublishUserInteractionRequest|PublishUserInteractionRequest" reflexio/models -S | head -n 20

Repository: ReflexioAI/reflexio

Length of output: 6473


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Show _insert_interaction / add_interaction section around INSERT OR REPLACE INTO interactions (sqlite_storage/_profiles.py:360-460) =="
sed -n '330,460p' reflexio/server/services/storage/sqlite_storage/_profiles.py

echo
echo "== Search generation_service.py for any existing-request/idempotency check (get_request/duplicate handling) =="
rg -n --hidden -S "get_request\(|request_id.*get|existing.*request|duplicate|idempotent|already.*request" reflexio/server/services/generation_service.py || true

echo
echo "== Search for any early return when request_id exists =="
rg -n --hidden -S "if .*request_id.*(exists|present|get_request)" reflexio/server/services/generation_service.py || true

echo
echo "== Inspect PublishUserInteractionRequest schema for request_id constraints/typing =="
rg -n "PublishUserInteractionRequest" reflexio/models/api_schema -S | head -n 50

Repository: ReflexioAI/reflexio

Length of output: 6287


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Show _insert_interaction / add_interaction section around INSERT OR REPLACE INTO interactions (sqlite_storage/_profiles.py:330-460) =="
sed -n '330,460p' reflexio/server/services/storage/sqlite_storage/_profiles.py

echo
echo "== Search generation_service.py for any existing-request/idempotency check (get_request/duplicate handling) =="
rg -n --hidden -S "get_request\(|existing.*request|duplicate|idempotent|already.*request" reflexio/server/services/generation_service.py || true

echo
echo "== Inspect PublishUserInteractionRequest schema for request_id constraints/typing =="
rg -n "class\s+PublishUserInteractionRequest|PublishUserInteractionRequest" reflexio/models/api_schema -S | head -n 80

Repository: ReflexioAI/reflexio

Length of output: 6229


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect PublishUserInteractionRequest.request_id field/validators (entities.py around class) =="
sed -n '530,640p' reflexio/models/api_schema/domain/entities.py

echo
echo "== Search for any validators/precondition checks referencing request_id =="
rg -n --hidden -S "request_id.*(min_length|max_length|validator|field_validator|model_validator|constr|required)|validate_.*request_id|duplicate.*request_id|idempot" reflexio/models reflexio/server tests | head -n 200

echo
echo "== Inspect Request model definition (created_at default) =="
# Locate class Request in entities.py or service_schemas
rg -n "class\s+Request\b" reflexio/models/api_schema -S
rg -n "Request\(" reflexio/models/api_schema -S | head -n 50

# Print the Request model area near where it's defined
REQ_LOC="$(rg -n "class\s+Request\b" reflexio/models/api_schema -S | head -n 1 | cut -d: -f1 || true)"
if [ -n "${REQ_LOC:-}" ]; then
  start=$((REQ_LOC-40)); end=$((REQ_LOC+120))
  sed -n "${start},${end}p" "$(echo reflexio/models/api_schema/domain/entities.py)" || true
fi

# Also try if Request is in a different file
for f in $(fd -e py "service_schemas.py" reflexio/models | head -n 5); do
  echo "== file: $f =="
  rg -n "class\s+Request\b" "$f" -S || true
done

Repository: ReflexioAI/reflexio

Length of output: 13903


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Request model (entities.py) around class Request =="
# entities.py is the one we saw earlier
sed -n '160,260p' reflexio/models/api_schema/domain/entities.py

echo
echo "== PublishUserInteractionRequest.request_id usage in Request construction =="
# show relevant generation_service block already partially shown, but include Request init region
sed -n '190,240p' reflexio/server/services/generation_service.py

echo
echo "== GenerationService.get_interaction_from_publish_user_interaction_request =="
# locate method
rg -n "get_interaction_from_publish_user_interaction_request" reflexio/server/services/generation_service.py
method_line=$(rg -n "get_interaction_from_publish_user_interaction_request" reflexio/server/services/generation_service.py | head -n 1 | cut -d: -f1)
start=$((method_line-20)); end=$((method_line+140))
sed -n "${start},${end}p" reflexio/server/services/generation_service.py

echo
echo "== Any early return / idempotency gate in GenerationService.run =="
rg -n --hidden -S "if .*request_id|request_id.*(get_request|exists|seen|duplicate|idempot)" reflexio/server/services/generation_service.py

echo
echo "== Operation queue idempotency logic (storage_base/_operations.py) =="
sed -n '120,220p' reflexio/server/services/storage/storage_base/_operations.py

echo
echo "== Tests for queue ignoring duplicate request_id (storage/test_storage_contract_operations.py around 127) =="
sed -n '90,170p' tests/server/services/storage/test_storage_contract_operations.py

Repository: ReflexioAI/reflexio

Length of output: 14653


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Interaction model definition (entities.py) around class Interaction =="
# Find Interaction class
I_LINE=$(rg -n "class\s+Interaction\b" reflexio/models/api_schema/domain/entities.py | head -n 1 | cut -d: -f1)
if [ -z "${I_LINE:-}" ]; then
  rg -n "Interaction\s*=\s*BaseModel" reflexio/models/api_schema/domain/entities.py || true
else
  start=$((I_LINE-40)); end=$((I_LINE+120))
  sed -n "${start},${end}p" reflexio/models/api_schema/domain/entities.py
fi

echo
echo "== GenerationService.get_interaction_from_publish_user_interaction_request implementation =="
M_LINE=$(rg -n "def\s+get_interaction_from_publish_user_interaction_request" reflexio/server/services/generation_service.py | head -n 1 | cut -d: -f1)
start=$((M_LINE-20)); end=$((M_LINE+220))
sed -n "${start},${end}p" reflexio/server/services/generation_service.py

echo
echo "== storage_base/_operations.py: look for duplicate request_id/idempotent queue logic =="
sed -n '120,200p' reflexio/server/services/storage/storage_base/_operations.py

echo
echo "== tests/server/services/storage/test_storage_contract_requests.py: check duplicate request_id add_request usage =="
sed -n '1,120p' tests/server/services/storage/test_storage_contract_requests.py

echo
echo "== tests/server/services/storage/test_storage_contract_operations.py around duplicate request_id queue ignore =="
sed -n '90,170p' tests/server/services/storage/test_storage_contract_operations.py

Repository: ReflexioAI/reflexio

Length of output: 19647


Fix non-idempotent request_id retries: duplicates overwrite requests but still insert new interactions
In reflexio/server/services/generation_service.py the effective id is computed as:

request_id = publish_user_interaction_request.request_id or str(uuid.uuid4())
  • Empty string is treated as falsy, so "" becomes a new UUID (since PublishUserInteractionRequest.request_id is str | None with no non-empty constraint).
  • The SQLite requests table uses request_id TEXT PRIMARY KEY, and RequestMixin.add_request() persists via INSERT OR REPLACE INTO requests, so a duplicate client request_id overwrites the prior Request row (changing created_at/metadata/etc.).
  • Each publish with that same request_id still inserts new interactions: GenerationService.get_interaction_from_publish_user_interaction_request() leaves Interaction.interaction_id at the default 0, and _insert_interaction() uses if interaction.interaction_id: to decide between INSERT OR REPLACE (only when interaction_id is non-zero) vs INSERT INTO interactions (append).
  • GenerationService.run() always performs add_request(...) + add_user_interactions_bulk(...) for the computed request_id (no early return when the request_id already exists). The only dedupe found is for the operation pending queue while blocked (try_acquire_in_progress_lock drops duplicate request_id entries), which doesn’t make the whole publish flow idempotent.

Either enforce idempotency semantics for client-supplied request_id (validate non-empty + return/no-op or otherwise prevent re-processing/interaction duplication) or explicitly document the “overwrite requests + append interactions” behavior and ensure downstream logic can’t be inconsistent.

🤖 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 `@reflexio/server/services/generation_service.py` around lines 175 - 177, The
code currently coerces empty strings into new UUIDs causing duplicate client
request_ids to overwrite Request rows but still append Interactions; change the
assignment so a provided empty string is treated as a supplied id (generate a
uuid only when publish_user_interaction_request.request_id is None), i.e. treat
None vs "" distinctly for publish_user_interaction_request.request_id; then in
GenerationService.run use RequestMixin.get_request(request_id) (or equivalent
existence check) before calling RequestMixin.add_request() and
add_user_interactions_bulk()/ _insert_interaction so you either return/no-op or
raise on a repeated request_id to enforce idempotency (or explicitly dedupe
interactions) and adjust
GenerationService.get_interaction_from_publish_user_interaction_request/_insert_interaction
logic as needed to avoid always appending when interaction_id == 0.

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 080b2a2. request_id is now NonEmptyStr | None, UUID generation only happens for None, and duplicate caller-supplied request IDs are rejected before Request or Interaction writes. Added tests for empty IDs and duplicate publish attempts.

@yilu331
yilu331 merged commit d80716f into main Jun 8, 2026
yilu331 added a commit that referenced this pull request Jun 9, 2026
## Summary
- Mark active extraction agent runs failed when extractor service
timeout handling fires for a request.
- Guard resumable extraction completion updates with expected active
statuses so late LLM/tool output cannot commit after a run is already
failed.
- Add SQLite storage and resumable-agent regression coverage for
status-guarded updates and late-output discard.

## Context
This is the focused replacement for the useful remaining backend
behavior from the now-closed #131. Prompt tuning, request-id, and
hard-timeout work already landed separately in #136.

## Tests
- `uv run python -c "import reflexio"`
- `uv run ruff check reflexio/server/services/base_generation_service.py
reflexio/server/services/extraction/resumable_agent.py
reflexio/server/services/storage/sqlite_storage/_agent_run.py
reflexio/server/services/storage/storage_base/_agent_run.py
tests/server/services/extraction/test_resumable_agent.py
tests/server/services/storage/sqlite_storage/test_agent_run_storage.py`
- `uv run pyright reflexio/server/services/base_generation_service.py
reflexio/server/services/extraction/resumable_agent.py
reflexio/server/services/storage/sqlite_storage/_agent_run.py
reflexio/server/services/storage/storage_base/_agent_run.py
tests/server/services/extraction/test_resumable_agent.py
tests/server/services/storage/sqlite_storage/test_agent_run_storage.py`
- `uv run pytest
tests/server/services/extraction/test_resumable_agent.py
tests/server/services/storage/sqlite_storage/test_agent_run_storage.py
-q --no-cov`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Bug Fixes
* Enhanced extraction timeout management to properly mark failed
extraction runs in the system
* Prevents processing and committing outputs that arrive after an
extraction operation has timed out and been marked as failed

## Tests
* Added tests to verify extraction timeout handling and validation of
late-output rejection behavior

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@yilu331
yilu331 deleted the feature/swe-bench-prompt-tuning-output-guidance branch June 12, 2026 07:11
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