ZhuLinsen/daily_stock_analysis · error · ValueError
terminal decision signal cannot be reactivated through statu
Error message
terminal decision signal cannot be reactivated through status update
What it means
Plain ValueError raised by DecisionSignalService.update_status (src/services/decision_signal_service.py:372) when the caller requests status='active' for a signal that is already in a TERMINAL_STATUSES status (e.g. closed/expired/cancelled) or whose expires_at is already in the past. Terminal signals are immutable by design: reactivation must go through creating a new signal, not a status flip.
Source
Thrown at src/services/decision_signal_service.py:372
"page_size": max(1, min(int(limit), 100)),
}
def update_status(
self,
signal_id: int,
*,
status: str,
metadata: Optional[Any] = None,
replace_metadata: bool = False,
) -> Dict[str, Any]:
status_norm = self._normalize_enum(status, SIGNAL_STATUSES, "status")
existing = self.repo.get(signal_id)
if existing is None:
raise DecisionSignalNotFoundError(f"Decision signal not found: {signal_id}")
if status_norm == "active" and (
existing.status in TERMINAL_STATUSES or self._is_expired(existing.expires_at)
):
raise ValueError("terminal decision signal cannot be reactivated through status update")
metadata_json = None
if replace_metadata:
if isinstance(metadata, dict):
normalized_metadata = dict(metadata)
if existing.decision_profile is None:
normalized_metadata.pop("decision_profile", None)
else:
normalized_metadata = self._synchronize_metadata_decision_profile(
normalized_metadata,
existing.decision_profile,
)
metadata_json = self._json_dumps(normalized_metadata)
else:
metadata_json = self._json_dumps(metadata)
row = self.repo.update_status(
signal_id,
status=status_norm,
metadata_json=metadata_json,View on GitHub (pinned to 5159bd72e8)
Solutions
- Do not reactivate: create a fresh signal (POST /decision-signals) capturing the new decision instead of flipping the terminal one back to active.
- If the signal should never have been terminal, investigate why it was closed/expired and fix the writer, then still create a new signal rather than mutating history.
- If expiry-driven, review expires_at defaults (_default_expires_at) so live signals don't silently lapse.
- For idempotent retry logic, treat this ValueError as a no-op signal that the request was already finalized.
Example fix
# before
service.update_signal_status(signal_id, status="active") # ValueError: terminal decision signal cannot be reactivated
# after
new_signal = service.create_signal({**payload, "stock_code": old["stock_code"], "market": old["market"], "action": old["action"]})
# keep audit trail: the terminal row stays terminal, the new row carries the reactivated decision Defensive patterns
Strategy: validation
Validate before calling
existing = service.get_signal(signal_id)
from src.services.decision_signal_service import TERMINAL_STATUSES
if status == 'active' and (existing['status'] in TERMINAL_STATUSES or is_expired(existing.get('expires_at'))):
raise RuntimeError('cannot reactivate; create a new signal instead') Type guard
def can_transition_to(current_status: str, expires_at, target: str) -> bool:
if target != 'active':
return True
return current_status not in TERMINAL_STATUSES and not expired(expires_at) Try / catch
try:
service.update_signal_status(sid, status='active')
except ValueError as exc:
if 'cannot be reactivated' in str(exc):
new = service.create_signal(rebuild_payload_from(old_signal)) # new row instead
else:
raise Prevention
- Model reactivation as create-new-signal, never as status flip.
- Make retry pipelines idempotent: a 'reactivate' request on a terminal row should create or no-op, not error.
- Watch expires_at defaults so active signals don't silently lapse into terminal-expired state.
When it happens
Trigger: PATCH /decision-signals/{id}/status with {"status": "active"} where the row's current status is terminal, or where _is_expired(existing.expires_at) is true (lazy expiry already lapsed). Note the check runs before repo.update_status, so even racing writers hit it on the freshly-read row.
Common situations: Automation that 'reopens' old signals by resetting status; clock skew or a long-paused scheduler causing expires_at to pass while the signal was still displayed as active in a cached UI; a retry pipeline blindly re-sending the previous 'active' request after the signal was closed by another actor.
Related errors
- {field_name} must be an integer
- {field_name} must be positive
- unsupported_report_type
- score must be between 0 and 100
- stock_code is required
AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15).
Data as JSON: /api/errors/84ce83e7724103a7.
Report an issue: GitHub.