code-context-control 2.28.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli/__init__.py +1 -0
- cli/_hook_utils.py +99 -0
- cli/c3.py +6152 -0
- cli/commands/__init__.py +1 -0
- cli/commands/common.py +312 -0
- cli/commands/parser.py +286 -0
- cli/docs.html +3178 -0
- cli/edits.html +878 -0
- cli/hook_auto_snapshot.py +142 -0
- cli/hook_c3_signal.py +61 -0
- cli/hook_c3read.py +116 -0
- cli/hook_edit_ledger.py +213 -0
- cli/hook_edit_unlock.py +170 -0
- cli/hook_filter.py +130 -0
- cli/hook_ghost_files.py +238 -0
- cli/hook_pretool_enforce.py +334 -0
- cli/hook_read.py +200 -0
- cli/hook_session_stats.py +62 -0
- cli/hook_terse_advisor.py +190 -0
- cli/hub.html +3764 -0
- cli/hub_server.py +1619 -0
- cli/mcp_proxy.py +428 -0
- cli/mcp_server.py +660 -0
- cli/server.py +2985 -0
- cli/tools/__init__.py +4 -0
- cli/tools/_helpers.py +65 -0
- cli/tools/agent.py +1165 -0
- cli/tools/compress.py +215 -0
- cli/tools/delegate.py +1184 -0
- cli/tools/edit.py +313 -0
- cli/tools/edits.py +118 -0
- cli/tools/filter.py +285 -0
- cli/tools/impact.py +163 -0
- cli/tools/memory.py +469 -0
- cli/tools/read.py +224 -0
- cli/tools/search.py +337 -0
- cli/tools/session.py +95 -0
- cli/tools/shell.py +193 -0
- cli/tools/status.py +306 -0
- cli/tools/validate.py +310 -0
- cli/ui/api.js +36 -0
- cli/ui/app.js +207 -0
- cli/ui/components/chat.js +758 -0
- cli/ui/components/dashboard.js +689 -0
- cli/ui/components/edits.js +220 -0
- cli/ui/components/instructions.js +481 -0
- cli/ui/components/memory.js +626 -0
- cli/ui/components/sessions.js +606 -0
- cli/ui/components/settings.js +1404 -0
- cli/ui/components/sidebar.js +156 -0
- cli/ui/icons.js +51 -0
- cli/ui/shared.js +119 -0
- cli/ui/theme.js +22 -0
- cli/ui.html +168 -0
- cli/ui_legacy.html +6797 -0
- cli/ui_nano.html +503 -0
- code_context_control-2.28.0.dist-info/METADATA +248 -0
- code_context_control-2.28.0.dist-info/RECORD +150 -0
- code_context_control-2.28.0.dist-info/WHEEL +5 -0
- code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
- code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
- code_context_control-2.28.0.dist-info/top_level.txt +5 -0
- core/__init__.py +75 -0
- core/config.py +269 -0
- core/ide.py +188 -0
- oracle/__init__.py +1 -0
- oracle/config.py +75 -0
- oracle/oracle.html +3900 -0
- oracle/oracle_server.py +663 -0
- oracle/services/__init__.py +1 -0
- oracle/services/c3_bridge.py +210 -0
- oracle/services/chat_engine.py +1103 -0
- oracle/services/chat_store.py +155 -0
- oracle/services/cross_memory.py +154 -0
- oracle/services/federated_graph.py +463 -0
- oracle/services/health_checker.py +117 -0
- oracle/services/insight_engine.py +307 -0
- oracle/services/memory_reader.py +106 -0
- oracle/services/memory_writer.py +182 -0
- oracle/services/ollama_bridge.py +332 -0
- oracle/services/project_scanner.py +87 -0
- oracle/services/review_agent.py +206 -0
- services/__init__.py +1 -0
- services/activity_log.py +93 -0
- services/agent_base.py +124 -0
- services/agents.py +1529 -0
- services/auto_memory.py +407 -0
- services/bench/__init__.py +6 -0
- services/bench/external/__init__.py +29 -0
- services/bench/external/aider_polyglot.py +405 -0
- services/bench/external/swe_bench.py +485 -0
- services/benchmark_dashboard.py +596 -0
- services/claude_md.py +785 -0
- services/compressor.py +592 -0
- services/context_snapshot.py +356 -0
- services/conversation_store.py +870 -0
- services/doc_index.py +537 -0
- services/e2e_benchmark.py +2884 -0
- services/e2e_evaluator.py +396 -0
- services/e2e_tasks.py +743 -0
- services/edit_ledger.py +459 -0
- services/embedding_index.py +341 -0
- services/error_reporting.py +123 -0
- services/file_memory.py +734 -0
- services/hub_service.py +585 -0
- services/indexer.py +712 -0
- services/memory.py +318 -0
- services/memory_consolidator.py +538 -0
- services/memory_graph.py +382 -0
- services/memory_grounder.py +304 -0
- services/memory_scorer.py +246 -0
- services/metrics.py +86 -0
- services/notifications.py +209 -0
- services/ollama_client.py +201 -0
- services/output_filter.py +488 -0
- services/parser.py +1238 -0
- services/project_manager.py +579 -0
- services/protocol.py +306 -0
- services/proxy_state.py +152 -0
- services/retrieval_broker.py +129 -0
- services/router.py +414 -0
- services/runtime.py +326 -0
- services/session_benchmark.py +1945 -0
- services/session_manager.py +1026 -0
- services/session_preloader.py +251 -0
- services/text_index.py +90 -0
- services/tool_classifier.py +176 -0
- services/transcript_index.py +340 -0
- services/validation_cache.py +155 -0
- services/vector_store.py +299 -0
- services/version_tracker.py +271 -0
- services/watcher.py +192 -0
- tui/__init__.py +0 -0
- tui/backend.py +59 -0
- tui/main.py +145 -0
- tui/screens/__init__.py +1 -0
- tui/screens/benchmark_view.py +109 -0
- tui/screens/claudemd_view.py +46 -0
- tui/screens/compress_view.py +52 -0
- tui/screens/index_view.py +74 -0
- tui/screens/init_view.py +82 -0
- tui/screens/mcp_view.py +73 -0
- tui/screens/optimize_view.py +41 -0
- tui/screens/pipe_view.py +46 -0
- tui/screens/projects_view.py +355 -0
- tui/screens/search_view.py +55 -0
- tui/screens/session_view.py +143 -0
- tui/screens/stats.py +158 -0
- tui/screens/ui_view.py +54 -0
- tui/theme.tcss +335 -0
services/memory.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Durable facts store with unified semantic identity and retrieval hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import json
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from services.text_index import TextIndex
|
|
12
|
+
|
|
13
|
+
# Flush pending recall telemetry (relevance_count, last_accessed_at) to disk
|
|
14
|
+
# after this many recalls when no correctness-critical write has occurred.
|
|
15
|
+
_RECALL_FLUSH_THRESHOLD = 10
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MemoryStore:
|
|
19
|
+
"""Persistent fact store with incremental lexical indexing."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, project_path: str, data_dir: str = ".c3/facts", vector_store=None):
|
|
22
|
+
self.project_path = Path(project_path)
|
|
23
|
+
self.data_dir = self.project_path / data_dir
|
|
24
|
+
self.data_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
self.facts_file = self.data_dir / "facts.json"
|
|
26
|
+
self.vector_store = vector_store
|
|
27
|
+
self.retrieval_broker = None
|
|
28
|
+
self.facts = self._load_facts()
|
|
29
|
+
self._facts_by_id = {fact["id"]: fact for fact in self.facts if fact.get("id")}
|
|
30
|
+
self._text_index = TextIndex()
|
|
31
|
+
self._rebuild_index()
|
|
32
|
+
# Recall-telemetry write-behind state.
|
|
33
|
+
self._facts_dirty = False
|
|
34
|
+
self._unflushed_recalls = 0
|
|
35
|
+
atexit.register(self._atexit_flush)
|
|
36
|
+
|
|
37
|
+
def set_retrieval_broker(self, broker):
|
|
38
|
+
self.retrieval_broker = broker
|
|
39
|
+
|
|
40
|
+
def remember(self, fact: str, category: str = "general", source_session: str = "") -> dict:
|
|
41
|
+
fact_id = uuid.uuid4().hex[:12]
|
|
42
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
43
|
+
entry = {
|
|
44
|
+
"id": fact_id,
|
|
45
|
+
"fact": fact,
|
|
46
|
+
"category": category,
|
|
47
|
+
"source_session": source_session,
|
|
48
|
+
"timestamp": now,
|
|
49
|
+
"last_accessed_at": None,
|
|
50
|
+
"relevance_count": 0,
|
|
51
|
+
"confidence": 1.0,
|
|
52
|
+
"source_quality": "user",
|
|
53
|
+
"lifecycle": "active",
|
|
54
|
+
"vector_id": fact_id,
|
|
55
|
+
"recall_sessions": [],
|
|
56
|
+
"confirmation_count": 0,
|
|
57
|
+
"contradiction_count": 0,
|
|
58
|
+
}
|
|
59
|
+
self.facts.append(entry)
|
|
60
|
+
self._facts_by_id[fact_id] = entry
|
|
61
|
+
self._index_fact(entry)
|
|
62
|
+
|
|
63
|
+
if self.vector_store:
|
|
64
|
+
try:
|
|
65
|
+
self.vector_store.add(
|
|
66
|
+
fact,
|
|
67
|
+
category,
|
|
68
|
+
metadata={
|
|
69
|
+
"fact_id": fact_id,
|
|
70
|
+
"source_session": source_session,
|
|
71
|
+
"source": "memory_store",
|
|
72
|
+
},
|
|
73
|
+
record_id=fact_id,
|
|
74
|
+
)
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
self._save_facts()
|
|
79
|
+
return {"stored": True, "id": fact_id, "total_facts": len(self.facts)}
|
|
80
|
+
|
|
81
|
+
def recall(self, query: str, top_k: int = 5, session_id: str = "") -> list[dict]:
|
|
82
|
+
if not self.facts:
|
|
83
|
+
return []
|
|
84
|
+
|
|
85
|
+
# Empty-query fallback: return most-salient + recently-accessed active facts.
|
|
86
|
+
# Without this, c3_memory(action='recall') silently returns nothing when the
|
|
87
|
+
# caller omits a query (common when agents use recall to warm context).
|
|
88
|
+
if not query or not query.strip():
|
|
89
|
+
active = [f for f in self.facts if f.get("lifecycle") != "archived"]
|
|
90
|
+
active.sort(
|
|
91
|
+
key=lambda f: (
|
|
92
|
+
int(f.get("relevance_count", 0)),
|
|
93
|
+
f.get("last_accessed_at") or f.get("timestamp") or "",
|
|
94
|
+
),
|
|
95
|
+
reverse=True,
|
|
96
|
+
)
|
|
97
|
+
return [
|
|
98
|
+
{**f, "score": 1.0, "search_method": "recent"}
|
|
99
|
+
for f in active[:top_k]
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
lexical_scores = dict(self._text_index.search(query, top_k=max(top_k * 5, 20)))
|
|
103
|
+
semantic_scores = {}
|
|
104
|
+
if self.vector_store:
|
|
105
|
+
try:
|
|
106
|
+
for result in self.vector_store.search(query, top_k=max(top_k * 3, top_k)):
|
|
107
|
+
fact_id = (result.get("metadata") or {}).get("fact_id") or result.get("id")
|
|
108
|
+
if fact_id:
|
|
109
|
+
semantic_scores[fact_id] = max(semantic_scores.get(fact_id, 0.0), float(result.get("score", 0.0)))
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
candidate_ids = set(lexical_scores) | set(semantic_scores)
|
|
114
|
+
if not candidate_ids:
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
max_lexical = max(lexical_scores.values()) if lexical_scores else 1.0
|
|
118
|
+
max_lexical = max(max_lexical, 0.001)
|
|
119
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
120
|
+
results = []
|
|
121
|
+
for fact_id in candidate_ids:
|
|
122
|
+
fact = self._facts_by_id.get(fact_id)
|
|
123
|
+
if not fact or fact.get("lifecycle") == "archived":
|
|
124
|
+
continue
|
|
125
|
+
lexical = lexical_scores.get(fact_id, 0.0) / max_lexical
|
|
126
|
+
semantic = semantic_scores.get(fact_id, 0.0)
|
|
127
|
+
score = 0.55 * lexical + 0.45 * semantic if semantic_scores else lexical
|
|
128
|
+
results.append({**fact, "score": round(score, 4), "search_method": "hybrid" if semantic_scores else "tfidf"})
|
|
129
|
+
results.sort(key=lambda item: item.get("score", 0.0), reverse=True)
|
|
130
|
+
|
|
131
|
+
# MMR rerank: balance relevance vs. diversity across a bounded candidate pool.
|
|
132
|
+
if top_k > 1 and len(results) > top_k:
|
|
133
|
+
pool = results[: max(top_k * 3, top_k + 5)]
|
|
134
|
+
results = self._mmr_rerank(pool, top_k, lam=0.7)
|
|
135
|
+
else:
|
|
136
|
+
results = results[:top_k]
|
|
137
|
+
|
|
138
|
+
changed = False
|
|
139
|
+
for result in results:
|
|
140
|
+
fact = self._facts_by_id.get(result["id"])
|
|
141
|
+
if not fact:
|
|
142
|
+
continue
|
|
143
|
+
fact["relevance_count"] = int(fact.get("relevance_count", 0)) + 1
|
|
144
|
+
fact["last_accessed_at"] = now
|
|
145
|
+
# Track cross-session recall spread
|
|
146
|
+
if session_id:
|
|
147
|
+
sessions = fact.get("recall_sessions") or []
|
|
148
|
+
if session_id not in sessions:
|
|
149
|
+
sessions.append(session_id)
|
|
150
|
+
# Keep bounded — last 50 session IDs
|
|
151
|
+
fact["recall_sessions"] = sessions[-50:]
|
|
152
|
+
changed = True
|
|
153
|
+
result["relevance_count"] = fact["relevance_count"]
|
|
154
|
+
result["last_accessed_at"] = now
|
|
155
|
+
if changed:
|
|
156
|
+
self._facts_dirty = True
|
|
157
|
+
self._unflushed_recalls += 1
|
|
158
|
+
if self._unflushed_recalls >= _RECALL_FLUSH_THRESHOLD:
|
|
159
|
+
self._save_facts()
|
|
160
|
+
return results
|
|
161
|
+
|
|
162
|
+
@staticmethod
|
|
163
|
+
def _mmr_rerank(candidates: list[dict], top_k: int, lam: float = 0.7) -> list[dict]:
|
|
164
|
+
"""Greedy Maximal Marginal Relevance over token-Jaccard similarity."""
|
|
165
|
+
if not candidates:
|
|
166
|
+
return candidates
|
|
167
|
+
token_cache: dict[str, set[str]] = {}
|
|
168
|
+
|
|
169
|
+
def toks(entry: dict) -> set[str]:
|
|
170
|
+
fid = entry.get("id", "")
|
|
171
|
+
cached = token_cache.get(fid)
|
|
172
|
+
if cached is None:
|
|
173
|
+
cached = set(TextIndex.tokenize(entry.get("fact", "")))
|
|
174
|
+
token_cache[fid] = cached
|
|
175
|
+
return cached
|
|
176
|
+
|
|
177
|
+
remaining = list(candidates)
|
|
178
|
+
selected = [remaining.pop(0)]
|
|
179
|
+
while remaining and len(selected) < top_k:
|
|
180
|
+
best_idx = 0
|
|
181
|
+
best_mmr = -1e9
|
|
182
|
+
for i, cand in enumerate(remaining):
|
|
183
|
+
c_toks = toks(cand)
|
|
184
|
+
max_sim = 0.0
|
|
185
|
+
if c_toks:
|
|
186
|
+
for s in selected:
|
|
187
|
+
s_toks = toks(s)
|
|
188
|
+
if not s_toks:
|
|
189
|
+
continue
|
|
190
|
+
sim = len(c_toks & s_toks) / len(c_toks | s_toks)
|
|
191
|
+
if sim > max_sim:
|
|
192
|
+
max_sim = sim
|
|
193
|
+
mmr = lam * cand.get("score", 0.0) - (1 - lam) * max_sim
|
|
194
|
+
if mmr > best_mmr:
|
|
195
|
+
best_mmr = mmr
|
|
196
|
+
best_idx = i
|
|
197
|
+
selected.append(remaining.pop(best_idx))
|
|
198
|
+
return selected
|
|
199
|
+
|
|
200
|
+
def flush(self) -> None:
|
|
201
|
+
"""Persist pending recall telemetry if dirty."""
|
|
202
|
+
if self._facts_dirty:
|
|
203
|
+
self._save_facts()
|
|
204
|
+
|
|
205
|
+
def _atexit_flush(self) -> None:
|
|
206
|
+
try:
|
|
207
|
+
self.flush()
|
|
208
|
+
except Exception:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
def query_all(self, query: str, top_k: int = 5) -> dict:
|
|
212
|
+
if self.retrieval_broker:
|
|
213
|
+
return self.retrieval_broker.search(query, top_k=top_k)
|
|
214
|
+
return {"facts": self.recall(query, top_k=top_k), "results": []}
|
|
215
|
+
|
|
216
|
+
def update_fact(self, fact_id: str, fact: str = "", category: str = "") -> dict:
|
|
217
|
+
entry = self._facts_by_id.get(fact_id)
|
|
218
|
+
if not entry:
|
|
219
|
+
return {"error": "not found", "id": fact_id}
|
|
220
|
+
if fact:
|
|
221
|
+
entry["fact"] = fact
|
|
222
|
+
if category:
|
|
223
|
+
entry["category"] = category
|
|
224
|
+
entry["last_accessed_at"] = datetime.now(timezone.utc).isoformat()
|
|
225
|
+
self._index_fact(entry)
|
|
226
|
+
if self.vector_store and fact:
|
|
227
|
+
try:
|
|
228
|
+
self.vector_store.delete(entry.get("vector_id") or fact_id)
|
|
229
|
+
self.vector_store.add(fact, entry["category"],
|
|
230
|
+
metadata={"fact_id": fact_id, "source": "memory_store"},
|
|
231
|
+
record_id=fact_id)
|
|
232
|
+
except Exception:
|
|
233
|
+
pass
|
|
234
|
+
self._save_facts()
|
|
235
|
+
return {"updated": True, "id": fact_id}
|
|
236
|
+
|
|
237
|
+
def delete_fact(self, fact_id: str) -> dict:
|
|
238
|
+
entry = self._facts_by_id.get(fact_id)
|
|
239
|
+
if not entry:
|
|
240
|
+
return {"error": "not found", "id": fact_id}
|
|
241
|
+
|
|
242
|
+
self.facts = [fact for fact in self.facts if fact.get("id") != fact_id]
|
|
243
|
+
self._facts_by_id.pop(fact_id, None)
|
|
244
|
+
self._text_index.remove(fact_id)
|
|
245
|
+
vector_deleted = False
|
|
246
|
+
if self.vector_store:
|
|
247
|
+
try:
|
|
248
|
+
vector_deleted = bool(self.vector_store.delete(entry.get("vector_id") or fact_id).get("deleted"))
|
|
249
|
+
except Exception:
|
|
250
|
+
vector_deleted = False
|
|
251
|
+
self._save_facts()
|
|
252
|
+
return {"deleted": True, "id": fact_id, "vector_deleted": vector_deleted}
|
|
253
|
+
|
|
254
|
+
def _index_fact(self, fact: dict):
|
|
255
|
+
doc = " ".join(
|
|
256
|
+
str(part)
|
|
257
|
+
for part in (
|
|
258
|
+
fact.get("fact", ""),
|
|
259
|
+
fact.get("category", ""),
|
|
260
|
+
fact.get("source_quality", ""),
|
|
261
|
+
fact.get("source_session", ""),
|
|
262
|
+
)
|
|
263
|
+
if part
|
|
264
|
+
)
|
|
265
|
+
self._text_index.add_or_update(fact["id"], doc)
|
|
266
|
+
|
|
267
|
+
def _rebuild_index(self):
|
|
268
|
+
docs = {}
|
|
269
|
+
for fact in self.facts:
|
|
270
|
+
if not fact.get("id"):
|
|
271
|
+
continue
|
|
272
|
+
docs[fact["id"]] = " ".join(
|
|
273
|
+
str(part)
|
|
274
|
+
for part in (
|
|
275
|
+
fact.get("fact", ""),
|
|
276
|
+
fact.get("category", ""),
|
|
277
|
+
fact.get("source_quality", ""),
|
|
278
|
+
fact.get("source_session", ""),
|
|
279
|
+
)
|
|
280
|
+
if part
|
|
281
|
+
)
|
|
282
|
+
self._text_index.rebuild(docs)
|
|
283
|
+
|
|
284
|
+
def _load_facts(self) -> list:
|
|
285
|
+
if not self.facts_file.exists():
|
|
286
|
+
return []
|
|
287
|
+
try:
|
|
288
|
+
with open(self.facts_file, encoding="utf-8") as handle:
|
|
289
|
+
facts = json.load(handle)
|
|
290
|
+
except Exception:
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
normalized = []
|
|
294
|
+
for fact in facts:
|
|
295
|
+
fact_id = fact.get("id") or uuid.uuid4().hex[:12]
|
|
296
|
+
normalized.append({
|
|
297
|
+
"id": fact_id,
|
|
298
|
+
"fact": fact.get("fact", ""),
|
|
299
|
+
"category": fact.get("category", "general"),
|
|
300
|
+
"source_session": fact.get("source_session", ""),
|
|
301
|
+
"timestamp": fact.get("timestamp", ""),
|
|
302
|
+
"last_accessed_at": fact.get("last_accessed_at"),
|
|
303
|
+
"relevance_count": int(fact.get("relevance_count", 0)),
|
|
304
|
+
"confidence": float(fact.get("confidence", 1.0)),
|
|
305
|
+
"source_quality": fact.get("source_quality", "legacy"),
|
|
306
|
+
"lifecycle": fact.get("lifecycle", "active"),
|
|
307
|
+
"vector_id": fact.get("vector_id") or fact_id,
|
|
308
|
+
"recall_sessions": fact.get("recall_sessions", []),
|
|
309
|
+
"confirmation_count": int(fact.get("confirmation_count", 0)),
|
|
310
|
+
"contradiction_count": int(fact.get("contradiction_count", 0)),
|
|
311
|
+
})
|
|
312
|
+
return normalized
|
|
313
|
+
|
|
314
|
+
def _save_facts(self):
|
|
315
|
+
with open(self.facts_file, "w", encoding="utf-8") as handle:
|
|
316
|
+
json.dump(self.facts, handle, indent=2)
|
|
317
|
+
self._facts_dirty = False
|
|
318
|
+
self._unflushed_recalls = 0
|