Ctrl K
ComunidadGuías…

claude_history_provenance.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Pure, bounded Claude source-lineage classification. Never edits transcripts.

The marker's wording is not evidence of a human Stop action. A synthetic user
record must belong to an already established prompt, not begin a new prompt.
Only source identifiers are carried between byte-cursor reads; never chat text.
"""
from __future__ import annotations

from datetime import datetime
import re


INTERRUPTION_MARKERS = frozenset({
    "[Request interrupted by user]",
    "[Request interrupted by user for tool use]",
})
_UUID = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}\Z")
_TIME = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)\Z")


def _uuid(value) -> str | None:
    return value if isinstance(value, str) and _UUID.fullmatch(value) else None


def _timestamp(value) -> str | None:
    if not isinstance(value, str) or not _TIME.fullmatch(value):
        return None
    try:
        return value if datetime.fromisoformat(value.replace("Z", "+00:00")).tzinfo else None
    except ValueError:
        return None


def normalize_claude_interruption_context(value, provider_session_id=None) -> dict:
    """Allowlist a tiny initialized cursor state, including conservative empties."""
    empty = {"version": 1}
    if not isinstance(value, dict) or value.get("version") != 1:
        return empty
    keys = ("session_id", "prompt_id", "anchor_event_id", "last_event_id")
    if any(not _uuid(value.get(key)) for key in keys):
        return empty
    if provider_session_id and value["session_id"] != provider_session_id:
        return empty
    return {"version": 1, **{key: value[key] for key in keys}}


class ClaudeInterruptionTracker:
    def __init__(self, initial_context=None):
        self._context = normalize_claude_interruption_context(initial_context)

    def export_context(self) -> dict:
        return dict(self._context)

    def consume(self, event) -> dict | None:
        if not isinstance(event, dict):
            self._context = {"version": 1}
            return None
        if event.get("isSidechain") is True:
            return None
        event_id = _uuid(event.get("uuid"))
        # Non-message queue bookkeeping has no UUID and is outside the lineage.
        if event_id is None:
            if not isinstance(event.get("type"), str) or event.get("type") in ("user", "assistant", "attachment"):
                self._context = {"version": 1}
            return None
        session_id = _uuid(event.get("sessionId"))
        parent_id = _uuid(event.get("parentUuid"))
        prompt_id = _uuid(event.get("promptId"))
        timestamp = _timestamp(event.get("timestamp"))
        message = event.get("message")
        content = message.get("content") if isinstance(message, dict) else None
        blocks = content if isinstance(content, list) else []
        marker = (
            event.get("type") == "user"
            and isinstance(message, dict) and message.get("role") == "user"
            and len(blocks) == 1 and isinstance(blocks[0], dict)
            and blocks[0].get("type") == "text"
            and isinstance(blocks[0].get("text"), str)
            and blocks[0]["text"] in INTERRUPTION_MARKERS
        )
        context = self._context
        continued = (
            session_id is not None and session_id == context.get("session_id")
            and parent_id is not None and parent_id == context.get("last_event_id")
        )
        origin = None
        if marker and session_id and timestamp and (
            event.get("isMeta") is True
            or (continued and prompt_id is not None and prompt_id == context.get("prompt_id")
                and event_id != context.get("anchor_event_id"))
        ):
            origin = {
                "provider": "claude", "kind": "interruption", "cause": "unknown",
                "event_id": event_id, "session_id": session_id, "timestamp": timestamp,
            }
            if parent_id:
                origin["parent_event_id"] = parent_id
            if prompt_id:
                origin["prompt_id"] = prompt_id

        has_text = isinstance(content, str) and bool(content.strip()) or any(
            isinstance(block, dict) and block.get("type") == "text"
            and isinstance(block.get("text"), str) and bool(block["text"].strip())
            for block in blocks
        )
        tool_result = any(isinstance(block, dict) and block.get("type") == "tool_result" for block in blocks)
        real_prompt = (
            event.get("type") == "user" and isinstance(message, dict)
            and message.get("role") == "user" and has_text and not tool_result
            and not marker and event.get("isMeta") is not True
            and session_id and prompt_id and timestamp
        )
        if real_prompt:
            self._context = {"version": 1, "session_id": session_id, "prompt_id": prompt_id,
                             "anchor_event_id": event_id, "last_event_id": event_id}
        elif continued:
            self._context = {**context, "last_event_id": event_id}
        else:
            self._context = {"version": 1}
        return origin