orchestrata_ultimate.session.archive

Reference › orchestrata_ultimate › session › archive

SessionArchiver manages the session history file, the archive that holds sessions older than the display window of context.md. It works on SessionRecord objects from models and complements the archiving that ContextFileManager does when it writes. The CLI archive command in cli/archive uses this kind of operation.

The class can check whether the history file exists and read it, archive a list of sessions to it, count the archived sessions and list their numbers, search the history for a query string, fetch one archived session by number, report statistics, clear the file, and decide with should_archive whether a context file is ready for archiving. See the session narrative for the wider design.

At a glance

Source src/orchestrata_ultimate/session/archive.py
Lines of code 262
Classes 1
Functions 0
Imports (internal) orchestrata_ultimate.core.base.traces, orchestrata_ultimate.session.models
Imported by orchestrata_ultimate.session, orchestrata_ultimate.session.cli.archive
Tests that import it tests/session/test_archive.py, tests/session/test_archive_coverage.py, tests/session/test_session_archive.py, tests/session/test_session_signatures.py

Class diagram

Class diagram orchestrata_ultimate.session.archive

Dependency diagram

Dependency diagram orchestrata_ultimate.session.archive

API

Classes

SessionArchiver

Decorators: traces(spec='006', story='FR-014', reqs=['REQ-006-FR14-004', 'REQ-006-FR14-005', 'REQ-006-FR15-001', 'REQ-006-FR15-002'… · Line 36

Manages session history archiving.

Method Signature Summary
__init__ __init__(root: str | Path) -> None Initialize archiver.
history_exists history_exists() -> bool Check if history file exists.
read_history read_history() -> str Read history file content.
archive archive(sessions: list[SessionRecord]) -> None Archive sessions to history.
count_archived count_archived() -> int Count archived sessions.
archived_session_nums archived_session_nums() -> list[int] Get archived session numbers.
search search(query: str) -> list[dict] Search history for query.
get_archived_session get_archived_session(seq: int) -> dict | None Get archived session by number.
stats stats() -> dict Get history statistics.
clear clear() -> bool Clear history file.
should_archive should_archive(ctx_path: Path) -> bool Check if context is ready for archiving.

Source

Permalink: https://gitlab.com/MSamy/orchestrata-ultimate-architecture/-/blob/a4cbc2bdf/src/orchestrata_ultimate/session/archive.py

"""Fresh implementation of archive management.

Handles session history archiving.
"""

import logging
import re
from pathlib import Path

from jinja2 import Environment, FileSystemLoader

from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.session.models import SessionRecord

# Spec: 006 - Session Management


logger = logging.getLogger(__name__)


@traces(
    spec="006",
    story="FR-014",
    reqs=[
        "REQ-006-FR14-004",
        "REQ-006-FR14-005",
        "REQ-006-FR15-001",
        "REQ-006-FR15-002",
        "REQ-006-FR15-003",
        "REQ-006-FR16-004",
        "REQ-006-FR17-004",
        "REQ-006-FR18-004",
        "REQ-006-NFR9-001",
    ],
)
class SessionArchiver:
    """Manages session history archiving."""

    HISTORY_FILE = "context.history.md"
    MAX_SESSIONS = 3
    MAX_LINES = 500

    def __init__(self, root: str | Path) -> None:
        """Initialize archiver.

        Args:
            root: Project root directory.
        """
        self.root = Path(root)
        self.history_path = self.root / self.HISTORY_FILE

        templates_dir = Path(__file__).parent / "templates"
        self.jinja = Environment(
            loader=FileSystemLoader(templates_dir),
            trim_blocks=True,
            lstrip_blocks=True,
        )

    def history_exists(self) -> bool:
        """Check if history file exists."""
        return self.history_path.exists()

    def read_history(self) -> str:
        """Read history file content.

        Returns:
            History content or empty string.
        """
        if not self.history_exists():
            return ""
        return self.history_path.read_text()

    @traces(
        spec="006",
        story="FR-015",
        reqs=[
            "REQ-006-FR15-001",
            "REQ-006-FR15-002",
            "REQ-006-FR15-003",
            "REQ-006-FR17-004",
            "REQ-006-NFR9-001",
        ],
    )
    def archive(self, sessions: list[SessionRecord]) -> None:
        """Archive sessions to history.

        Args:
            sessions: Sessions to archive.
        """
        if not sessions:
            return

        tpl = self.jinja.get_template("session.md.j2")
        new_content = ""

        for session in sessions:
            new_content += tpl.render(session=session) + "\n\n"

        if self.history_exists():
            existing = self.history_path.read_text()
            header = "# Session History Archive\n\n"
            header += "This file contains archived sessions.\n"
            header += "Sessions are archived when context.md exceeds 500 lines.\n\n"

            if existing.startswith("# Session History Archive"):
                full_content = existing + "\n" + new_content
            else:
                full_content = header + existing + "\n" + new_content
        else:
            header = "# Session History Archive\n\n"
            header += "This file contains archived sessions.\n"
            header += "Sessions are archived when context.md exceeds 500 lines.\n\n"
            full_content = header + new_content

        self.history_path.write_text(full_content)

    @traces(spec="006", story="FR-018", reqs=["REQ-006-FR18-004"])
    def count_archived(self) -> int:
        """Count archived sessions.

        Returns:
            Number of archived sessions.
        """
        if not self.history_exists():
            return 0

        content = self.read_history()
        pattern = r"### Session \d+"
        matches = re.findall(pattern, content)
        return len(matches)

    @traces(spec="006", story="FR-015", reqs=["REQ-006-FR15-003", "REQ-006-FR16-004"])
    def archived_session_nums(self) -> list[int]:
        """Get archived session numbers.

        Returns:
            List of session numbers.
        """
        if not self.history_exists():
            return []

        content = self.read_history()
        pattern = r"### Session (\d+)"
        matches = re.findall(pattern, content)
        return [int(m) for m in matches]

    def search(self, query: str) -> list[dict]:
        """Search history for query.

        Args:
            query: Search query.

        Returns:
            List of matches with context.
        """
        if not self.history_exists():
            return []

        content = self.read_history()
        lines = content.splitlines()
        results = []
        query_lower = query.lower()

        for i, line in enumerate(lines):
            if query_lower in line.lower():
                start = max(0, i - 2)
                end = min(len(lines), i + 3)
                context = "\n".join(lines[start:end])

                results.append(
                    {
                        "line_number": i + 1,
                        "line": line,
                        "context": context,
                    }
                )

        return results

    def get_archived_session(self, seq: int) -> dict | None:
        """Get archived session by number.

        Args:
            seq: Session number.

        Returns:
            Session data or None.
        """
        if not self.history_exists():
            return None

        content = self.read_history()
        pattern = rf"### Session {seq:03d} - ([\d-]+ [\d:]+) - (.+?)\n"
        match = re.search(pattern, content)

        if not match:
            pattern = rf"### Session {seq} - ([\d-]+ [\d:]+) - (.+?)\n"
            match = re.search(pattern, content)

        if not match:
            return None

        return {
            "seq": seq,
            "date": match.group(1),
            "model": match.group(2).strip(),
        }

    @traces(spec="006", story="FR-018", reqs=["REQ-006-FR18-004"])
    def stats(self) -> dict:
        """Get history statistics.

        Returns:
            Stats dict.
        """
        if not self.history_exists():
            return {
                "exists": False,
                "sessions_count": 0,
                "lines": 0,
                "size_bytes": 0,
            }

        content = self.read_history()
        lines = len(content.splitlines())
        size = self.history_path.stat().st_size

        return {
            "exists": True,
            "sessions_count": self.count_archived(),
            "lines": lines,
            "size_bytes": size,
        }

    def clear(self) -> bool:
        """Clear history file.

        Returns:
            True if cleared.
        """
        if not self.history_exists():
            return False

        self.history_path.unlink()
        return True

    @traces(spec="006", story="FR-014", reqs=["REQ-006-FR14-005"])
    def should_archive(self, ctx_path: Path) -> bool:
        """Check if context is ready for archiving.

        Args:
            ctx_path: Path to context.md.

        Returns:
            True if archiving needed.
        """
        if not ctx_path.exists():
            return False

        content = ctx_path.read_text()
        lines = len(content.splitlines())
        return lines > self.MAX_LINES