Skip to content

perf: reduce chat latency — dedup, prewarm, parallelism, caching - #533

Merged
beng360 merged 24 commits into
mainfrom
feat/latency-instrumentation
Jun 22, 2026
Merged

perf: reduce chat latency — dedup, prewarm, parallelism, caching#533
beng360 merged 24 commits into
mainfrom
feat/latency-instrumentation

Conversation

@OlivierTrudeau

@OlivierTrudeau OlivierTrudeau commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

What this does

Fixes the slow chat replies we've been seeing on Slack and UI (10-30s down to ~5s consistent).

The main problems were:

  1. Celery was running the same task twiceacks_late=True + Redis broker caused redelivery while the task was still executing. This made Slack replies "change" after appearing.
  2. Cold starts on every other message — Worker child processes weren't pre-warmed, so each new child paid a ~4.6s import + init penalty on its first task.
  3. Sequential operations that could be parallel — Guardrails, prompt assembly, and tool loading were all blocking each other unnecessarily.

Changes

Reliability fixes:

  • Add Redis SETNX dedup lock so duplicate task deliveries are caught and skipped immediately
  • Add exec to celery worker command so the process gets SIGTERM directly from K8s (prevents ghost consumers after deploys)

Performance:

  • Synchronous worker_process_init pre-warms guardrails, MCP, and Agent singleton on every child fork — no more cold starts even when max_tasks_per_child recycles workers
  • Guardrails check_input fires concurrently with prompt assembly and tool loading (only blocks right before the LLM call)
  • build_prompt_segments and get_cloud_tools run in parallel via ThreadPoolExecutor
  • Slack "Thinking..." message and channel context fetch run in parallel
  • Fixed tool cache key that included id(tool_capture) — guaranteed a cache miss every single message

Instrumentation:

  • [LATENCY] logs at every stage: queue wait, agent init, guardrails, prompt build, LLM TTFT, Slack send, E2E totals
  • Startup DNS resolution check and environment logging for K8s debugging

Summary by CodeRabbit

  • Performance Improvements

    • Celery workers now prewarm guardrails and other agent resources to improve first-response time.
    • Agent prompt/tool preparation is more concurrent, and cloud tool wrappers are reused more reliably.
    • Background chats reuse a per-worker agent and wait for worker readiness.
  • Reliability

    • Background chat runs are deduplicated to prevent duplicate executions.
    • Safer handling of concurrent tool calls reduces race-related issues.
  • Slack Integration

    • “Thinking...” messages post to the correct thread earlier, with improved context and fewer duplicate updates.
  • Safety

    • Input-rail checks no longer delay message persistence, while streaming is still blocked when required.

@OlivierTrudeau
OlivierTrudeau requested a review from a team as a code owner June 18, 2026 19:53
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4414efc4-749e-4677-bf9e-110dd8978fb3

📥 Commits

Reviewing files that changed from the base of the PR and between fa09c93 and a1c5be3.

📒 Files selected for processing (1)
  • server/celery_config.py

Walkthrough

The PR adds a per-worker prewarm mechanism that initializes NeMo Guardrails, the MCP preloader, and a singleton Agent on Celery worker startup. Input guardrail evaluation is moved from a synchronous pre-stream gate to a concurrent background asyncio.Task. Cloud tool wrappers are re-keyed with stable cache identifiers and made safe for concurrent capture access. Background tasks gain Redis deduplication and Slack responses can be sent early inside the async workflow.

Changes

Worker Performance, Concurrent Guardrails & Background Task Improvements

Layer / File(s) Summary
Deployment: exec celery PID takeover
deploy/helm/aurora/templates/celery-worker-deployment.yaml
Container command updated to sh -c 'exec celery ...' so Celery replaces the shell as the active process.
Worker prewarm hook, guardrails sync prewarm, and singleton Agent
server/celery_config.py, server/guardrails/input_rail.py, server/chat/background/task.py
celery_config.py registers a worker_process_init handler launching a daemon thread to pre-warm Guardrails via _ensure_rails_in_thread(), MCP preloader, and Agent. input_rail.py adds _rails_thread_lock, _record_init_failure(), and _ensure_rails_in_thread() to coordinate sync and async initialization under a backoff gate. task.py introduces _get_worker_agent() with double-checked locking for a per-worker singleton Agent.
Background task deduplication, singleton reuse, and early Slack send
server/chat/background/task.py
run_background_chat waits on _prewarm_ready and acquires a Redis SETNX dedup lock (returning "deduplicated" on collision, deleting in finally). _execute_background_chat reuses the singleton Agent and removes explicit Weaviate close. Slack responses can be sent early inside the async workflow; _send_response_to_slack is refactored to return bool, returning False on missing prerequisites and True after sending.
Concurrent deferred input guardrails and prompt parallelization
server/chat/backend/agent/workflow.py, server/chat/backend/agent/agent.py
workflow.py adds _guardrail_task_var ContextVar and replaces the blocking input-rail gate with a background asyncio.create_task, persisting the user message immediately without awaiting guardrail completion. agent.py awaits the guardrail task before streaming and returns early when blocked; prompt-segment building is parallelized on a ThreadPoolExecutor while cloud tools are fetched on the main thread.
Stable cloud-tool cache and lock-safe capture wrapper
server/chat/backend/agent/tools/cloud_tools.py
get_cloud_tools() replaces the id(tool_capture)-based cache key with a stable "capture"/"nocapture" tag plus context-flags key. wrap_func_with_capture resolves the active capture dynamically at call time. All current_tool_calls iteration paths (success-path signature match and fallback, sequential fallback, error-path signature match and "exactly one incomplete" fallback) now snapshot under tool_capture.lock.
Slack event dispatch: parallelization and dispatch_ts
server/routes/slack/slack_events.py, server/routes/slack/slack_events_helpers.py
Threaded replies now send "Thinking…" before loading session/thread context sequentially; top-level mentions parallelize "Thinking…" with channel-context fetch via ThreadPoolExecutor. send_message_to_aurora gains `dispatch_ts: float

Sequence Diagram(s)

sequenceDiagram
  participant CeleryWorker
  participant _prewarm_worker
  participant _ensure_rails_in_thread
  participant _get_worker_agent
  participant _prewarm_ready
  participant run_background_chat
  participant RedisLock
  participant _execute_background_chat

  rect rgba(173, 216, 230, 0.5)
    note over CeleryWorker, _prewarm_ready: Worker startup prewarm
    CeleryWorker->>_prewarm_worker: worker_process_init
    _prewarm_worker->>_ensure_rails_in_thread: build LLMRails under _rails_thread_lock
    _prewarm_worker->>_get_worker_agent: create Agent singleton
    _prewarm_worker->>_prewarm_ready: set()
  end

  rect rgba(144, 238, 144, 0.5)
    note over run_background_chat, _execute_background_chat: Task execution with dedup guard
    run_background_chat->>_prewarm_ready: wait(timeout=30)
    run_background_chat->>RedisLock: SETNX(task_id)
    alt already running
      run_background_chat-->>run_background_chat: return deduplicated
    else acquired
      run_background_chat->>_execute_background_chat: run with singleton Agent
    end
  end
Loading
sequenceDiagram
  participant Workflow
  participant _guardrail_task_var
  participant check_input
  participant MessageStore
  participant agentic_tool_flow
  participant ThreadPoolExecutor

  Workflow->>check_input: asyncio.create_task(check_input(msg_text))
  Workflow->>_guardrail_task_var: store task
  Workflow->>MessageStore: handle_immediate_save (optimistic)
  Workflow->>agentic_tool_flow: begin streaming
  par prompt preparation
    ThreadPoolExecutor->>ThreadPoolExecutor: build_prompt_segments(...)
  and tool preparation
    agentic_tool_flow->>agentic_tool_flow: get_cloud_tools()
  end
  agentic_tool_flow->>_guardrail_task_var: await task result
  alt blocked
    agentic_tool_flow-->>Workflow: emit_block_event, guardrail_blocked=True, return early
  else allowed
    agentic_tool_flow-->>Workflow: stream tokens to client
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • Arvo-AI/aurora#272: Both PRs modify server/chat/backend/agent/workflow.py around early per-turn UI persistence—main PR changes the guardrail gating to call handle_immediate_save immediately, while PR #272 changes handle_immediate_save and Workflow.stream's persistence behavior to be append-only—so the diffs are directly connected at the message-saving call/site and surrounding workflow control flow.
  • Arvo-AI/aurora#333: Both PRs modify the guardrails input-rail flow by changing server/chat/backend/agent/workflow.py's Workflow.stream blocked-input handling and extending server/guardrails/input_rail.py to affect check_input behavior/reasoning, so the main PR's guardrail scheduling integrates with the retrieved PR's guardrail categorization/UI behavior.
  • Arvo-AI/aurora#439: Both PRs modify server/chat/background/task.py's Celery/background chat execution flow—run_background_chat/_execute_background_chat session/incident finalization and surrounding control logic—so the changes are code-level related.

Suggested reviewers

  • beng360

🐇 Exec! The shell steps aside,
A prewarm thread wakes up inside.
Guards check in the background now,
Deduplicated — take a bow!
Slack replies early, tools stay cached,
A faster bunny, nothing's thrashed!

🚥 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 concisely captures the main performance improvements: deduplication, worker pre-warming, parallelism, and caching—all of which are the core optimizations addressed across the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/latency-instrumentation

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.

Comment thread server/utils/secrets/secret_ref_utils.py Fixed
Comment thread server/celery_config.py Fixed
Comment thread server/routes/slack/slack_events.py Fixed
@OlivierTrudeau
OlivierTrudeau force-pushed the feat/latency-instrumentation branch from 1446a29 to 4bb4c94 Compare June 18, 2026 19:56

@aurora-test-app1 aurora-test-app1 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.

Aurora Risk Review

Verdict: RISKY

This PR introduces three deployment-day risks: a CI environment-variable validation gate that is actively failing on the head SHA (confirmed run ID 27785708973), a guardrail safety regression where optimistic message persistence combined with a silently-swallowed delete failure leaves blocked messages in LLM context, and a fragile token-count heuristic that mislabels LLM stream errors as guardrail blocks. The latency improvements themselves are sound, but these three issues need resolution before merge.

Findings

# Severity File Finding
1 HIGH .github/workflows/validate-env-vars.yml:1 Validate Environment Variables CI gate is actively failing on the head SHA
2 HIGH server/chat/backend/agent/workflow.py:1082 Optimistic message persist + silent delete failure leaves blocked messages in LLM context
3 MEDIUM server/chat/backend/agent/workflow.py:1316 Zero-token heuristic conflates LLM stream errors with guardrail blocks, showing wrong error message to users

Aurora reviews PRs for incident prevention. This is advisory only and does not block merge.

Comment thread server/chat/backend/agent/workflow.py Outdated
Comment thread server/chat/backend/agent/workflow.py

@aurora-test-app1 aurora-test-app1 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.

Aurora Risk Review — Latest changes

Verdict: SAFE

No new incident risk in the latest changes.


Aurora reviews PRs for incident prevention.

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

Actionable comments posted: 13

Caution

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

⚠️ Outside diff range comments (2)
server/chat/backend/agent/tools/cloud_tools.py (1)

1118-1162: ⚠️ Potential issue | 🟠 Major

Protect reads/writes of _tc.current_tool_calls with _tc.lock in matching paths.

Multiple iterations over _tc.current_tool_calls at lines 1118–1162 (and again at lines 1211–1221, 1250–1253) lack lock protection while mutations occur outside the lock. This contradicts the protected access pattern already used elsewhere in the wrapper (line 1095+ for placeholder updates). With concurrent tool callbacks and cleanup threads, unprotected dictionary iteration can throw RuntimeError: dictionary changed size during iteration, and mutations can race with deletions.

Apply the snapshot pattern used in the codebase: acquire _tc.lock, snapshot current_tool_calls.items() to a list, then iterate; protect all mutations with the lock as well.

Pattern example
- for call_id, call_info in _tc.current_tool_calls.items():
+ with _tc.lock:
+     calls_snapshot = list(_tc.current_tool_calls.items())
+ for call_id, call_info in calls_snapshot:
    ...

- call_info = _tc.current_tool_calls[matching_tool_call_id]
- call_info['input'] = signature_payload
+ with _tc.lock:
+     call_info = _tc.current_tool_calls.get(matching_tool_call_id)
+     if call_info:
+         call_info['input'] = signature_payload
🤖 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 `@server/chat/backend/agent/tools/cloud_tools.py` around lines 1118 - 1162, The
multiple iterations over _tc.current_tool_calls dictionary (in the for loops
checking signatures, tool names, and candidates from lines 1118-1162) lack lock
protection, creating race conditions with concurrent mutations. Wrap all reads
from _tc.current_tool_calls with _tc.lock by acquiring the lock before the
iterations, converting _tc.current_tool_calls.items() to a list snapshot to
prevent RuntimeError during concurrent modifications, and ensure all mutations
to call_info dictionaries (such as setting call_info['input'] and
call_info['signature']) also occur within the locked section. Follow the same
protected access pattern already used elsewhere in the wrapper for consistency.
server/routes/slack/slack_events_helpers.py (1)

691-696: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Capture dispatch_ts at the Celery enqueue boundary.

The worker logs time.time() - trigger_metadata["dispatch_ts"] as Celery queue wait, but the current caller captures this before send_message_to_aurora() may create a session and prepare metadata. That helper/database time will be misreported as queue wait; set dispatch_ts immediately before .delay() instead.

Proposed fix
-        if dispatch_ts:
-            trigger_metadata["dispatch_ts"] = dispatch_ts
-        
         # Launch background chat task
         # For Slack `@mentions`, link to incident but don't send "investigation started" notifications
+        import time as _aurora_time
+        trigger_metadata["dispatch_ts"] = _aurora_time.time()
         run_background_chat.delay(
🤖 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 `@server/routes/slack/slack_events_helpers.py` around lines 691 - 696, The
dispatch_ts is currently being set too early in the flow, before
send_message_to_aurora() executes, which means any time spent in that helper
function gets incorrectly attributed to Celery queue wait time in worker logs.
Move the assignment of trigger_metadata["dispatch_ts"] = dispatch_ts to
immediately before the run_background_chat.delay() call so that the timestamp
accurately captures only the queue wait time, not the pre-enqueueing processing
time.
🤖 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 `@server/celery_config.py`:
- Around line 256-298: The _prewarm_worker function connected to
worker_process_init is performing blocking operations that exceed Celery's
4-second timeout, causing worker processes to be terminated. Move the heavy
initialization work (NeMo Guardrails via _build_rails_sync, MCP preloader via
start_mcp_preloader, and Agent singleton via _get_worker_agent) out of the
worker_process_init signal handler and into a background warmup task that runs
asynchronously or implement lazy initialization that occurs on first task
execution instead of blocking the worker startup process.

In `@server/chat/backend/agent/agent.py`:
- Around line 347-360: The get_cloud_tools() call is being executed in a
ThreadPoolExecutor worker thread, but it depends on thread-local user context
that was set on the main thread via set_user_context() and set_tool_capture().
Since thread-local context doesn't propagate to worker threads,
get_cloud_tools() returns tools with default context instead of the intended
capture/cache behavior. Remove the _tools_future submission from the thread pool
and instead call get_cloud_tools() directly on the current thread after
obtaining the segments result from _prompt_future.result(). Keep only the
build_prompt_segments call in the ThreadPoolExecutor worker.

In `@server/chat/backend/agent/providers/bedrock_provider.py`:
- Around line 148-155: The cache key construction at line 148 in the
BedrockProvider class only includes native_model, region, and temperature, but
ignores other kwargs that remain after the streaming parameter is removed at
line 143. These remaining kwargs are later applied to the model config before
instantiation, meaning different runtime kwargs will incorrectly reuse a stale
cached model_instance. Include the effective kwargs (all kwargs except
streaming) in the cache_key construction at line 148, or alternatively bypass
the cache check when non-streaming kwargs are present.
- Around line 150-158: The BedrockProvider class accesses the shared
_native_client_cache dictionary without synchronization, creating race
conditions in multithreaded Flask environments. Add a class-level threading lock
to BedrockProvider (e.g., _cache_lock), then wrap all accesses to
_native_client_cache with this lock using a context manager. This includes the
read-check-evict block around lines 150-157 where the cache is checked and
entries are deleted, and the write operation around line 184 where new clients
are stored in the cache. Ensure every read, delete, and write operation on
_native_client_cache is protected by acquiring the lock to prevent concurrent
modifications that could cause KeyError exceptions or duplicate client
instantiations.

In `@server/chat/backend/agent/utils/immediate_save_handler.py`:
- Around line 57-72: The immediate_save_handler code performs an unprotected
read and blindly removes the last user message from the messages array, which
can delete the wrong message if concurrent operations modify the chat_sessions
row (another tab appends a message, or _append_new_turn_ui_messages writes
simultaneously). Fix this by adding a FOR UPDATE clause to the SELECT query to
lock the row before modification, and modify the function to accept a
current-turn message identifier (generated message id is preferred over text)
passed from the caller in agentic_tool_flow. Instead of popping the last user
message, iterate through the messages array to find and remove only the message
that matches the provided identifier, ensuring the correct message is rolled
back even under concurrent access.

In `@server/chat/backend/agent/workflow.py`:
- Around line 1072-1077: The guardrail check via check_input is only enforced in
the agentic_tool_flow path, but the workflow can route to alternative paths like
triage before direct_react (around lines 180-188) which bypass the guardrail
enforcement and allow unvetted input to reach LLM nodes. Add a graph-level
guardrail gate that runs before _route_start to ensure all input paths are
protected, or alternatively make every first-hop node (agentic_tool_flow,
triage, direct_react) await the guardrail task stored in _guardrail_task_var
before executing any LLM calls.
- Around line 1320-1343: The guardrail fallback path in the code starting around
line 1320 is not fail-closed because it treats pending or errored guardrail
tasks as None, potentially ignoring slow or failed safety checks. Replace the
conditional check for _guardrail_task.done() with an unconditional await of
_guardrail_task.result(), and modify the exception handling to fail closed
(raise or yield an error message) instead of silently setting rail_result to
None. Additionally, ensure that when guardrails block the message in this
fallback path, the code sets the guardrail_blocked flag and performs the same
audit/rollback/state cleanup that happens in the main agentic_tool_flow branch,
rather than just yielding a message and returning immediately.

In `@server/chat/background/task.py`:
- Around line 47-65: The cached PostgreSQLClient created in _get_worker_agent()
reuses the same database connection across multiple background tasks without
resetting the RLS context for each task, which can cause different users' tasks
to see incorrect or stale data. In the _execute_background_chat() function,
after calling _get_worker_agent() to retrieve the cached agent, you must call
set_rls_context() from utils.auth.stateless_auth to set the current user and
organization context on the PostgreSQLClient's connection before the workflow
executes any database operations. This ensures each task runs with the correct
RLS context for its user.
- Line 1344: Remove the f-string prefix from the logger.info call on line 1344
since the string is constant with no variable interpolation. Additionally,
locate the logging call in the except block around lines 1489-1490 and replace
the standard logger call (such as logger.error) with logger.exception() to
properly capture and log exception information within the exception handler.
- Around line 486-494: The current deduplication logic in the task method only
checks if the Redis key exists but doesn't verify whether the original task
execution is actually still running or has already completed. This can cause the
task to return a "deduplicated" status prematurely even if the worker crashed
after acquiring the lock, leaving the session stuck. Instead of storing just "1"
in the _dedup_key, store an owner identifier or heartbeat timestamp. When the
dedup key exists, check if the owner/heartbeat indicates the original task is
verifiably still running (within a reasonable heartbeat window) or has completed
successfully. Only skip execution and return the "deduplicated" status if you
can confirm one of these conditions; otherwise, allow the task to proceed as a
legitimate retry. This ensures that genuinely concurrent duplicates are still
prevented while crashed tasks can be properly retried.

In `@server/main_chatbot.py`:
- Around line 1672-1685: The REDIS_URL logging in the startup environment info
block is a security risk because slicing to 30 characters still exposes
authentication credentials in the format `redis://:password@...`. Remove the
slicing logic on the REDIS_URL environment variable and instead log only the
presence or absence of the URL (such as "set" or "unset"), or parse the URL and
log only safe components like the scheme, host, and port while completely
excluding any credentials.

In `@server/routes/slack/slack_events_helpers.py`:
- Around line 630-631: The function signature containing the parameters
channel_context, thinking_message_ts, and dispatch_ts has implicit None defaults
without explicit type union syntax, violating Ruff RUF013. Update the type
annotations for these parameters to explicitly include None in the union (e.g.,
change str to str | None and float to float | None). At minimum, fix the
parameters on the changed lines 630-631, but ideally update the entire function
signature starting at line 628 for consistency.

In `@server/routes/slack/slack_events.py`:
- Line 183: In the logger.info call that logs the `@Aurora` mention processing,
remove the raw text snippet `{text[:100]}` from the log message as it may expose
secrets or sensitive customer data. Replace the f-string logging with lazy
logging by using the old-style % formatting or passing parameters separately to
the logger.info method, so that string interpolation only occurs if the log
level is actually enabled. Keep only the metadata fields like channel and
final_thread_ts in the log output.

---

Outside diff comments:
In `@server/chat/backend/agent/tools/cloud_tools.py`:
- Around line 1118-1162: The multiple iterations over _tc.current_tool_calls
dictionary (in the for loops checking signatures, tool names, and candidates
from lines 1118-1162) lack lock protection, creating race conditions with
concurrent mutations. Wrap all reads from _tc.current_tool_calls with _tc.lock
by acquiring the lock before the iterations, converting
_tc.current_tool_calls.items() to a list snapshot to prevent RuntimeError during
concurrent modifications, and ensure all mutations to call_info dictionaries
(such as setting call_info['input'] and call_info['signature']) also occur
within the locked section. Follow the same protected access pattern already used
elsewhere in the wrapper for consistency.

In `@server/routes/slack/slack_events_helpers.py`:
- Around line 691-696: The dispatch_ts is currently being set too early in the
flow, before send_message_to_aurora() executes, which means any time spent in
that helper function gets incorrectly attributed to Celery queue wait time in
worker logs. Move the assignment of trigger_metadata["dispatch_ts"] =
dispatch_ts to immediately before the run_background_chat.delay() call so that
the timestamp accurately captures only the queue wait time, not the
pre-enqueueing processing time.
🪄 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

Run ID: 842f112d-f6f3-4500-a8ec-30cfca07db99

📥 Commits

Reviewing files that changed from the base of the PR and between 3d80461 and ea0f77d.

📒 Files selected for processing (16)
  • deploy/helm/aurora/templates/celery-worker-deployment.yaml
  • deploy/helm/aurora/templates/chatbot-deployment.yaml
  • server/celery_config.py
  • server/chat/backend/agent/agent.py
  • server/chat/backend/agent/prompt/composer.py
  • server/chat/backend/agent/providers/bedrock_provider.py
  • server/chat/backend/agent/tools/cloud_tools.py
  • server/chat/backend/agent/utils/immediate_save_handler.py
  • server/chat/backend/agent/workflow.py
  • server/chat/background/task.py
  • server/guardrails/input_rail.py
  • server/main_chatbot.py
  • server/routes/slack/slack_events.py
  • server/routes/slack/slack_events_helpers.py
  • server/utils/db/connection_pool.py
  • server/utils/secrets/secret_ref_utils.py

Comment thread server/celery_config.py Outdated
Comment thread server/chat/backend/agent/agent.py Outdated
Comment thread server/chat/backend/agent/providers/bedrock_provider.py Outdated
Comment thread server/chat/backend/agent/providers/bedrock_provider.py Outdated
Comment thread server/chat/backend/agent/utils/immediate_save_handler.py Outdated
Comment thread server/chat/background/task.py
Comment thread server/chat/background/task.py Outdated
Comment thread server/main_chatbot.py Outdated
Comment thread server/routes/slack/slack_events_helpers.py Outdated
Comment thread server/routes/slack/slack_events.py Outdated

@aurora-test-app1 aurora-test-app1 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.

Aurora Risk Review — Latest changes

Verdict: SAFE

No new incident risk in the latest changes.


Aurora reviews PRs for incident prevention.

@aurora-test-app1 aurora-test-app1 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.

Aurora Risk Review — Latest changes

Verdict: SAFE

No new incident risk in the latest changes.


Aurora reviews PRs for incident prevention.

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

Caution

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

⚠️ Outside diff range comments (1)
server/chat/backend/agent/agent.py (1)

877-877: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove unused imports from guardrails module.

_BLOCKED_REASON, _FAIL_CLOSED_AUTH, and _FAIL_CLOSED_CONNECTIVITY are imported but never used in this function.

🧹 Proposed fix
-                                from guardrails.input_rail import InputRailResult, _BLOCKED_REASON, _FAIL_CLOSED_AUTH, _FAIL_CLOSED_CONNECTIVITY
+                                from guardrails.input_rail import InputRailResult
🤖 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 `@server/chat/backend/agent/agent.py` at line 877, Remove the three unused
imports from the guardrails.input_rail import statement on line 877 of agent.py.
The constants _BLOCKED_REASON, _FAIL_CLOSED_AUTH, and _FAIL_CLOSED_CONNECTIVITY
are not used anywhere in the function and should be deleted from the import
statement, keeping only InputRailResult which is actively used.
🤖 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.

Outside diff comments:
In `@server/chat/backend/agent/agent.py`:
- Line 877: Remove the three unused imports from the guardrails.input_rail
import statement on line 877 of agent.py. The constants _BLOCKED_REASON,
_FAIL_CLOSED_AUTH, and _FAIL_CLOSED_CONNECTIVITY are not used anywhere in the
function and should be deleted from the import statement, keeping only
InputRailResult which is actively used.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f05f5a85-acbf-42f9-ac65-55aa62d13ed2

📥 Commits

Reviewing files that changed from the base of the PR and between 774257d and 284f48e.

📒 Files selected for processing (4)
  • server/celery_config.py
  • server/chat/backend/agent/agent.py
  • server/chat/background/task.py
  • server/routes/slack/slack_events.py

OlivierTrudeau and others added 17 commits June 18, 2026 17:19
Adds [LATENCY]-prefixed timing logs across the full chat message
pipeline to diagnose ~30s response times in the deployed K8s
environment. Instruments: auth/provider resolution, guardrails
input rail, prompt building, tool loading, LLM model creation,
Bedrock instantiation, Vault secret fetches, DB pool acquisition,
and end-to-end time-to-first-token. Also adds POD_NAME/NODE_NAME
env vars to the chatbot Helm deployment for pod identification.

Co-authored-by: Cursor <cursoragent@cursor.com>
The _pwa_first_token_logged variable needs nonlocal declaration
to be accessible from the nested process_stream closure.

Co-authored-by: Cursor <cursoragent@cursor.com>
…allelism)

- Cache ChatBedrockConverse instances per (model, region, temp) to skip
  repeated boto3 session + TLS handshake (~300-570ms savings per message)
- Per-worker singleton Agent in Celery to avoid recreating PostgreSQLClient,
  WeaviateClient, and LLMManager on every background task (~2s savings)
- Parallelize build_prompt_segments + get_cloud_tools in agentic_tool_flow
  (overlaps CPU-bound prompt assembly with tool loading)
- Parallelize Slack "Thinking..." message send with channel context fetch
  using ThreadPoolExecutor (~800ms savings on new top-level messages)

Co-authored-by: Cursor <cursoragent@cursor.com>
Fire the NeMo Guardrails input rail as a concurrent asyncio.Task instead of
awaiting it synchronously. The guardrails LLM call (~1.2s) now runs in
parallel with provider resolution, tool loading, prompt assembly, and LLM
client creation inside agentic_tool_flow.

Messages are persisted optimistically on receipt; if guardrails blocks, the
persisted message is rolled back via delete_last_saved_message. The LLM is
never reached when blocked — safety guarantees are preserved.

Uses a contextvars.ContextVar to pass the pending task from workflow.stream()
to agentic_tool_flow without modifying the Pydantic State schema.

Co-authored-by: Cursor <cursoragent@cursor.com>
… dedup

- Make worker_process_init prewarm synchronous so children are fully warm
  before accepting tasks (was a daemon thread that raced with incoming tasks)
- Remove verbose TOOL CAPTURE debug logging from cloud_tools.py
- Simplify dedup guard Redis access (backend.client is always available)
- Fix persist condition in workflow.py to match original behavior (save even
  when _rail_msg_text is empty, matching pre-refactor logic)
- Clean up DNS check in main_chatbot.py startup

Co-authored-by: Cursor <cursoragent@cursor.com>
- Remove secret ref from latency log (CodeQL clear-text sensitive data)
- Use single import style for guardrails.input_rail (code quality)
- Remove unused sent_msg assignment in non-background path (code quality)

Co-authored-by: Cursor <cursoragent@cursor.com>
The Bedrock client cache saved ~300-570ms by reusing boto3 sessions,
but production runs on OpenRouter where ChatOpenAI instantiation is
already cheap (~10ms). Reverting to keep the diff minimal and avoid
dead code for the active provider path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Debugging-only code that adds noise to the diff without lasting value.

Co-authored-by: Cursor <cursoragent@cursor.com>
Blocked messages sitting in chat history are harmless — it's what the
user typed. The optimistic persist doesn't need a rollback path.

Co-authored-by: Cursor <cursoragent@cursor.com>
- Move prewarm to background thread with Event gate (Celery 4s timeout)
- Keep get_cloud_tools on main thread (needs thread-local context)
- Fix constant f-string lint warning
- Remove user message text from Slack processing log (privacy)

Co-authored-by: Cursor <cursoragent@cursor.com>
- Guardrail fallback now fails closed on exceptions (was silently ignoring)
- Use logger.exception for early Slack send failure
- Add Optional type annotations on dispatch_ts parameter

Co-authored-by: Cursor <cursoragent@cursor.com>
@OlivierTrudeau
OlivierTrudeau force-pushed the feat/latency-instrumentation branch from 0bf1b32 to d3aef04 Compare June 18, 2026 21:21
@aurora-test-app1

Copy link
Copy Markdown

🔍 Aurora is reviewing this PR for incident risk. This usually takes a minute or two — findings will appear as a review when it's done.

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (3)
server/chat/backend/agent/tools/cloud_tools.py (3)

1057-1058: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize None values the same way as workflow signatures.

Workflow._build_tool_signature() drops None values, but this wrapper keeps them. Optional defaults like output_file=None can miss exact matching and force unsafe fallbacks.

Proposed fix
     def _sanitize_kwargs_for_signature(kwargs: Dict[str, Any]) -> Dict[str, Any]:
-        return {k: v for k, v in kwargs.items() if k not in INTERNAL_CONTEXT_KEYS}
+        return {
+            k: v
+            for k, v in kwargs.items()
+            if k not in INTERNAL_CONTEXT_KEYS and v is not None
+        }
🤖 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 `@server/chat/backend/agent/tools/cloud_tools.py` around lines 1057 - 1058, The
`_sanitize_kwargs_for_signature` function currently filters out internal context
keys but does not drop `None` values, while `Workflow._build_tool_signature()`
drops them during normalization. This mismatch causes optional parameters with
`None` defaults (like `output_file=None`) to fail exact matching. Modify the
dictionary comprehension in `_sanitize_kwargs_for_signature` to exclude both
keys present in `INTERNAL_CONTEXT_KEYS` and any entries where the value is
`None`, ensuring consistency with the workflow signature building behavior.

1137-1164: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not attach output to an arbitrary oldest call when matching is ambiguous.

When multiple incomplete calls for the same tool remain, choosing the oldest can persist a successful output under the wrong tool_call_id. Mirror the error path’s fail-safe behavior unless there is an explicit sequential-execution signal.

Proposed fix
                         if len(candidate_ids) == 1:
                             matching_tool_call_id = candidate_ids[0][0]
                             call_info = _tc.current_tool_calls[matching_tool_call_id]
                             call_info['input'] = signature_payload
                             call_info['signature'] = tool_signature
                             logging.info(
                                 "Matched tool call by single incomplete candidate: %s (updated signature)",
                                 matching_tool_call_id,
                             )
                         elif len(candidate_ids) > 1:
-                            candidate_ids.sort(key=lambda x: x[1] if x[1] else datetime.min)
-                            matching_tool_call_id = candidate_ids[0][0]
-                            call_info = _tc.current_tool_calls[matching_tool_call_id]
-                            call_info['input'] = signature_payload
-                            call_info['signature'] = tool_signature
-                            logging.warning(
-                                f"SEQUENTIAL FALLBACK: Found {len(candidate_ids)} incomplete {tool_name} calls, "
-                                f"matched to oldest: {matching_tool_call_id}. "
-                                f"This is expected for OpenAI sequential execution."
-                            )
+                            logging.error(
+                                "Ambiguous %s completion: %d incomplete calls remain and no signature matched; "
+                                "leaving capture unmatched rather than misattributing output",
+                                tool_name,
+                                len(candidate_ids),
+                            )
🤖 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 `@server/chat/backend/agent/tools/cloud_tools.py` around lines 1137 - 1164, In
the branch where len(candidate_ids) > 1 in the tool call matching logic, the
code currently sorts candidates by start_time and assigns the output to the
oldest incomplete call without verification. This can incorrectly attach output
to the wrong tool_call_id. Instead of assigning to the oldest candidate,
implement a fail-safe approach that handles the ambiguous matching case
(multiple incomplete calls of the same tool with no clear match) by avoiding
arbitrary assignment. Look for how error cases are handled elsewhere in the
matching logic to mirror similar fail-safe behavior when sequential execution
cannot be definitively determined.

1292-1334: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Apply the PR-review read-only gate to all write-capable tools.

The new gate removes iac_tool, cloud_exec, github_commit, tailscale_ssh, and kubectl, but PR review can still receive write-capable tools later in this function, including GitLab write actions, Bitbucket mutators, Jira mutators, SharePoint page creation, and cloudflare_action.

Also applies to: 2031-2156, 2235-2311, 2438-2477

🤖 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 `@server/chat/backend/agent/tools/cloud_tools.py` around lines 1292 - 1334, The
PR review read-only gate using the `not is_pr_review` check has been
inconsistently applied across write-capable tools in this function. While
`iac_tool`, `cloud_exec`, `github_commit`, and `tailscale_ssh` are properly
gated, other write-capable tools later in the function (including GitLab write
actions, Bitbucket mutators, Jira mutators, SharePoint page creation, and
`cloudflare_action`) are missing this protection. For each of these unprotected
write-capable tools, add `and not is_pr_review` to their conditional checks to
ensure they cannot be used during PR review mode, maintaining consistency with
the existing patterns used for other write tools in the function.
🤖 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 `@server/chat/backend/agent/tools/cloud_tools.py`:
- Around line 1024-1034: The cache key construction in the code starting around
the cache_key variable assignment is missing the trigger_action_id value, which
can cause different trigger actions for the same user and mode to incorrectly
reuse the same cached entry. Extract the trigger_action_id from the
state_context (similar to how other attributes like trigger_rca_requested and
is_background are extracted using getattr with a default value), and then
include this value in the cache_key string template alongside the other
parameters like rca_flag, is_background, is_postmortem_action, is_pr_review, and
is_rca_context to ensure each unique action has a unique cache entry.
- Around line 1037-1044: The cache hit in the caching logic (returning early
from _langchain_tools_cache) bypasses the connector capability and connectivity
checks that occur later in the function, allowing stale tools to be exposed
after credentials are removed or org-scoped changes occur. To fix this, either
incorporate connector and organization capability version information into the
cache_key itself so that cache invalidation happens automatically when
capabilities change, or ensure that is_<connector>_connected(user_id) checks are
performed even on cache hits before returning the cached tools. This ensures
agent tools are always gated behind proper connectivity validation regardless of
whether the result comes from cache or not. Apply this same validation pattern
to all other cache hit locations mentioned in the same file.
- Around line 1115-1123: The code iterates over _tc.current_tool_calls in
multiple places without holding _tc.lock, creating a race condition where other
tool executions can modify or delete entries during iteration. To fix this,
acquire _tc.lock before creating a snapshot of _tc.current_tool_calls (e.g., via
.copy() or list()), then release the lock and iterate over the snapshot for
matching logic. Apply this pattern to all three locations where
current_tool_calls is iterated: around the first loop checking signature
matching, the fallback loop matching by tool_name and command, and any other
iteration loops in the specified line ranges (1141-1149, 1208-1218). When you
need to mutate state based on the matched result, re-acquire _tc.lock before
accessing or modifying _tc.current_tool_calls again.

In `@server/chat/background/task.py`:
- Around line 47-65: The _get_worker_agent() function lacks thread
synchronization, allowing multiple threads to simultaneously create separate
Agent instances when called from both the prewarm daemon thread and task
execution threads. Add a global threading.Lock variable (similar to how
_worker_agent is declared globally) and use it to wrap the entire singleton
creation logic in _get_worker_agent() so that only one thread can initialize the
Agent, PostgreSQLClient, and WeaviateClient instances at a time. Acquire the
lock before the null check on _worker_agent and release it after initialization
completes to prevent resource leaks from duplicate instances.
- Around line 1447-1448: The `_slack_early_sent` flag is being set to True
unconditionally after calling `_send_response_to_slack()`, even when the
function returns normally without actually sending anything (no channel, no
assistant message, or missing Slack client). This causes the parent code to
incorrectly skip the fallback send. Modify the `_send_response_to_slack()`
function to return a boolean value indicating whether it actually sent a message
successfully, and then only set `_slack_early_sent = True` when the function
returns True, not unconditionally.
- Around line 483-495: The Redis deduplication guard check using the SETNX
operation on `_dedup_key` is currently running after the
`_prewarm_ready.wait(timeout=30)` call, which causes duplicate task redeliveries
to unnecessarily block on prewarm for up to 30 seconds. Move the entire dedup
guard block (the lines that create `_dedup_key`, instantiate `_redis`, and
perform the `_redis.set(_dedup_key, "1", nx=True, ex=1800)` SETNX check) to
execute before the prewarm wait so that duplicate redeliveries fail the dedup
check immediately without occupying worker slots.

---

Outside diff comments:
In `@server/chat/backend/agent/tools/cloud_tools.py`:
- Around line 1057-1058: The `_sanitize_kwargs_for_signature` function currently
filters out internal context keys but does not drop `None` values, while
`Workflow._build_tool_signature()` drops them during normalization. This
mismatch causes optional parameters with `None` defaults (like
`output_file=None`) to fail exact matching. Modify the dictionary comprehension
in `_sanitize_kwargs_for_signature` to exclude both keys present in
`INTERNAL_CONTEXT_KEYS` and any entries where the value is `None`, ensuring
consistency with the workflow signature building behavior.
- Around line 1137-1164: In the branch where len(candidate_ids) > 1 in the tool
call matching logic, the code currently sorts candidates by start_time and
assigns the output to the oldest incomplete call without verification. This can
incorrectly attach output to the wrong tool_call_id. Instead of assigning to the
oldest candidate, implement a fail-safe approach that handles the ambiguous
matching case (multiple incomplete calls of the same tool with no clear match)
by avoiding arbitrary assignment. Look for how error cases are handled elsewhere
in the matching logic to mirror similar fail-safe behavior when sequential
execution cannot be definitively determined.
- Around line 1292-1334: The PR review read-only gate using the `not
is_pr_review` check has been inconsistently applied across write-capable tools
in this function. While `iac_tool`, `cloud_exec`, `github_commit`, and
`tailscale_ssh` are properly gated, other write-capable tools later in the
function (including GitLab write actions, Bitbucket mutators, Jira mutators,
SharePoint page creation, and `cloudflare_action`) are missing this protection.
For each of these unprotected write-capable tools, add `and not is_pr_review` to
their conditional checks to ensure they cannot be used during PR review mode,
maintaining consistency with the existing patterns used for other write tools in
the function.
🪄 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

Run ID: df06d68c-2186-4bd3-9ebf-921055e61e67

📥 Commits

Reviewing files that changed from the base of the PR and between 284f48e and 480b435.

📒 Files selected for processing (8)
  • deploy/helm/aurora/templates/celery-worker-deployment.yaml
  • server/celery_config.py
  • server/chat/backend/agent/agent.py
  • server/chat/backend/agent/tools/cloud_tools.py
  • server/chat/backend/agent/workflow.py
  • server/chat/background/task.py
  • server/routes/slack/slack_events.py
  • server/routes/slack/slack_events_helpers.py

Comment thread server/chat/backend/agent/tools/cloud_tools.py Outdated
Comment thread server/chat/backend/agent/tools/cloud_tools.py
Comment thread server/chat/backend/agent/tools/cloud_tools.py Outdated
Comment thread server/chat/background/task.py
Comment thread server/chat/background/task.py Outdated
Comment thread server/chat/background/task.py Outdated
@OlivierTrudeau
OlivierTrudeau force-pushed the feat/latency-instrumentation branch 3 times, most recently from d94b278 to 2fa5743 Compare June 18, 2026 22:11

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
server/chat/backend/agent/tools/cloud_tools.py (1)

1190-1215: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cap tool output before capturing it.

capture_tool_end() receives output_str before cap_tool_output() runs, so collected ToolMessages—especially on cancellation—can persist the raw oversized output that the returned LangChain tool result avoids.

Proposed fix
+                from chat.backend.agent.utils.tool_output_cap import cap_tool_output
+                result_str = json.dumps(result) if isinstance(result, dict) else str(result)
+                capped_result = cap_tool_output(result_str, tool_name)
+
                 if matching_tool_call_id and tool_capture:
@@
                     if not already_captured:
                         try:
-                            output_str = json.dumps(result) if isinstance(result, dict) else str(result)
-                            tool_capture.capture_tool_end(matching_tool_call_id, output_str, is_error=False)
+                            tool_capture.capture_tool_end(matching_tool_call_id, capped_result, is_error=False)
                             logging.info(f"Wrapper called capture_tool_end for {matching_tool_call_id}")
                         except Exception as capture_error:
                             logging.error(f"Failed to capture tool end in wrapper: {capture_error}")
@@
-                # Cap tool output before returning to LangChain so the ReAct
-                # loop never accumulates oversized ToolMessages.
-                from chat.backend.agent.utils.tool_output_cap import cap_tool_output
-                result_str = json.dumps(result) if isinstance(result, dict) else str(result)
-                result = cap_tool_output(result_str, tool_name)
-
-                return result
+                return capped_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 `@server/chat/backend/agent/tools/cloud_tools.py` around lines 1190 - 1215, The
cap_tool_output function is being called after capture_tool_end, which means the
captured tool end message contains the raw uncapped output while the returned
result is capped. Move the cap_tool_output call and result_str computation
(currently at lines 1210-1212) to execute before the capture_tool_end call
(around line 1194), so that capture_tool_end receives the already-capped
output_str instead of the raw output, ensuring consistency between captured and
returned tool results.
🤖 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 `@server/chat/backend/agent/tools/cloud_tools.py`:
- Around line 1107-1123: Remove the sensitive payload logging statements in the
tool capture block that are exposing raw kwargs, current_tool_calls, call_info
details, and result data. Specifically, remove or replace the logging.info calls
that log the KWARGS, the tool_capture.current_tool_calls dictionary, the
call_info, and the result variable, as these can contain sensitive information
like secrets, commands, or cloud resource details. Replace these with logging
statements that only capture non-sensitive metadata such as tool_name and
call_id for debugging purposes.
- Around line 1165-1170: The sorting lambda in the candidate_ids.sort() call
uses datetime.min as a fallback when x[1] (start_time) is None, but this causes
a TypeError because datetime.min is timezone-naive while start_time is always
timezone-aware (created with datetime.now(timezone.utc)). Replace the
datetime.min fallback with datetime.min.replace(tzinfo=timezone.utc) to ensure
both the fallback value and the actual start_time values are timezone-aware and
can be compared without error.

In `@server/chat/backend/agent/workflow.py`:
- Around line 1075-1082: The synchronous handle_immediate_save() function blocks
the event loop immediately after scheduling the check_input() guardrail task,
preventing the guardrails from running concurrently. Move the
handle_immediate_save() call into a worker thread using asyncio utilities (such
as loop.run_in_executor()) so that the event loop remains available to execute
the guardrails task concurrently with the database persistence operation.
- Around line 1061-1077: The is_scaffold variable is computed to identify
system-authored background RCA messages but is not being used to prevent
guardrail checks. Add is_scaffold to the conditional guard on the line that
schedules the check_input task, so the condition reads if msg_text and not
is_pr_review and not is_scaffold: to skip input rails for scaffold messages just
as PR review messages are skipped.

---

Outside diff comments:
In `@server/chat/backend/agent/tools/cloud_tools.py`:
- Around line 1190-1215: The cap_tool_output function is being called after
capture_tool_end, which means the captured tool end message contains the raw
uncapped output while the returned result is capped. Move the cap_tool_output
call and result_str computation (currently at lines 1210-1212) to execute before
the capture_tool_end call (around line 1194), so that capture_tool_end receives
the already-capped output_str instead of the raw output, ensuring consistency
between captured and returned tool results.
🪄 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

Run ID: f4987ec8-2505-4859-88d7-52c22443f1fd

📥 Commits

Reviewing files that changed from the base of the PR and between 480b435 and 3aa84db.

📒 Files selected for processing (2)
  • server/chat/backend/agent/tools/cloud_tools.py
  • server/chat/backend/agent/workflow.py

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
server/chat/backend/agent/tools/cloud_tools.py (1)

1190-1215: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cap tool output before capturing it.

capture_tool_end() receives output_str before cap_tool_output() runs, so collected ToolMessages—especially on cancellation—can persist the raw oversized output that the returned LangChain tool result avoids.

Proposed fix
+                from chat.backend.agent.utils.tool_output_cap import cap_tool_output
+                result_str = json.dumps(result) if isinstance(result, dict) else str(result)
+                capped_result = cap_tool_output(result_str, tool_name)
+
                 if matching_tool_call_id and tool_capture:
@@
                     if not already_captured:
                         try:
-                            output_str = json.dumps(result) if isinstance(result, dict) else str(result)
-                            tool_capture.capture_tool_end(matching_tool_call_id, output_str, is_error=False)
+                            tool_capture.capture_tool_end(matching_tool_call_id, capped_result, is_error=False)
                             logging.info(f"Wrapper called capture_tool_end for {matching_tool_call_id}")
                         except Exception as capture_error:
                             logging.error(f"Failed to capture tool end in wrapper: {capture_error}")
@@
-                # Cap tool output before returning to LangChain so the ReAct
-                # loop never accumulates oversized ToolMessages.
-                from chat.backend.agent.utils.tool_output_cap import cap_tool_output
-                result_str = json.dumps(result) if isinstance(result, dict) else str(result)
-                result = cap_tool_output(result_str, tool_name)
-
-                return result
+                return capped_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 `@server/chat/backend/agent/tools/cloud_tools.py` around lines 1190 - 1215, The
cap_tool_output function is being called after capture_tool_end, which means the
captured tool end message contains the raw uncapped output while the returned
result is capped. Move the cap_tool_output call and result_str computation
(currently at lines 1210-1212) to execute before the capture_tool_end call
(around line 1194), so that capture_tool_end receives the already-capped
output_str instead of the raw output, ensuring consistency between captured and
returned tool results.
🤖 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 `@server/chat/backend/agent/tools/cloud_tools.py`:
- Around line 1107-1123: Remove the sensitive payload logging statements in the
tool capture block that are exposing raw kwargs, current_tool_calls, call_info
details, and result data. Specifically, remove or replace the logging.info calls
that log the KWARGS, the tool_capture.current_tool_calls dictionary, the
call_info, and the result variable, as these can contain sensitive information
like secrets, commands, or cloud resource details. Replace these with logging
statements that only capture non-sensitive metadata such as tool_name and
call_id for debugging purposes.
- Around line 1165-1170: The sorting lambda in the candidate_ids.sort() call
uses datetime.min as a fallback when x[1] (start_time) is None, but this causes
a TypeError because datetime.min is timezone-naive while start_time is always
timezone-aware (created with datetime.now(timezone.utc)). Replace the
datetime.min fallback with datetime.min.replace(tzinfo=timezone.utc) to ensure
both the fallback value and the actual start_time values are timezone-aware and
can be compared without error.

In `@server/chat/backend/agent/workflow.py`:
- Around line 1075-1082: The synchronous handle_immediate_save() function blocks
the event loop immediately after scheduling the check_input() guardrail task,
preventing the guardrails from running concurrently. Move the
handle_immediate_save() call into a worker thread using asyncio utilities (such
as loop.run_in_executor()) so that the event loop remains available to execute
the guardrails task concurrently with the database persistence operation.
- Around line 1061-1077: The is_scaffold variable is computed to identify
system-authored background RCA messages but is not being used to prevent
guardrail checks. Add is_scaffold to the conditional guard on the line that
schedules the check_input task, so the condition reads if msg_text and not
is_pr_review and not is_scaffold: to skip input rails for scaffold messages just
as PR review messages are skipped.

---

Outside diff comments:
In `@server/chat/backend/agent/tools/cloud_tools.py`:
- Around line 1190-1215: The cap_tool_output function is being called after
capture_tool_end, which means the captured tool end message contains the raw
uncapped output while the returned result is capped. Move the cap_tool_output
call and result_str computation (currently at lines 1210-1212) to execute before
the capture_tool_end call (around line 1194), so that capture_tool_end receives
the already-capped output_str instead of the raw output, ensuring consistency
between captured and returned tool results.
🪄 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

Run ID: f4987ec8-2505-4859-88d7-52c22443f1fd

📥 Commits

Reviewing files that changed from the base of the PR and between 480b435 and 3aa84db.

📒 Files selected for processing (2)
  • server/chat/backend/agent/tools/cloud_tools.py
  • server/chat/backend/agent/workflow.py
🛑 Comments failed to post (4)
server/chat/backend/agent/tools/cloud_tools.py (2)

1107-1123: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove sensitive tool payload logging.

These INFO logs dump raw kwargs, current_tool_calls, and pre-capped result. Tool arguments and outputs can include commands, cloud resources, logs, or secrets; keep only metadata.

Proposed fix
-                    logging.info(f"TOOL CAPTURE: KWARGS: {kwargs}")
                     signature_payload = _sanitize_kwargs_for_signature(kwargs)
@@
-                    logging.info(f"TOOL CAPTURE: Tool capture instance found: {tool_capture}")
-                    logging.info(f"TOOL CAPTURE: Tool capture instance found: {tool_capture.current_tool_calls}")
+                    logging.debug(
+                        "TOOL CAPTURE: matching %s call with %d sanitized kwargs",
+                        tool_name,
+                        len(signature_payload),
+                    )
                     for call_id, call_info in tool_capture.current_tool_calls.items():
-                        logging.info(f"TOOL CAPTURE: Call info: {call_info}")
-                        logging.info(f"TOOL CAPTURE: Call signature right side: {tool_signature}")
-                        logging.info(f"TOOL CAPTURE: Call result: {result}")
                         if call_info.get('signature') == tool_signature:
                             matching_tool_call_id = call_id
🧰 Tools
🪛 ast-grep (0.43.0)

[info] 1109-1109: use jsonify instead of json.dumps for JSON output
Context: json.dumps(signature_payload, sort_keys=True)
Note: Security best practice.

(use-jsonify)

🪛 Ruff (0.15.17)

[warning] 1107-1107: Logging statement uses f-string

(G004)


[warning] 1118-1118: Logging statement uses f-string

(G004)


[warning] 1119-1119: Logging statement uses f-string

(G004)


[warning] 1121-1121: Logging statement uses f-string

(G004)


[warning] 1122-1122: Logging statement uses f-string

(G004)


[warning] 1123-1123: Logging statement uses f-string

(G004)

🤖 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 `@server/chat/backend/agent/tools/cloud_tools.py` around lines 1107 - 1123,
Remove the sensitive payload logging statements in the tool capture block that
are exposing raw kwargs, current_tool_calls, call_info details, and result data.
Specifically, remove or replace the logging.info calls that log the KWARGS, the
tool_capture.current_tool_calls dictionary, the call_info, and the result
variable, as these can contain sensitive information like secrets, commands, or
cloud resource details. Replace these with logging statements that only capture
non-sensitive metadata such as tool_name and call_id for debugging purposes.

1165-1170: ⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm the mixed aware/naive datetime sort failure and the timestamp-based fix.

python - <<'PY'
from datetime import datetime, timezone

items = [("aware", datetime.now(timezone.utc)), ("missing", None)]

try:
    sorted(items, key=lambda x: x[1] if x[1] else datetime.min)
except TypeError as exc:
    print("expected failure:", exc)

fixed = sorted(items, key=lambda x: x[1].timestamp() if x[1] else float("-inf"))
print("fixed order:", [name for name, _ in fixed])
PY

Repository: Arvo-AI/aurora

Length of output: 164


🏁 Script executed:

# Check the actual code at the specified lines
if [ -f "server/chat/backend/agent/tools/cloud_tools.py" ]; then
    wc -l server/chat/backend/agent/tools/cloud_tools.py
    sed -n '1160,1175p' server/chat/backend/agent/tools/cloud_tools.py
else
    echo "File not found"
fi

Repository: Arvo-AI/aurora

Length of output: 1275


🏁 Script executed:

# Search for where start_time is set and its type to confirm if it's timezone-aware
rg -B 5 -A 2 'start_time' server/chat/backend/agent/tools/cloud_tools.py | head -100

Repository: Arvo-AI/aurora

Length of output: 1731


🏁 Script executed:

# Check for datetime imports and usage patterns in the file
rg '(from datetime|import datetime)' server/chat/backend/agent/tools/cloud_tools.py

Repository: Arvo-AI/aurora

Length of output: 88


🏁 Script executed:

# Search for where candidate_ids is populated to understand the structure of x[1]
rg -B 10 'candidate_ids' server/chat/backend/agent/tools/cloud_tools.py | head -150

Repository: Arvo-AI/aurora

Length of output: 2460


🏁 Script executed:

# Search for where start_time is set to confirm it's timezone-aware
rg -B 3 -A 3 "start_time.*=" server/chat/backend/agent/tools/cloud_tools.py | head -80

Repository: Arvo-AI/aurora

Length of output: 40


🏁 Script executed:

# Check if datetime.utcnow, timezone.utc, or other aware datetime creation is used
rg "(utcnow|timezone\.|aware|astimezone)" server/chat/backend/agent/tools/cloud_tools.py

Repository: Arvo-AI/aurora

Length of output: 40


🏁 Script executed:

# Search more broadly for start_time assignments in the entire agent tools directory
rg "start_time" server/chat/backend/agent/tools/ -B 2 -A 2

Repository: Arvo-AI/aurora

Length of output: 12748


🏁 Script executed:

# Check what datetime methods are used in the file
rg "datetime\." server/chat/backend/agent/tools/cloud_tools.py

Repository: Arvo-AI/aurora

Length of output: 507


🏁 Script executed:

# Look at the tool_capture module to understand the structure of current_tool_calls
fd -t f "tool_capture" server/chat/backend/agent/tools/ 2>/dev/null || fd -t f "tool_capture" server/ 2>/dev/null | head -5

Repository: Arvo-AI/aurora

Length of output: 40


🏁 Script executed:

# Find where current_tool_calls is defined and populated
rg "current_tool_calls" server/chat/backend/agent/tools/ -B 3 -A 3 | grep -E "(start_time|datetime)" | head -50

Repository: Arvo-AI/aurora

Length of output: 461


🏁 Script executed:

# Search for tool_capture module definition
find server -name "*tool_capture*" -o -name "*capture*" 2>/dev/null | head -10

Repository: Arvo-AI/aurora

Length of output: 114


🏁 Script executed:

# Check if there's a place where start_time is explicitly set
rg "\['start_time'\]|\"start_time\"" server/chat/backend/agent/tools/cloud_tools.py -B 2 -A 2

Repository: Arvo-AI/aurora

Length of output: 40


🏁 Script executed:

# Examine the tool_context_capture.py file
cat -n server/chat/backend/agent/utils/tool_context_capture.py | head -200

Repository: Arvo-AI/aurora

Length of output: 11166


🏁 Script executed:

# Search for start_time in tool_context_capture
rg "start_time" server/chat/backend/agent/utils/tool_context_capture.py -B 3 -A 3

Repository: Arvo-AI/aurora

Length of output: 1601


Replace naive datetime.min fallback with timezone-aware comparison.

When sorting candidate_ids by start_time, mixing timezone-aware datetimes with naive datetime.min raises TypeError: can't compare offset-naive and offset-aware datetimes. Since start_time is always set as datetime.now(timezone.utc) (timezone-aware), the fallback must also be timezone-aware.

Proposed fix
-                            candidate_ids.sort(key=lambda x: x[1] if x[1] else datetime.min)
+                            candidate_ids.sort(
+                                key=lambda x: x[1].timestamp() if x[1] else float("-inf")
+                            )
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 1168-1168: Use of datetime.datetime.min without timezone information

(DTZ901)

🤖 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 `@server/chat/backend/agent/tools/cloud_tools.py` around lines 1165 - 1170, The
sorting lambda in the candidate_ids.sort() call uses datetime.min as a fallback
when x[1] (start_time) is None, but this causes a TypeError because datetime.min
is timezone-naive while start_time is always timezone-aware (created with
datetime.now(timezone.utc)). Replace the datetime.min fallback with
datetime.min.replace(tzinfo=timezone.utc) to ensure both the fallback value and
the actual start_time values are timezone-aware and can be compared without
error.

Source: Linters/SAST tools

server/chat/backend/agent/workflow.py (2)

1061-1077: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Skip input rails for RCA scaffold messages.

is_scaffold is computed on Line 1063 but Line 1075 still schedules check_input(msg_text) for scaffold HumanMessages. These are system-authored background RCA prompts, so a rail block or fail-closed result can incorrectly abort an RCA run.

Proposed fix
-            if msg_text and not is_pr_review:
+            if msg_text and not is_pr_review and not is_scaffold:
                 _guardrail_task = asyncio.create_task(check_input(msg_text))
                 _guardrail_task_var.set(_guardrail_task)
🤖 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 `@server/chat/backend/agent/workflow.py` around lines 1061 - 1077, The
is_scaffold variable is computed to identify system-authored background RCA
messages but is not being used to prevent guardrail checks. Add is_scaffold to
the conditional guard on the line that schedules the check_input task, so the
condition reads if msg_text and not is_pr_review and not is_scaffold: to skip
input rails for scaffold messages just as PR review messages are skipped.

1075-1082: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Do not block the event loop immediately after scheduling the rail task.

asyncio.create_task() will not run check_input() while the coroutine stays in the synchronous handle_immediate_save() DB path. Move the save into a worker thread so guardrails can actually overlap with persistence.

Proposed fix
             if input_state.session_id and input_state.user_id and not is_scaffold:
                 from chat.backend.agent.utils.immediate_save_handler import handle_immediate_save
-                handle_immediate_save(input_state.session_id, input_state.user_id, msg_text or last_msg.content)
+                await asyncio.to_thread(
+                    handle_immediate_save,
+                    input_state.session_id,
+                    input_state.user_id,
+                    msg_text or last_msg.content,
+                )
🤖 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 `@server/chat/backend/agent/workflow.py` around lines 1075 - 1082, The
synchronous handle_immediate_save() function blocks the event loop immediately
after scheduling the check_input() guardrail task, preventing the guardrails
from running concurrently. Move the handle_immediate_save() call into a worker
thread using asyncio utilities (such as loop.run_in_executor()) so that the
event loop remains available to execute the guardrails task concurrently with
the database persistence operation.

@OlivierTrudeau
OlivierTrudeau force-pushed the feat/latency-instrumentation branch 3 times, most recently from cd9dad2 to 52998b5 Compare June 18, 2026 22:34
Keep only the cache key fix (remove id(tool_capture)) and the dynamic
resolution of tool_capture via get_tool_capture() at call time.
Remove TOOL CAPTURE debug logs. No cosmetic renames.

Co-authored-by: Cursor <cursoragent@cursor.com>
@OlivierTrudeau
OlivierTrudeau force-pushed the feat/latency-instrumentation branch from 52998b5 to 0ea1554 Compare June 18, 2026 22:35
…ntation

# Conflicts:
#	server/chat/background/task.py
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

Comment thread server/chat/backend/agent/workflow.py
Comment thread server/chat/backend/agent/agent.py Outdated
Comment thread server/celery_config.py Outdated
Comment thread server/chat/background/task.py
Comment thread server/guardrails/input_rail.py Fixed
Comment thread server/guardrails/input_rail.py Fixed

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
server/chat/backend/agent/tools/cloud_tools.py (1)

1027-1033: ⚠️ Potential issue | 🟠 Major

Cache key missing trigger_action_id – stale action IDs can execute.

The cache key (line 1033) omits trigger_action_id, but _action_id is extracted at line 1340 and pinned to tools via closure. When a different action later hits the cache with identical other parameters, the cached tool carries the original action ID, not the current one.

Include trigger_action_id in the cache key to differentiate cache entries by action:

Proposed fix
     is_pr_review = getattr(state_context, 'is_pr_review', False) if state_context else False
     is_rca_context = _is_background_rca(state_context, is_background)
+    trigger_action_id = getattr(state_context, 'trigger_action_id', None) if state_context else None
     capture_tag = "capture" if tool_capture else "nocapture"
-    cache_key = f"{user_id}:{capture_tag}:{mode_suffix}:background={is_background}:rca={rca_flag}:postmortem={is_postmortem_action}:is_rca_ctx={is_rca_context}:pr_review={is_pr_review}"
+    cache_key = (
+        f"{user_id}:{capture_tag}:{mode_suffix}:background={is_background}:"
+        f"rca={rca_flag}:postmortem={is_postmortem_action}:is_rca_ctx={is_rca_context}:"
+        f"pr_review={is_pr_review}:action_id={trigger_action_id or ''}"
+    )
🤖 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 `@server/chat/backend/agent/tools/cloud_tools.py` around lines 1027 - 1033, The
cache_key construction in the line containing the f-string assignment is missing
the trigger_action_id parameter, which causes different actions with identical
other parameters to share the same cached tool and reuse stale action IDs.
Extract trigger_action_id from state_context using getattr (similar to how
trigger_rca_requested, is_background, is_postmortem_action, and is_pr_review are
extracted) and append it to the cache_key f-string to ensure each action gets
its own distinct cache entry.
server/chat/background/task.py (1)

829-830: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Send the current in-memory assistant response to Slack, not the last persisted one.

The early Slack path calls _send_response_to_slack() and then suppresses the parent fallback when it returns True, but the helper queries chat_sessions.messages and picks the last persisted assistant message. If the current turn has not flushed yet—or a guardrail-blocked Slack turn produced no new assistant message—this can update Slack with a previous response and skip the real fallback.

Suggested direction
-                _slack_early_sent = _send_response_to_slack(user_id, session_id, trigger_metadata)
+                _slack_early_sent = _send_response_to_slack(
+                    user_id,
+                    session_id,
+                    trigger_metadata,
+                    response_text=_latest_assistant_text(state.messages),
+                )
@@
-def _send_response_to_slack(user_id: str, session_id: str, trigger_metadata: Dict[str, Any]) -> bool:
+def _send_response_to_slack(
+    user_id: str,
+    session_id: str,
+    trigger_metadata: Dict[str, Any],
+    response_text: Optional[str] = None,
+) -> bool:
@@
-        # Get the last assistant message from the chat session
-        with db_pool.get_admin_connection() as conn:
+        last_assistant_message = response_text
+        if not last_assistant_message:
+            # Keep the existing DB lookup as the parent-task fallback path.
+            with db_pool.get_admin_connection() as conn:

Add _latest_assistant_text(...) near the Slack helper and return False for early send when the current state has no assistant response.

Also applies to: 1461-1465, 1970-2016

🤖 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 `@server/chat/background/task.py` around lines 829 - 830, The issue is that the
early Slack send path in the code around line 829 calls
_send_response_to_slack() which retrieves the last persisted assistant message
from the database, but this may be stale if the current turn hasn't flushed yet
or was blocked by a guardrail. To fix this, create a new helper function
_latest_assistant_text() that returns the current in-memory assistant response
for the present turn rather than querying persisted messages, then use this
helper in the early send check to ensure Slack receives the actual current
response, and return False if no current assistant response exists to avoid
sending outdated messages.
🤖 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 `@server/chat/background/task.py`:
- Around line 692-698: The `notify_action_started` call and other dispatcher
notification calls (such as `notify_investigation_failed`,
`notify_action_completed`) are not gated by the `send_notifications` flag,
causing notifications to be sent even when callers opt out. Wrap the try-except
block containing the `notify_action_started` call with an `if
send_notifications:` guard at the beginning, and apply the same gating pattern
to all other `notify_*` calls throughout the file at the locations mentioned
(around lines 843-851, 872-888, 901-917, 1304, 1541-1567). Keep any database
status update operations outside these guards so they always execute regardless
of the notification flag.

In `@server/guardrails/input_rail.py`:
- Around line 177-189: The issue is that `_rails_thread_lock` is being acquired
on the event loop in the async code path (referenced in the range 207-214),
which blocks the workflow before the work can be moved to a worker thread,
preventing concurrent guardrail/prompt work during cold start. Extract the lock
acquisition and rails construction logic into a new synchronous helper function
(similar in structure to `prewarm_rails_sync()`), then call this helper via
`asyncio.to_thread()` from the async code paths instead of acquiring the lock
directly in the coroutine. This moves the lock contention off the event loop and
into the worker thread where it belongs.

In `@server/routes/slack/slack_events.py`:
- Line 363: In the logger.warning call, convert the f-string formatting to lazy
formatting using percent placeholders. Replace the f-string with a string
containing `%s` placeholders for clicker_user_id, clicker_org_id, and
incident_org_id, and pass the variables as a tuple argument to the
logger.warning method using the `%` operator. This ensures string formatting
only occurs when the log level is actually enabled, avoiding unnecessary
computation when warnings are disabled.

---

Outside diff comments:
In `@server/chat/backend/agent/tools/cloud_tools.py`:
- Around line 1027-1033: The cache_key construction in the line containing the
f-string assignment is missing the trigger_action_id parameter, which causes
different actions with identical other parameters to share the same cached tool
and reuse stale action IDs. Extract trigger_action_id from state_context using
getattr (similar to how trigger_rca_requested, is_background,
is_postmortem_action, and is_pr_review are extracted) and append it to the
cache_key f-string to ensure each action gets its own distinct cache entry.

In `@server/chat/background/task.py`:
- Around line 829-830: The issue is that the early Slack send path in the code
around line 829 calls _send_response_to_slack() which retrieves the last
persisted assistant message from the database, but this may be stale if the
current turn hasn't flushed yet or was blocked by a guardrail. To fix this,
create a new helper function _latest_assistant_text() that returns the current
in-memory assistant response for the present turn rather than querying persisted
messages, then use this helper in the early send check to ensure Slack receives
the actual current response, and return False if no current assistant response
exists to avoid sending outdated messages.
🪄 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

Run ID: 63c9984d-3e80-4cd4-b99d-15386ad83809

📥 Commits

Reviewing files that changed from the base of the PR and between 480b435 and 06de116.

📒 Files selected for processing (8)
  • server/celery_config.py
  • server/chat/backend/agent/agent.py
  • server/chat/backend/agent/tools/cloud_tools.py
  • server/chat/backend/agent/workflow.py
  • server/chat/background/task.py
  • server/guardrails/input_rail.py
  • server/routes/slack/slack_events.py
  • server/routes/slack/slack_events_helpers.py

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
server/chat/backend/agent/tools/cloud_tools.py (1)

1027-1033: ⚠️ Potential issue | 🟠 Major

Cache key missing trigger_action_id – stale action IDs can execute.

The cache key (line 1033) omits trigger_action_id, but _action_id is extracted at line 1340 and pinned to tools via closure. When a different action later hits the cache with identical other parameters, the cached tool carries the original action ID, not the current one.

Include trigger_action_id in the cache key to differentiate cache entries by action:

Proposed fix
     is_pr_review = getattr(state_context, 'is_pr_review', False) if state_context else False
     is_rca_context = _is_background_rca(state_context, is_background)
+    trigger_action_id = getattr(state_context, 'trigger_action_id', None) if state_context else None
     capture_tag = "capture" if tool_capture else "nocapture"
-    cache_key = f"{user_id}:{capture_tag}:{mode_suffix}:background={is_background}:rca={rca_flag}:postmortem={is_postmortem_action}:is_rca_ctx={is_rca_context}:pr_review={is_pr_review}"
+    cache_key = (
+        f"{user_id}:{capture_tag}:{mode_suffix}:background={is_background}:"
+        f"rca={rca_flag}:postmortem={is_postmortem_action}:is_rca_ctx={is_rca_context}:"
+        f"pr_review={is_pr_review}:action_id={trigger_action_id or ''}"
+    )
🤖 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 `@server/chat/backend/agent/tools/cloud_tools.py` around lines 1027 - 1033, The
cache_key construction in the line containing the f-string assignment is missing
the trigger_action_id parameter, which causes different actions with identical
other parameters to share the same cached tool and reuse stale action IDs.
Extract trigger_action_id from state_context using getattr (similar to how
trigger_rca_requested, is_background, is_postmortem_action, and is_pr_review are
extracted) and append it to the cache_key f-string to ensure each action gets
its own distinct cache entry.
server/chat/background/task.py (1)

829-830: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Send the current in-memory assistant response to Slack, not the last persisted one.

The early Slack path calls _send_response_to_slack() and then suppresses the parent fallback when it returns True, but the helper queries chat_sessions.messages and picks the last persisted assistant message. If the current turn has not flushed yet—or a guardrail-blocked Slack turn produced no new assistant message—this can update Slack with a previous response and skip the real fallback.

Suggested direction
-                _slack_early_sent = _send_response_to_slack(user_id, session_id, trigger_metadata)
+                _slack_early_sent = _send_response_to_slack(
+                    user_id,
+                    session_id,
+                    trigger_metadata,
+                    response_text=_latest_assistant_text(state.messages),
+                )
@@
-def _send_response_to_slack(user_id: str, session_id: str, trigger_metadata: Dict[str, Any]) -> bool:
+def _send_response_to_slack(
+    user_id: str,
+    session_id: str,
+    trigger_metadata: Dict[str, Any],
+    response_text: Optional[str] = None,
+) -> bool:
@@
-        # Get the last assistant message from the chat session
-        with db_pool.get_admin_connection() as conn:
+        last_assistant_message = response_text
+        if not last_assistant_message:
+            # Keep the existing DB lookup as the parent-task fallback path.
+            with db_pool.get_admin_connection() as conn:

Add _latest_assistant_text(...) near the Slack helper and return False for early send when the current state has no assistant response.

Also applies to: 1461-1465, 1970-2016

🤖 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 `@server/chat/background/task.py` around lines 829 - 830, The issue is that the
early Slack send path in the code around line 829 calls
_send_response_to_slack() which retrieves the last persisted assistant message
from the database, but this may be stale if the current turn hasn't flushed yet
or was blocked by a guardrail. To fix this, create a new helper function
_latest_assistant_text() that returns the current in-memory assistant response
for the present turn rather than querying persisted messages, then use this
helper in the early send check to ensure Slack receives the actual current
response, and return False if no current assistant response exists to avoid
sending outdated messages.
🤖 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 `@server/chat/background/task.py`:
- Around line 692-698: The `notify_action_started` call and other dispatcher
notification calls (such as `notify_investigation_failed`,
`notify_action_completed`) are not gated by the `send_notifications` flag,
causing notifications to be sent even when callers opt out. Wrap the try-except
block containing the `notify_action_started` call with an `if
send_notifications:` guard at the beginning, and apply the same gating pattern
to all other `notify_*` calls throughout the file at the locations mentioned
(around lines 843-851, 872-888, 901-917, 1304, 1541-1567). Keep any database
status update operations outside these guards so they always execute regardless
of the notification flag.

In `@server/guardrails/input_rail.py`:
- Around line 177-189: The issue is that `_rails_thread_lock` is being acquired
on the event loop in the async code path (referenced in the range 207-214),
which blocks the workflow before the work can be moved to a worker thread,
preventing concurrent guardrail/prompt work during cold start. Extract the lock
acquisition and rails construction logic into a new synchronous helper function
(similar in structure to `prewarm_rails_sync()`), then call this helper via
`asyncio.to_thread()` from the async code paths instead of acquiring the lock
directly in the coroutine. This moves the lock contention off the event loop and
into the worker thread where it belongs.

In `@server/routes/slack/slack_events.py`:
- Line 363: In the logger.warning call, convert the f-string formatting to lazy
formatting using percent placeholders. Replace the f-string with a string
containing `%s` placeholders for clicker_user_id, clicker_org_id, and
incident_org_id, and pass the variables as a tuple argument to the
logger.warning method using the `%` operator. This ensures string formatting
only occurs when the log level is actually enabled, avoiding unnecessary
computation when warnings are disabled.

---

Outside diff comments:
In `@server/chat/backend/agent/tools/cloud_tools.py`:
- Around line 1027-1033: The cache_key construction in the line containing the
f-string assignment is missing the trigger_action_id parameter, which causes
different actions with identical other parameters to share the same cached tool
and reuse stale action IDs. Extract trigger_action_id from state_context using
getattr (similar to how trigger_rca_requested, is_background,
is_postmortem_action, and is_pr_review are extracted) and append it to the
cache_key f-string to ensure each action gets its own distinct cache entry.

In `@server/chat/background/task.py`:
- Around line 829-830: The issue is that the early Slack send path in the code
around line 829 calls _send_response_to_slack() which retrieves the last
persisted assistant message from the database, but this may be stale if the
current turn hasn't flushed yet or was blocked by a guardrail. To fix this,
create a new helper function _latest_assistant_text() that returns the current
in-memory assistant response for the present turn rather than querying persisted
messages, then use this helper in the early send check to ensure Slack receives
the actual current response, and return False if no current assistant response
exists to avoid sending outdated messages.
🪄 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

Run ID: 63c9984d-3e80-4cd4-b99d-15386ad83809

📥 Commits

Reviewing files that changed from the base of the PR and between 480b435 and 06de116.

📒 Files selected for processing (8)
  • server/celery_config.py
  • server/chat/backend/agent/agent.py
  • server/chat/backend/agent/tools/cloud_tools.py
  • server/chat/backend/agent/workflow.py
  • server/chat/background/task.py
  • server/guardrails/input_rail.py
  • server/routes/slack/slack_events.py
  • server/routes/slack/slack_events_helpers.py
🛑 Comments failed to post (3)
server/chat/background/task.py (1)

692-698: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor send_notifications for all per-task dispatcher notifications.

send_notifications gates investigation-start notifications, but the new action-start/action-complete and failure dispatcher calls still run when a caller opts out; _execute_background_chat() also accepts the flag without using it. Gate only the notify_* calls on this flag and keep DB status updates unchanged.

Suggested gating pattern
-        if is_action_source:
+        if is_action_source and send_notifications:
             try:
                 from utils.notifications.dispatcher import notify_action_started
                 notify_action_started(user_id, trigger_metadata, session_id)
@@
-        if trigger_metadata and trigger_metadata.get('source') == 'action':
+        if send_notifications and trigger_metadata and trigger_metadata.get('source') == 'action':
             if not result.get("action_notification_sent"):
                 action_status = 'error' if result.get("guardrail_blocked") else 'success'
@@
-            if action_status:
+            if send_notifications and action_status:
                 try:
                     from utils.notifications.dispatcher import notify_action_completed
                     notify_action_completed(user_id, trigger_metadata, session_id, status=action_status, error_message=action_error_msg)

Apply the same if send_notifications: guard around the notify_investigation_failed(...) and timeout/error notify_action_completed(...) blocks.

Also applies to: 843-851, 872-888, 901-917, 1304-1304, 1541-1567

🧰 Tools
🪛 Ruff (0.15.17)

[warning] 697-697: Do not catch blind exception: Exception

(BLE001)


[warning] 698-698: Logging statement uses f-string

(G004)

🤖 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 `@server/chat/background/task.py` around lines 692 - 698, The
`notify_action_started` call and other dispatcher notification calls (such as
`notify_investigation_failed`, `notify_action_completed`) are not gated by the
`send_notifications` flag, causing notifications to be sent even when callers
opt out. Wrap the try-except block containing the `notify_action_started` call
with an `if send_notifications:` guard at the beginning, and apply the same
gating pattern to all other `notify_*` calls throughout the file at the
locations mentioned (around lines 843-851, 872-888, 901-917, 1304, 1541-1567).
Keep any database status update operations outside these guards so they always
execute regardless of the notification flag.

Source: Linters/SAST tools

server/guardrails/input_rail.py (1)

177-189: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Move the rails construction lock off the event loop.

_get_rails() can run while the prewarm thread owns _rails_thread_lock; acquiring that threading.Lock inside the coroutine blocks the workflow event loop before asyncio.to_thread() runs, defeating the concurrent guardrail/prompt work during cold start. Put the shared lock acquisition inside a sync helper that runs in the worker thread, and call that helper from both paths.

Suggested lock handoff
+def _ensure_rails_sync():
+    """Build or return the cached rails instance under the thread lock."""
+    global _rails_instance, _last_init_failure_ts
+    if _rails_instance is not None:
+        return _rails_instance
+    with _rails_thread_lock:
+        if _rails_instance is not None:
+            return _rails_instance
+        try:
+            _rails_instance = _build_rails_sync()
+        except Exception:
+            _last_init_failure_ts = time.monotonic()
+            raise
+        return _rails_instance
+
+
 def prewarm_rails_sync() -> None:
     """Sync init for Celery worker prewarm — thread-safe with async lazy init."""
-    global _rails_instance, _last_init_failure_ts
-    if _rails_instance is not None:
-        return
-    with _rails_thread_lock:
-        if _rails_instance is not None:
-            return
-        try:
-            _rails_instance = _build_rails_sync()
-        except Exception:
-            _last_init_failure_ts = time.monotonic()
-            raise
+    _ensure_rails_sync()
@@
     async with _get_lock():
-        with _rails_thread_lock:
-            if _rails_instance is not None:
-                return _rails_instance
-            try:
-                _rails_instance = await asyncio.to_thread(_build_rails_sync)
-            except Exception:
-                _last_init_failure_ts = time.monotonic()
-                raise
-    return _rails_instance
+        if _rails_instance is not None:
+            return _rails_instance
+        return await asyncio.to_thread(_ensure_rails_sync)

Also applies to: 207-214

🧰 Tools
🪛 Ruff (0.15.17)

[warning] 179-179: Using the global statement to update _rails_instance is discouraged

(PLW0603)


[warning] 179-179: Using the global statement to update _last_init_failure_ts is discouraged

(PLW0603)

🤖 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 `@server/guardrails/input_rail.py` around lines 177 - 189, The issue is that
`_rails_thread_lock` is being acquired on the event loop in the async code path
(referenced in the range 207-214), which blocks the workflow before the work can
be moved to a worker thread, preventing concurrent guardrail/prompt work during
cold start. Extract the lock acquisition and rails construction logic into a new
synchronous helper function (similar in structure to `prewarm_rails_sync()`),
then call this helper via `asyncio.to_thread()` from the async code paths
instead of acquiring the lock directly in the coroutine. This moves the lock
contention off the event loop and into the worker thread where it belongs.
server/routes/slack/slack_events.py (1)

363-363: ⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify remaining logger f-strings in this file
rg -nP 'logger\.(debug|info|warning|error|exception|critical)\(\s*f"' server/routes/slack/slack_events.py

Repository: Arvo-AI/aurora

Length of output: 2599


Use lazy logger formatting at Line 363.

The logger call uses an f-string, which triggers Ruff G004 and eagerly formats even when the log level is disabled. Convert to lazy formatting using % placeholders:

Proposed fix
-            logger.warning(f"User {clicker_user_id} (org {clicker_org_id}) tried to run suggestion for incident in org {incident_org_id}")
+            logger.warning(
+                "User %s (org %s) tried to run suggestion for incident in org %s",
+                clicker_user_id,
+                clicker_org_id,
+                incident_org_id,
+            )
📝 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.

            logger.warning(
                "User %s (org %s) tried to run suggestion for incident in org %s",
                clicker_user_id,
                clicker_org_id,
                incident_org_id,
            )
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 363-363: Logging statement uses f-string

(G004)

🤖 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 `@server/routes/slack/slack_events.py` at line 363, In the logger.warning call,
convert the f-string formatting to lazy formatting using percent placeholders.
Replace the f-string with a string containing `%s` placeholders for
clicker_user_id, clicker_org_id, and incident_org_id, and pass the variables as
a tuple argument to the logger.warning method using the `%` operator. This
ensures string formatting only occurs when the log level is actually enabled,
avoiding unnecessary computation when warnings are disabled.

Source: Linters/SAST tools

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

Actionable comments posted: 1

🤖 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 `@server/guardrails/input_rail.py`:
- Around line 183-191: In the prewarm_rails_sync function, the direct
acquisition of _rails_thread_lock blocks the event loop when the Celery prewarm
daemon thread holds it during _build_rails_sync execution. Move the critical
section (lock acquisition, backoff validation, and _build_rails_sync call) into
a worker thread using asyncio.to_thread() so the event loop remains unblocked
for concurrent requests. Additionally, re-check the backoff condition inside the
critical section after acquiring _rails_thread_lock to prevent the race
condition where a caller passes the backoff check before the lock but gets
delayed waiting for it, then bypasses the backoff window when finally acquiring
the lock.
🪄 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

Run ID: c20d39fa-ba8f-447e-b3cd-dc0ec0a7aa48

📥 Commits

Reviewing files that changed from the base of the PR and between 06de116 and 87488f9.

📒 Files selected for processing (1)
  • server/guardrails/input_rail.py

Comment thread server/guardrails/input_rail.py Outdated

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

Actionable comments posted: 1

🤖 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 `@server/guardrails/input_rail.py`:
- Around line 183-202: The error message "input rail init recently failed;
backing off" is duplicated three times in the module. Extract this literal
string to a module-level constant near the top of the file with other constants,
naming it something like _INIT_BACKOFF_ERROR. Then replace all three occurrences
of the literal string (one in the _ensure_rails_in_thread function at line 196
and two other locations in the file) with references to this new constant to
improve maintainability.
🪄 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

Run ID: 57c33ee1-c41f-45f6-86f5-65279525deb2

📥 Commits

Reviewing files that changed from the base of the PR and between 87488f9 and fa09c93.

📒 Files selected for processing (2)
  • server/celery_config.py
  • server/guardrails/input_rail.py

Comment thread server/guardrails/input_rail.py
@beng360
beng360 merged commit faebae4 into main Jun 22, 2026
15 checks passed
@beng360
beng360 deleted the feat/latency-instrumentation branch June 22, 2026 17:00
@sonarqubecloud

Copy link
Copy Markdown

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.

3 participants