Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,7 @@
## 2025-07-15 - Fast Bounding Box Pre-filter
**Learning:** Calculating great circle distance (Haversine) for every issue against a target location is computationally expensive (O(N) with heavy math ops like sin, cos, atan2). In high-traffic aggregations, this can become a bottleneck.
**Action:** Use a fast bounding box pre-filter (`get_bounding_box` with a 5% epsilon) to quickly discard issues that are definitely outside the search radius before running the expensive exact haversine distance calculation.

## 2026-07-20 - Unbounded In-Memory Caches & Process Safety
**Learning:** Class-level in-memory dictionaries used to achieve O(1) blockchain lookups can leak memory if they grow unboundedly in high-traffic applications. Additionally, single-column select queries in SQLAlchemy provide a highly performant and process-safe database-level alternative when multiple worker processes are involved.
**Action:** Always bound in-memory cache growth (e.g., limit to 1000 items with eviction) and combine with optimized database column-selection queries to guarantee process-safety and prevent memory leaks.
54 changes: 52 additions & 2 deletions backend/escalation_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"""

import datetime
import hashlib
import threading
from typing import List, Dict, Any, Optional
from sqlalchemy.orm import Session
from sqlalchemy import and_, or_
Expand All @@ -17,6 +19,11 @@ class EscalationEngine:
Engine for handling grievance escalations based on SLA breaches and severity changes.
"""

# Cache for O(1) blockchain integrity hash lookups
# Stores {grievance_id: last_integrity_hash}
_audit_last_hash_cache = {}
_cache_lock = threading.Lock()

def __init__(self, routing_service: RoutingService, sla_service: SLAConfigService,
rules_config: Dict[str, Any]):
"""
Expand Down Expand Up @@ -248,25 +255,68 @@ def _escalate_grievance(self, grievance: Grievance, reason: EscalationReason,
# Recalculate SLA
self._recalculate_sla(grievance, db)

# Create audit log
# Retrieve previous hash (O(1) from cache or O(log N) from indexed DB)
prev_hash = self._get_last_audit_hash(grievance.id, db)

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.

P1: Concurrent escalations can fork the chain because both transactions may calculate from the same cached predecessor before either commits. Serialize append operations per grievance with a database transaction/row or advisory lock, and read the latest persisted hash under that lock.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/escalation_engine.py, line 259:

<comment>Concurrent escalations can fork the chain because both transactions may calculate from the same cached predecessor before either commits. Serialize append operations per grievance with a database transaction/row or advisory lock, and read the latest persisted hash under that lock.</comment>

<file context>
@@ -248,25 +255,68 @@ def _escalate_grievance(self, grievance: Grievance, reason: EscalationReason,
 
-            # Create audit log
+            # Retrieve previous hash (O(1) from cache or O(log N) from indexed DB)
+            prev_hash = self._get_last_audit_hash(grievance.id, db)
+
+            # Calculate new integrity hash: SHA256(grievance_id | reason.value | prev_hash)
</file context>


# Calculate new integrity hash: SHA256(grievance_id | reason.value | prev_hash)
reason_val = reason.value if hasattr(reason, "value") else str(reason)
hash_input = f"{grievance.id}|{reason_val}|{prev_hash or 'GENESIS'}"

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.

P1: Changes to authorities, notes, or timestamp remain reported as "Integrity verified" because none are committed by this digest. Hash a canonical representation of every protected audit field and use the same representation during verification.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/escalation_engine.py, line 263:

<comment>Changes to authorities, notes, or timestamp remain reported as "Integrity verified" because none are committed by this digest. Hash a canonical representation of every protected audit field and use the same representation during verification.</comment>

<file context>
@@ -248,25 +255,68 @@ def _escalate_grievance(self, grievance: Grievance, reason: EscalationReason,
+
+            # Calculate new integrity hash: SHA256(grievance_id | reason.value | prev_hash)
+            reason_val = reason.value if hasattr(reason, "value") else str(reason)
+            hash_input = f"{grievance.id}|{reason_val}|{prev_hash or 'GENESIS'}"
+            new_hash = hashlib.sha256(hash_input.encode()).hexdigest()
+
</file context>

new_hash = hashlib.sha256(hash_input.encode()).hexdigest()

# Create audit log with blockchain integration
audit_log = EscalationAudit(
grievance_id=grievance.id,
previous_authority=previous_authority,
new_authority=grievance.assigned_authority,
reason=reason,
notes=notes
notes=notes,
integrity_hash=new_hash,
previous_integrity_hash=prev_hash

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.

P1: Removing an audit record is not detected: verification never resolves previous_integrity_hash to an actual predecessor. Traverse and validate the predecessor chain (including genesis and ordering) when reporting audit integrity.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/escalation_engine.py, line 274:

<comment>Removing an audit record is not detected: verification never resolves `previous_integrity_hash` to an actual predecessor. Traverse and validate the predecessor chain (including genesis and ordering) when reporting audit integrity.</comment>

<file context>
@@ -248,25 +255,68 @@ def _escalate_grievance(self, grievance: Grievance, reason: EscalationReason,
-                notes=notes
+                notes=notes,
+                integrity_hash=new_hash,
+                previous_integrity_hash=prev_hash
             )
 
</file context>

)

db.add(audit_log)
db.commit()

# Update O(1) cache for next escalation audit (with max size limit to prevent memory leak)
with self._cache_lock:

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.

P1: The lock is released after the cache-miss check but re-acquired later for the cache-update, leaving an unlocked window where a concurrent escalation for the same grievance can create a new audit record and update the cache. The first thread then writes stale data into the cache, which the caller unconditionally overwrites with its own new hash — potentially overwriting a newer hash written by the concurrent thread. This can permanently break the hash chain, because the final cache entry after both threads finish may point to an audit that is not the most recent one. A subsequent escalation will then link against the wrong previous hash, producing a chain link that does not match what the DB actually contains. Fix: hold the lock across the full lookup-and-store, or re-check the cache under lock after the DB query before writing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/escalation_engine.py, line 281:

<comment>The lock is released after the cache-miss check but re-acquired later for the cache-update, leaving an unlocked window where a concurrent escalation for the same grievance can create a new audit record and update the cache. The first thread then writes stale data into the cache, which the caller unconditionally overwrites with its own new hash — potentially overwriting a newer hash written by the concurrent thread. This can permanently break the hash chain, because the final cache entry after both threads finish may point to an audit that is not the most recent one. A subsequent escalation will then link against the wrong previous hash, producing a chain link that does not match what the DB actually contains. Fix: hold the lock across the full lookup-and-store, or re-check the cache under lock after the DB query before writing.</comment>

<file context>
@@ -248,25 +255,68 @@ def _escalate_grievance(self, grievance: Grievance, reason: EscalationReason,
             db.commit()
 
+            # Update O(1) cache for next escalation audit (with max size limit to prevent memory leak)
+            with self._cache_lock:
+                if len(self._audit_last_hash_cache) >= 1000:
+                    self._audit_last_hash_cache.clear()
</file context>

if len(self._audit_last_hash_cache) >= 1000:
self._audit_last_hash_cache.clear()

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.

P2: Evicting the entire dictionary with dict.clear() when the cache hits 1000 items causes a severe performance burst: every single grievance that had a cached hash now misses, triggering up to 1000 DB queries in quick succession. This directly contradicts the "blazing-fast O(1)" promise — the escalations immediately after a clear pay O(log N) DB cost. Consider an LRU eviction (pop the oldest 200 entries, or use cachetools.LRUCache/OrderedDict.popitem(last=False)) to maintain steady cache-hit ratios.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/escalation_engine.py, line 283:

<comment>Evicting the entire dictionary with `dict.clear()` when the cache hits 1000 items causes a severe performance burst: every single grievance that had a cached hash now misses, triggering up to 1000 DB queries in quick succession. This directly contradicts the "blazing-fast O(1)" promise — the escalations immediately after a clear pay O(log N) DB cost. Consider an LRU eviction (pop the oldest 200 entries, or use `cachetools.LRUCache`/`OrderedDict.popitem(last=False)`) to maintain steady cache-hit ratios.</comment>

<file context>
@@ -248,25 +255,68 @@ def _escalate_grievance(self, grievance: Grievance, reason: EscalationReason,
+            # Update O(1) cache for next escalation audit (with max size limit to prevent memory leak)
+            with self._cache_lock:
+                if len(self._audit_last_hash_cache) >= 1000:
+                    self._audit_last_hash_cache.clear()
+                self._audit_last_hash_cache[grievance.id] = new_hash
+
</file context>

self._audit_last_hash_cache[grievance.id] = new_hash

return True

except Exception as e:
db.rollback()
print(f"Error during escalation: {e}")
return False

Comment on lines +258 to 292

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Hash chain can fork under concurrent escalations of the same grievance.

prev_hash is read (cache or DB) and the new EscalationAudit row is committed with no exclusivity spanning that read-then-write. Two concurrent calls (e.g., two simultaneous manual_escalate/escalate_grievance_severity requests) for the same grievance_id can both observe the same prev_hash and each commit a distinct audit row referencing it as previous_integrity_hash. Since verify_audit_integrity (backend/grievance_service.py) validates each record independently against its own previous_integrity_hash, both forked rows would pass verification individually — defeating the tamper-evidence guarantee this feature is meant to provide.

Consider serializing escalation per grievance (e.g., SELECT ... FOR UPDATE on the grievance row, or a DB-level unique constraint on (grievance_id, previous_integrity_hash) with retry-on-conflict) to make the chain write atomic with the read of the previous hash.

🧰 Tools
🪛 Ruff (0.15.21)

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

(BLE001)

🤖 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 `@backend/escalation_engine.py` around lines 258 - 292, Serialize audit
creation per grievance in the escalation method containing
`_get_last_audit_hash`, locking the grievance row with a database `FOR UPDATE`
transaction before reading `prev_hash` and committing `EscalationAudit`. Ensure
concurrent escalations for the same `grievance_id` cannot share the same
previous hash, while preserving the existing rollback and cache-update behavior.

def _get_last_audit_hash(self, grievance_id: int, db: Session) -> Optional[str]:
"""
Retrieves the last integrity hash for an escalation audit of a grievance.
Bolt Optimization: Uses thread-safe memory cache for O(1) lookup.
"""
with self._cache_lock:
if grievance_id in self._audit_last_hash_cache:
return self._audit_last_hash_cache[grievance_id]

# Cache miss: Fallback to indexed DB query using optimized single-column selection
from sqlalchemy import desc
last_audit_hash = db.query(EscalationAudit.integrity_hash)\
.filter(EscalationAudit.grievance_id == grievance_id)\

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.

P2: Cache misses will degrade as audit history grows because this lookup has no index for its filter/order pattern. Add and migrate a composite (grievance_id, id) index for the fallback query.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/escalation_engine.py, line 305:

<comment>Cache misses will degrade as audit history grows because this lookup has no index for its filter/order pattern. Add and migrate a composite `(grievance_id, id)` index for the fallback query.</comment>

<file context>
@@ -248,25 +255,68 @@ def _escalate_grievance(self, grievance: Grievance, reason: EscalationReason,
+        # Cache miss: Fallback to indexed DB query using optimized single-column selection
+        from sqlalchemy import desc
+        last_audit_hash = db.query(EscalationAudit.integrity_hash)\
+            .filter(EscalationAudit.grievance_id == grievance_id)\
+            .order_by(desc(EscalationAudit.id))\
+            .first()
</file context>

.order_by(desc(EscalationAudit.id))\
.first()

last_hash = last_audit_hash[0] if last_audit_hash else None

# Update cache for next time (with max size limit to prevent memory leak)
if last_hash:
with self._cache_lock:
if len(self._audit_last_hash_cache) >= 1000:
self._audit_last_hash_cache.clear()
self._audit_last_hash_cache[grievance_id] = last_hash

return last_hash

def _recalculate_sla(self, grievance: Grievance, db: Session) -> None:
"""
Recalculate SLA deadline for a grievance.
Expand Down
58 changes: 48 additions & 10 deletions backend/grievance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sqlalchemy import and_, desc
from datetime import datetime, timezone, timedelta

from backend.models import Grievance, Jurisdiction, GrievanceStatus, SeverityLevel, GrievanceFollower
from backend.models import Grievance, Jurisdiction, GrievanceStatus, SeverityLevel, GrievanceFollower, EscalationAudit
from backend.database import SessionLocal
from backend.routing_service import RoutingService
from backend.sla_config_service import SLAConfigService
Expand Down Expand Up @@ -169,8 +169,10 @@ def follow_grievance(self, grievance_id: int, user_email: str, db: Session = Non
db.commit()
db.refresh(follower)

# Update O(1) cache for next follower
# Update O(1) cache for next follower (with max size limit to prevent memory leak)
with self._cache_lock:
if len(self._follower_last_hash_cache) >= 1000:
self._follower_last_hash_cache.clear()
self._follower_last_hash_cache[grievance_id] = new_hash

return follower
Expand All @@ -192,17 +194,19 @@ def _get_last_integrity_hash(self, grievance_id: int, db: Session) -> Optional[s
if grievance_id in self._follower_last_hash_cache:
return self._follower_last_hash_cache[grievance_id]

# Cache miss: Fallback to indexed DB query
last_follower = db.query(GrievanceFollower)\
# Cache miss: Fallback to indexed DB query using optimized single-column selection
last_follower_hash = db.query(GrievanceFollower.integrity_hash)\
.filter(GrievanceFollower.grievance_id == grievance_id)\
.order_by(desc(GrievanceFollower.id))\
.first()

last_hash = last_follower.integrity_hash if last_follower else None
last_hash = last_follower_hash[0] if last_follower_hash else None

# Update cache for next time
# Update cache for next time (with max size limit to prevent memory leak)
if last_hash:
with self._cache_lock:
if len(self._follower_last_hash_cache) >= 1000:
self._follower_last_hash_cache.clear()
self._follower_last_hash_cache[grievance_id] = last_hash

return last_hash
Expand Down Expand Up @@ -238,6 +242,38 @@ def verify_follower_integrity(self, follower_id: int, db: Session = None) -> Dic
if is_local_session:
db.close()

def verify_audit_integrity(self, audit_id: int, db: Session = None) -> Dict[str, Any]:
"""
Verify the blockchain-style integrity of an escalation audit record.
"""
is_local_session = False
if db is None:
db = SessionLocal()
is_local_session = True

try:
audit = db.query(EscalationAudit).filter(EscalationAudit.id == audit_id).first()
if not audit:
return {"is_valid": False, "message": "Audit record not found"}

# Re-calculate hash: SHA256(grievance_id | reason.value | prev_hash)
reason_val = audit.reason.value if hasattr(audit.reason, "value") else str(audit.reason)
hash_input = f"{audit.grievance_id}|{reason_val}|{audit.previous_integrity_hash or 'GENESIS'}"
calculated_hash = hashlib.sha256(hash_input.encode()).hexdigest()

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.

P1: A modified audit trail can be forged by recomputing this unkeyed SHA-256 hash; verify_audit_integrity has no secret or immutable external anchor to distinguish forged hashes. Authenticate entries with a protected HMAC/signature and retain its key outside the audit database (or anchor chain heads externally).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/grievance_service.py, line 262:

<comment>A modified audit trail can be forged by recomputing this unkeyed SHA-256 hash; `verify_audit_integrity` has no secret or immutable external anchor to distinguish forged hashes. Authenticate entries with a protected HMAC/signature and retain its key outside the audit database (or anchor chain heads externally).</comment>

<file context>
@@ -238,6 +242,38 @@ def verify_follower_integrity(self, follower_id: int, db: Session = None) -> Dic
+            # Re-calculate hash: SHA256(grievance_id | reason.value | prev_hash)
+            reason_val = audit.reason.value if hasattr(audit.reason, "value") else str(audit.reason)
+            hash_input = f"{audit.grievance_id}|{reason_val}|{audit.previous_integrity_hash or 'GENESIS'}"
+            calculated_hash = hashlib.sha256(hash_input.encode()).hexdigest()
+
+            is_valid = (calculated_hash == audit.integrity_hash)
</file context>


is_valid = (calculated_hash == audit.integrity_hash)

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.

P1: When audit.integrity_hash is None (e.g., for audit rows created before this feature was deployed, or outside _escalate_grievance), the comparison calculated_hash == audit.integrity_hash will always be False since calculated_hash is a real SHA-256 digest. This causes the function to return "INTEGRITY BREACH DETECTED", misleadingly implying tampering rather than simply indicating no integrity data is available.

Consider adding an early return when audit.integrity_hash is None with a distinct message like "No integrity data available for this record".

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/grievance_service.py, line 264:

<comment>When `audit.integrity_hash` is `None` (e.g., for audit rows created before this feature was deployed, or outside `_escalate_grievance`), the comparison `calculated_hash == audit.integrity_hash` will always be `False` since `calculated_hash` is a real SHA-256 digest. This causes the function to return `"INTEGRITY BREACH DETECTED"`, misleadingly implying tampering rather than simply indicating no integrity data is available.

Consider adding an early return when `audit.integrity_hash is None` with a distinct message like `"No integrity data available for this record"`.</comment>

<file context>
@@ -238,6 +242,38 @@ def verify_follower_integrity(self, follower_id: int, db: Session = None) -> Dic
+            hash_input = f"{audit.grievance_id}|{reason_val}|{audit.previous_integrity_hash or 'GENESIS'}"
+            calculated_hash = hashlib.sha256(hash_input.encode()).hexdigest()
+
+            is_valid = (calculated_hash == audit.integrity_hash)
+
+            return {
</file context>
Suggested change
is_valid = (calculated_hash == audit.integrity_hash)
if audit.integrity_hash is None:
return {"is_valid": False, "message": "No integrity data available for this record"}
is_valid = (calculated_hash == audit.integrity_hash)


return {
"is_valid": is_valid,
"current_hash": audit.integrity_hash,
"calculated_hash": calculated_hash,
"previous_hash": audit.previous_integrity_hash,
"message": "Integrity verified" if is_valid else "INTEGRITY BREACH DETECTED"
}
finally:
if is_local_session:
db.close()

Comment on lines +245 to +276

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Legacy audit records with no stored hash are reported as "INTEGRITY BREACH DETECTED."

EscalationAudit.integrity_hash is nullable (backend/models.py), so any audit row predating this feature (or created outside _escalate_grievance) has integrity_hash = None. Here, calculated_hash (always a real SHA-256 digest) can never equal None, so is_valid becomes False and the response claims a breach — misleadingly implying tampering rather than "no integrity data available."

🛡️ Proposed fix
             audit = db.query(EscalationAudit).filter(EscalationAudit.id == audit_id).first()
             if not audit:
                 return {"is_valid": False, "message": "Audit record not found"}
 
+            if audit.integrity_hash is None:
+                return {"is_valid": False, "message": "No integrity data available for this record"}
+
             # Re-calculate hash: SHA256(grievance_id | reason.value | prev_hash)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def verify_audit_integrity(self, audit_id: int, db: Session = None) -> Dict[str, Any]:
"""
Verify the blockchain-style integrity of an escalation audit record.
"""
is_local_session = False
if db is None:
db = SessionLocal()
is_local_session = True
try:
audit = db.query(EscalationAudit).filter(EscalationAudit.id == audit_id).first()
if not audit:
return {"is_valid": False, "message": "Audit record not found"}
# Re-calculate hash: SHA256(grievance_id | reason.value | prev_hash)
reason_val = audit.reason.value if hasattr(audit.reason, "value") else str(audit.reason)
hash_input = f"{audit.grievance_id}|{reason_val}|{audit.previous_integrity_hash or 'GENESIS'}"
calculated_hash = hashlib.sha256(hash_input.encode()).hexdigest()
is_valid = (calculated_hash == audit.integrity_hash)
return {
"is_valid": is_valid,
"current_hash": audit.integrity_hash,
"calculated_hash": calculated_hash,
"previous_hash": audit.previous_integrity_hash,
"message": "Integrity verified" if is_valid else "INTEGRITY BREACH DETECTED"
}
finally:
if is_local_session:
db.close()
def verify_audit_integrity(self, audit_id: int, db: Session = None) -> Dict[str, Any]:
"""
Verify the blockchain-style integrity of an escalation audit record.
"""
is_local_session = False
if db is None:
db = SessionLocal()
is_local_session = True
try:
audit = db.query(EscalationAudit).filter(EscalationAudit.id == audit_id).first()
if not audit:
return {"is_valid": False, "message": "Audit record not found"}
if audit.integrity_hash is None:
return {"is_valid": False, "message": "No integrity data available for this record"}
# Re-calculate hash: SHA256(grievance_id | reason.value | prev_hash)
reason_val = audit.reason.value if hasattr(audit.reason, "value") else str(audit.reason)
hash_input = f"{audit.grievance_id}|{reason_val}|{audit.previous_integrity_hash or 'GENESIS'}"
calculated_hash = hashlib.sha256(hash_input.encode()).hexdigest()
is_valid = (calculated_hash == audit.integrity_hash)
return {
"is_valid": is_valid,
"current_hash": audit.integrity_hash,
"calculated_hash": calculated_hash,
"previous_hash": audit.previous_integrity_hash,
"message": "Integrity verified" if is_valid else "INTEGRITY BREACH DETECTED"
}
finally:
if is_local_session:
db.close()
🤖 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 `@backend/grievance_service.py` around lines 245 - 276, Update
verify_audit_integrity to handle EscalationAudit records with a missing
integrity_hash before comparing hashes. Return a non-breach response indicating
integrity data is unavailable, while preserving the existing hash calculation
and verification behavior for records with a stored hash.

def get_grievance(self, grievance_id: int, db: Session = None) -> Optional[Grievance]:
"""
Get a grievance by ID.
Expand Down Expand Up @@ -305,32 +341,34 @@ def update_grievance_status(self, grievance_id: int, status: GrievanceStatus,
db.close()

def escalate_grievance_severity(self, grievance_id: int, new_severity: SeverityLevel,
reason: str = "") -> bool:
reason: str = "", db: Session = None) -> bool:
"""
Escalate grievance severity.

Args:
grievance_id: Grievance ID
new_severity: New severity level
reason: Reason for escalation
db: Database session

Returns:
True if escalation successful
"""
return self.escalation_engine.escalate_grievance_severity(grievance_id, new_severity, reason)
return self.escalation_engine.escalate_grievance_severity(grievance_id, new_severity, reason, db)

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.

P2: Passing a caller-owned session here causes the engine to close it before this method returns, breaking the service’s established db ownership contract and any surrounding unit-of-work. Track whether the engine created the session, then close only locally created sessions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At backend/grievance_service.py, line 357:

<comment>Passing a caller-owned session here causes the engine to close it before this method returns, breaking the service’s established `db` ownership contract and any surrounding unit-of-work. Track whether the engine created the session, then close only locally created sessions.</comment>

<file context>
@@ -305,32 +341,34 @@ def update_grievance_status(self, grievance_id: int, status: GrievanceStatus,
             True if escalation successful
         """
-        return self.escalation_engine.escalate_grievance_severity(grievance_id, new_severity, reason)
+        return self.escalation_engine.escalate_grievance_severity(grievance_id, new_severity, reason, db)
 
-    def manual_escalate(self, grievance_id: int, reason: str = "") -> bool:
</file context>


def manual_escalate(self, grievance_id: int, reason: str = "") -> bool:
def manual_escalate(self, grievance_id: int, reason: str = "", db: Session = None) -> bool:
"""
Manually escalate a grievance.

Args:
grievance_id: Grievance ID
reason: Reason for escalation
db: Database session

Returns:
True if escalation successful
"""
return self.escalation_engine.manual_escalate(grievance_id, reason)
return self.escalation_engine.manual_escalate(grievance_id, reason, db)

def run_escalation_check(self) -> Dict[str, int]:
"""
Expand Down
Loading
Loading