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.
Files changed (150) hide show
  1. cli/__init__.py +1 -0
  2. cli/_hook_utils.py +99 -0
  3. cli/c3.py +6152 -0
  4. cli/commands/__init__.py +1 -0
  5. cli/commands/common.py +312 -0
  6. cli/commands/parser.py +286 -0
  7. cli/docs.html +3178 -0
  8. cli/edits.html +878 -0
  9. cli/hook_auto_snapshot.py +142 -0
  10. cli/hook_c3_signal.py +61 -0
  11. cli/hook_c3read.py +116 -0
  12. cli/hook_edit_ledger.py +213 -0
  13. cli/hook_edit_unlock.py +170 -0
  14. cli/hook_filter.py +130 -0
  15. cli/hook_ghost_files.py +238 -0
  16. cli/hook_pretool_enforce.py +334 -0
  17. cli/hook_read.py +200 -0
  18. cli/hook_session_stats.py +62 -0
  19. cli/hook_terse_advisor.py +190 -0
  20. cli/hub.html +3764 -0
  21. cli/hub_server.py +1619 -0
  22. cli/mcp_proxy.py +428 -0
  23. cli/mcp_server.py +660 -0
  24. cli/server.py +2985 -0
  25. cli/tools/__init__.py +4 -0
  26. cli/tools/_helpers.py +65 -0
  27. cli/tools/agent.py +1165 -0
  28. cli/tools/compress.py +215 -0
  29. cli/tools/delegate.py +1184 -0
  30. cli/tools/edit.py +313 -0
  31. cli/tools/edits.py +118 -0
  32. cli/tools/filter.py +285 -0
  33. cli/tools/impact.py +163 -0
  34. cli/tools/memory.py +469 -0
  35. cli/tools/read.py +224 -0
  36. cli/tools/search.py +337 -0
  37. cli/tools/session.py +95 -0
  38. cli/tools/shell.py +193 -0
  39. cli/tools/status.py +306 -0
  40. cli/tools/validate.py +310 -0
  41. cli/ui/api.js +36 -0
  42. cli/ui/app.js +207 -0
  43. cli/ui/components/chat.js +758 -0
  44. cli/ui/components/dashboard.js +689 -0
  45. cli/ui/components/edits.js +220 -0
  46. cli/ui/components/instructions.js +481 -0
  47. cli/ui/components/memory.js +626 -0
  48. cli/ui/components/sessions.js +606 -0
  49. cli/ui/components/settings.js +1404 -0
  50. cli/ui/components/sidebar.js +156 -0
  51. cli/ui/icons.js +51 -0
  52. cli/ui/shared.js +119 -0
  53. cli/ui/theme.js +22 -0
  54. cli/ui.html +168 -0
  55. cli/ui_legacy.html +6797 -0
  56. cli/ui_nano.html +503 -0
  57. code_context_control-2.28.0.dist-info/METADATA +248 -0
  58. code_context_control-2.28.0.dist-info/RECORD +150 -0
  59. code_context_control-2.28.0.dist-info/WHEEL +5 -0
  60. code_context_control-2.28.0.dist-info/entry_points.txt +4 -0
  61. code_context_control-2.28.0.dist-info/licenses/LICENSE +201 -0
  62. code_context_control-2.28.0.dist-info/top_level.txt +5 -0
  63. core/__init__.py +75 -0
  64. core/config.py +269 -0
  65. core/ide.py +188 -0
  66. oracle/__init__.py +1 -0
  67. oracle/config.py +75 -0
  68. oracle/oracle.html +3900 -0
  69. oracle/oracle_server.py +663 -0
  70. oracle/services/__init__.py +1 -0
  71. oracle/services/c3_bridge.py +210 -0
  72. oracle/services/chat_engine.py +1103 -0
  73. oracle/services/chat_store.py +155 -0
  74. oracle/services/cross_memory.py +154 -0
  75. oracle/services/federated_graph.py +463 -0
  76. oracle/services/health_checker.py +117 -0
  77. oracle/services/insight_engine.py +307 -0
  78. oracle/services/memory_reader.py +106 -0
  79. oracle/services/memory_writer.py +182 -0
  80. oracle/services/ollama_bridge.py +332 -0
  81. oracle/services/project_scanner.py +87 -0
  82. oracle/services/review_agent.py +206 -0
  83. services/__init__.py +1 -0
  84. services/activity_log.py +93 -0
  85. services/agent_base.py +124 -0
  86. services/agents.py +1529 -0
  87. services/auto_memory.py +407 -0
  88. services/bench/__init__.py +6 -0
  89. services/bench/external/__init__.py +29 -0
  90. services/bench/external/aider_polyglot.py +405 -0
  91. services/bench/external/swe_bench.py +485 -0
  92. services/benchmark_dashboard.py +596 -0
  93. services/claude_md.py +785 -0
  94. services/compressor.py +592 -0
  95. services/context_snapshot.py +356 -0
  96. services/conversation_store.py +870 -0
  97. services/doc_index.py +537 -0
  98. services/e2e_benchmark.py +2884 -0
  99. services/e2e_evaluator.py +396 -0
  100. services/e2e_tasks.py +743 -0
  101. services/edit_ledger.py +459 -0
  102. services/embedding_index.py +341 -0
  103. services/error_reporting.py +123 -0
  104. services/file_memory.py +734 -0
  105. services/hub_service.py +585 -0
  106. services/indexer.py +712 -0
  107. services/memory.py +318 -0
  108. services/memory_consolidator.py +538 -0
  109. services/memory_graph.py +382 -0
  110. services/memory_grounder.py +304 -0
  111. services/memory_scorer.py +246 -0
  112. services/metrics.py +86 -0
  113. services/notifications.py +209 -0
  114. services/ollama_client.py +201 -0
  115. services/output_filter.py +488 -0
  116. services/parser.py +1238 -0
  117. services/project_manager.py +579 -0
  118. services/protocol.py +306 -0
  119. services/proxy_state.py +152 -0
  120. services/retrieval_broker.py +129 -0
  121. services/router.py +414 -0
  122. services/runtime.py +326 -0
  123. services/session_benchmark.py +1945 -0
  124. services/session_manager.py +1026 -0
  125. services/session_preloader.py +251 -0
  126. services/text_index.py +90 -0
  127. services/tool_classifier.py +176 -0
  128. services/transcript_index.py +340 -0
  129. services/validation_cache.py +155 -0
  130. services/vector_store.py +299 -0
  131. services/version_tracker.py +271 -0
  132. services/watcher.py +192 -0
  133. tui/__init__.py +0 -0
  134. tui/backend.py +59 -0
  135. tui/main.py +145 -0
  136. tui/screens/__init__.py +1 -0
  137. tui/screens/benchmark_view.py +109 -0
  138. tui/screens/claudemd_view.py +46 -0
  139. tui/screens/compress_view.py +52 -0
  140. tui/screens/index_view.py +74 -0
  141. tui/screens/init_view.py +82 -0
  142. tui/screens/mcp_view.py +73 -0
  143. tui/screens/optimize_view.py +41 -0
  144. tui/screens/pipe_view.py +46 -0
  145. tui/screens/projects_view.py +355 -0
  146. tui/screens/search_view.py +55 -0
  147. tui/screens/session_view.py +143 -0
  148. tui/screens/stats.py +158 -0
  149. tui/screens/ui_view.py +54 -0
  150. 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