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

  1. Do not reactivate: create a fresh signal (POST /decision-signals) capturing the new decision instead of flipping the terminal one back to active.
  2. 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.
  3. If expiry-driven, review expires_at defaults (_default_expires_at) so live signals don't silently lapse.
  4. 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

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


AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15). Data as JSON: /api/errors/84ce83e7724103a7. Report an issue: GitHub.