File size: 6,517 Bytes
891669b
d715ed0
 
 
 
891669b
 
 
 
 
 
 
6186ca4
cce30c5
891669b
e9073c0
 
 
891669b
 
 
 
6186ca4
 
adbf39e
6186ca4
cce30c5
 
 
 
acf2df3
adbf39e
 
 
 
 
 
 
 
 
891669b
e9073c0
 
 
 
891669b
859c2fd
891669b
 
 
 
 
 
 
 
 
cce30c5
acf2df3
891669b
 
cce30c5
 
 
 
 
edeb6b2
 
cce30c5
891669b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edeb6b2
e9073c0
 
d4d4456
e9073c0
 
d4d4456
edeb6b2
 
 
e9073c0
 
d4d4456
 
891669b
edeb6b2
891669b
 
 
68bf032
e9073c0
 
 
edeb6b2
e9073c0
 
 
 
 
 
 
891669b
68bf032
891669b
 
e9073c0
891669b
 
e9073c0
d715ed0
 
 
891669b
 
edeb6b2
891669b
 
cce30c5
891669b
 
cce30c5
891669b
 
cce30c5
891669b
 
cce30c5
891669b
 
cce30c5
891669b
 
d4d4456
891669b
 
d4d4456
891669b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# ---- Changelog ----
# [2026-04-06] Josh + Claude — Add edit_file facade method
# What: Delegate edit_file to FilesystemTool
# Why: New edit_file tool needs facade wiring like read_file/write_file
# How: One-line delegation to self._fs.edit_file()
# [2026-03-29] Chisel/TQB — Block C: Rewrite as thin facade
# What: RecursiveContextManager is now a facade delegating to tools/ classes
# Why: PRD Block C — split 267-line god-class into focused single-responsibility tools
# How: Constructor wires tool instances; methods delegate; cross-cutting sync stays here
# -------------------

import logging
import os
from pathlib import Path
from typing import List, Dict
from huggingface_hub import HfApi, hf_hub_download

from openclaw_hook import NeuroGraphMemory
from tools import FilesystemTool, GitTool, NotebookTool, NeurographTool, ShellTool, WorkspaceTool

logger = logging.getLogger("recursive_context")


class RecursiveContextManager:
    def __init__(self, repo_path: str, ng=None):
        self.repo_path = Path(repo_path)
        self.memory_path = self.repo_path / "memory"
        self.notebook_file = self.memory_path / "notebook.json"
        self.token = os.getenv("HF_TOKEN")
        self.dataset_id = os.getenv("DATASET_ID", "Executor-Tyrant-Framework/clawdbot-memory")

        # Use the passed-in NG instance (owned by worker_ng) or fall back to creating one
        if ng is not None:
            self.ng = ng
        else:
            neurograph_workspace = os.getenv(
                "NEUROGRAPH_WORKSPACE_DIR",
                str(self.repo_path / "data" / "neurograph_worker")
            )
            self.ng = NeuroGraphMemory.get_instance(workspace_dir=neurograph_workspace)
        logger.info("NeuroGraph Memory Loaded.")

        self._saves_since_ng_backup = 0
        self.NG_BACKUP_EVERY_N = 10

        # --- Tool instances ---
        import policy_engine as pe  # Cricket-shaped enforcement
        self._fs = FilesystemTool(self.repo_path, pe)
        self._git = GitTool(self.repo_path, pe)
        self._notebook = NotebookTool(self.repo_path, pe,
                                       notebook_file=self.notebook_file,
                                       save_callback=self._save_notebook)
        self._ng_tool = NeurographTool(self.repo_path, self.ng, pe)
        self._shell = ShellTool(self.repo_path, pe)
        self._workspace = WorkspaceTool(self.repo_path, self.ng, pe)

        self._init_memory()

    # === Cross-cutting sync (stays in facade) ===

    def _init_memory(self):
        self.memory_path.mkdir(parents=True, exist_ok=True)
        if self.token:
            try:
                hf_hub_download(
                    repo_id=self.dataset_id, filename="notebook.json", repo_type="dataset",
                    token=self.token, local_dir=self.memory_path, local_dir_use_symlinks=False
                )
            except (OSError, ValueError) as e:
                logger.warning("Failed to download notebook from HF: %s", e)
                self._notebook._save_local([])

    def _save_notebook(self, notes: List[Dict]):
        if self.token and self.dataset_id:
            try:
                api = HfApi(token=self.token)
                api.upload_file(
                    path_or_fileobj=self.notebook_file, path_in_repo="notebook.json",
                    repo_id=self.dataset_id, repo_type="dataset",
                    commit_message=f"Notebook Update: {len(notes)}"
                )
            except (OSError, ConnectionError) as e:
                logger.warning("HF notebook sync failed: %s", e)

    def _backup_ng_checkpoint_to_dataset(self):
        if not self.token:
            return
        checkpoint_path = Path(self.ng.save())
        if not checkpoint_path.exists():
            return
        try:
            api = HfApi(token=self.token)
            api.upload_file(
                path_or_fileobj=checkpoint_path,
                path_in_repo="neurograph/main.msgpack",
                repo_id=self.dataset_id,
                repo_type="dataset",
                commit_message=f"NeuroGraph checkpoint ({self.ng.stats()['nodes']} nodes)"
            )
            logger.info("NeuroGraph checkpoint uploaded.")
        except (OSError, ConnectionError) as e:
            logger.warning("NeuroGraph checkpoint upload failed: %s", e)

    def save_conversation_turn(self, user_msg: str, assist_msg: str, turn_id: int):
        from openclaw_hook import NeuroGraphMemory
        from universal_ingestor import SourceType
        combined = f"USER: {user_msg}\n\nASSISTANT: {assist_msg}"
        self.ng.on_message(combined, source_type=SourceType.TEXT)
        self.ng.step(5)
        self._saves_since_ng_backup += 1
        if self._saves_since_ng_backup >= self.NG_BACKUP_EVERY_N:
            self._backup_ng_checkpoint_to_dataset()
            self._saves_since_ng_backup = 0

    # === Delegated methods ===

    def read_file(self, path, start_line=None, end_line=None):
        return self._fs.read_file(path, start_line, end_line)

    def write_file(self, path, content):
        return self._fs.write_file(path, content)

    def edit_file(self, path, old_text, new_text):
        return self._fs.edit_file(path, old_text, new_text)

    def list_files(self, path=".", max_depth=3):
        return self._fs.list_files(path, max_depth)

    def push_to_github(self, message):
        return self._git.push_to_github(message)

    def pull_from_github(self, branch):
        return self._git.pull_from_github(branch)

    def create_shadow_branch(self):
        return self._git.create_shadow_branch()

    def notebook_read(self):
        return self._notebook.notebook_read()

    def notebook_add(self, content):
        return self._notebook.notebook_add(content)

    def notebook_delete(self, index):
        return self._notebook.notebook_delete(index)

    def search_conversations(self, query, n=5):
        return self._ng_tool.search_conversations(query, n)

    def search_code(self, query, n=5):
        return self._ng_tool.search_code(query, n)

    def search_testament(self, query, n=5):
        return self._ng_tool.search_testament(query, n)

    def ingest_workspace(self):
        return self._ng_tool.ingest_workspace()

    def shell_execute(self, command):
        return self._shell.shell_execute(command)

    def map_repository_structure(self):
        return self._workspace.map_repository_structure()

    def get_stats(self):
        return self._workspace.get_stats()