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
cli/tools/memory.py ADDED
@@ -0,0 +1,469 @@
1
+ """c3_memory — Facts, graph, scoring, grounding, and cross-session recall."""
2
+ from datetime import datetime, timezone
3
+
4
+
5
+ def handle_memory(action: str, query: str, fact: str, category: str,
6
+ top_k: int, svc, finalize, fact_id: str = "") -> str:
7
+ if action == "add":
8
+ sid = (svc.session_mgr.current_session or {}).get("id", "")
9
+ res = svc.memory.remember(fact, category or "general", sid)
10
+ return finalize("c3_memory", {"action": action},
11
+ f"[remembered:{res['id']}] total:{res['total_facts']}", res['id'])
12
+
13
+ if action == "recall":
14
+ session_id = (svc.session_mgr.current_session or {}).get("id", "")
15
+ results = svc.memory.recall(query, top_k=top_k, session_id=session_id)
16
+ # Small recalls skip scoring + graph spreading to stay fast —
17
+ # agents using top_k<=3 want quick lookups, not full enrichment.
18
+ fast_mode = top_k <= 3
19
+ backend = "tfidf"
20
+ if svc.vector_store:
21
+ v_res = svc.vector_store.search(query, top_k=top_k)
22
+ for r in v_res:
23
+ semantic_text = (r.get("content") or r.get("text") or r.get("fact") or "").strip()
24
+ if not semantic_text:
25
+ continue
26
+ if not any(f.get("fact") == semantic_text for f in results):
27
+ metadata = r.get("metadata") or {}
28
+ results.append({
29
+ "category": metadata.get("category", r.get("category", "semantic")),
30
+ "fact": semantic_text,
31
+ })
32
+ if v_res:
33
+ backend = "hybrid"
34
+
35
+ # Record co-recall edges in the memory graph
36
+ graph = getattr(svc, "memory_graph", None)
37
+ if graph and len(results) >= 2:
38
+ recalled_ids = [r["id"] for r in results if r.get("id")]
39
+ if len(recalled_ids) >= 2:
40
+ graph.record_co_recall(recalled_ids[:top_k])
41
+
42
+ # Enrich results with salience scores (skipped in fast_mode)
43
+ scorer = getattr(svc, "memory_scorer", None)
44
+ if scorer and not fast_mode:
45
+ for r in results:
46
+ if r.get("id"):
47
+ s = scorer.score(r, graph)
48
+ r["salience"] = s["salience"]
49
+ r["tier"] = s["tier"]
50
+
51
+ # Spreading activation: find related facts via graph (skipped in fast_mode)
52
+ activated_extra = []
53
+ if graph and results and not fast_mode:
54
+ seed_ids = [r["id"] for r in results if r.get("id")][:5]
55
+ if seed_ids:
56
+ activated = graph.spreading_activation(seed_ids, max_depth=2, max_results=5)
57
+ facts_by_id = {f["id"]: f for f in svc.memory.facts}
58
+ for a in activated:
59
+ fact = facts_by_id.get(a["id"])
60
+ if fact and not any(r.get("id") == a["id"] for r in results):
61
+ activated_extra.append(fact)
62
+
63
+ # Local RAG Pipeline: auto-retrieve project docs on first recall
64
+ precontext = ""
65
+ if hasattr(svc, "preloader") and svc.preloader:
66
+ if session_id:
67
+ precontext = svc.preloader.preload(query, session_id, top_k=top_k)
68
+
69
+ if not results and not activated_extra and not precontext:
70
+ return finalize("c3_memory", {"action": action},
71
+ f"[memory:recall:{query}] 0 results (backend:{backend})", "0")
72
+ parts = []
73
+ for f in results[:top_k]:
74
+ sal = f" sal={f['salience']:.2f}/{f['tier']}" if f.get("salience") is not None else ""
75
+ parts.append(f"[{f['category']}]{sal} {f['fact']}")
76
+ if activated_extra:
77
+ parts.append(f" [graph:activated] {len(activated_extra)} related facts:")
78
+ for f in activated_extra[:3]:
79
+ parts.append(f" [{f.get('category','')}] {f['fact'][:80]}")
80
+ recall_text = f"[recall:{query}] {len(results)} facts (backend:{backend})\n" + "\n".join(parts)
81
+
82
+ if precontext:
83
+ recall_text = precontext + recall_text
84
+
85
+ return finalize("c3_memory", {"action": action}, recall_text, f"{len(results)}f")
86
+
87
+ if action == "index":
88
+ # Compact index — IDs + one-liners. Follow up with fetch(fact_id=...) for full text.
89
+ session_id = (svc.session_mgr.current_session or {}).get("id", "")
90
+ results = svc.memory.recall(query, top_k=top_k, session_id=session_id)
91
+ scorer = getattr(svc, "memory_scorer", None)
92
+ graph = getattr(svc, "memory_graph", None)
93
+ if not results:
94
+ return finalize("c3_memory", {"action": action},
95
+ f"[memory:index] 0 results for '{query}'", "0")
96
+ lines = [f"[memory:index] {len(results)} facts — use fetch(fact_id='id1,id2,...') for full text"]
97
+ for f in results:
98
+ sal = ""
99
+ if scorer and f.get("id"):
100
+ s = scorer.score(f, graph)
101
+ sal = f" sal={s['salience']:.2f}/{s['tier']}"
102
+ snippet = f["fact"][:80].replace("\n", " ")
103
+ lines.append(f" {f['id']} [{f['category']}]{sal} {snippet}")
104
+ return finalize("c3_memory", {"action": action}, "\n".join(lines), f"{len(results)}f")
105
+
106
+ if action == "fetch":
107
+ # Full details for specific fact IDs (comma-separated).
108
+ if not fact_id:
109
+ return "[memory:error] fetch requires fact_id (comma-separated IDs from index)"
110
+ ids = [i.strip() for i in fact_id.split(",") if i.strip()]
111
+ facts_by_id = {f["id"]: f for f in svc.memory.facts}
112
+ lines = []
113
+ found = 0
114
+ for fid in ids:
115
+ f = facts_by_id.get(fid)
116
+ if not f:
117
+ lines.append(f" {fid} — not found")
118
+ continue
119
+ found += 1
120
+ rc = f.get("relevance_count", 0)
121
+ ts = (f.get("timestamp") or "")[:10]
122
+ lines.append(f"[{fid}] [{f['category']}] rc={rc} added={ts}")
123
+ lines.append(f" {f['fact']}")
124
+ header = f"[memory:fetch] {found}/{len(ids)} facts"
125
+ return finalize("c3_memory", {"action": action}, header + "\n" + "\n".join(lines), f"{found}f")
126
+
127
+ if action == "query":
128
+ res = svc.memory.query_all(query, top_k=top_k)
129
+ backend = "tfidf"
130
+ if svc.vector_store and svc.vector_store.vector_enabled:
131
+ backend = "hybrid"
132
+ parts = [f"[{f['category']}] {f['fact'][:80]}" for f in res['facts']]
133
+ parts += [f"[session:{s['session_id'][:12]}] {s.get('summary', '')[:80]}"
134
+ for s in res.get('sessions', [])]
135
+ parts += [f"[conversation:{c['session_id'][:12]}] {(c.get('snippet') or c.get('text', ''))[:80]}"
136
+ for c in res.get('conversations', [])[:top_k]]
137
+ parts += [f"[file:{f['path']}] {(f.get('summary') or '')[:80]}"
138
+ for f in res.get('files', [])[:top_k]]
139
+ return finalize("c3_memory", {"action": action},
140
+ f"[query:{query}] {len(parts)} hits (backend:{backend})\n" + "\n".join(parts),
141
+ f"{len(parts)}h")
142
+
143
+ if action == "update":
144
+ if not fact_id:
145
+ return "[memory:error] update requires fact_id"
146
+ res = svc.memory.update_fact(fact_id, fact=fact, category=category)
147
+ if res.get("error"):
148
+ return f"[memory:error] {res['error']} (id={fact_id})"
149
+ return finalize("c3_memory", {"action": action},
150
+ f"[updated:{fact_id}]", fact_id)
151
+
152
+ if action == "delete":
153
+ if not fact_id:
154
+ return "[memory:error] delete requires fact_id"
155
+ res = svc.memory.delete_fact(fact_id)
156
+ if res.get("error"):
157
+ return f"[memory:error] {res['error']} (id={fact_id})"
158
+ return finalize("c3_memory", {"action": action},
159
+ f"[deleted:{fact_id}]", fact_id)
160
+
161
+ if action == "list":
162
+ all_facts = svc.memory.facts
163
+ active = [f for f in all_facts if f.get("lifecycle") != "archived"]
164
+ facts = active
165
+ if category:
166
+ facts = [f for f in facts if f.get("category") == category]
167
+ if not facts:
168
+ total = len(all_facts)
169
+ active_n = len(active)
170
+ if category and active_n > 0:
171
+ cats = sorted({f.get("category", "general") for f in active})
172
+ hint = f" (no match for category='{category}'; active categories: {', '.join(cats)})"
173
+ else:
174
+ hint = ""
175
+ return finalize("c3_memory", {"action": action},
176
+ f"[memory:list] 0 facts (total={total} active={active_n}){hint}", "0")
177
+ by_cat: dict = {}
178
+ for f in facts:
179
+ by_cat.setdefault(f.get("category", "general"), []).append(f)
180
+ header_scope = f"category='{category}'" if category else "all"
181
+ lines = [f"[memory:list] {len(facts)} fact(s) scope={header_scope} "
182
+ f"(total={len(all_facts)} active={len(active)})"]
183
+ for cat, entries in sorted(by_cat.items()):
184
+ lines.append(f" [{cat}] ({len(entries)})")
185
+ for e in entries:
186
+ rc = e.get("relevance_count", 0)
187
+ lines.append(f" {e['id']} (rc={rc}) {e['fact'][:80]}")
188
+ return finalize("c3_memory", {"action": action},
189
+ "\n".join(lines), f"{len(facts)}f")
190
+
191
+ if action == "review":
192
+ facts = [f for f in svc.memory.facts if f.get("lifecycle") != "archived"]
193
+ total = len(facts)
194
+ scorer = getattr(svc, "memory_scorer", None)
195
+ graph = getattr(svc, "memory_graph", None)
196
+
197
+ # Score all facts and partition by tier
198
+ tier_counts = {"core": 0, "active": 0, "dormant": 0, "ephemeral": 0}
199
+ scored_facts = []
200
+ if scorer:
201
+ for f in facts:
202
+ s = scorer.score(f, graph)
203
+ scored_facts.append((f, s))
204
+ tier_counts[s["tier"]] += 1
205
+
206
+ # Unused: never recalled
207
+ unused = [f for f in facts if f.get("relevance_count", 0) == 0]
208
+ # Simple Jaccard duplicate detection
209
+ def _tokens(text):
210
+ return set(text.lower().split())
211
+ pairs = []
212
+ for i in range(len(facts)):
213
+ for j in range(i + 1, len(facts)):
214
+ a, b = facts[i], facts[j]
215
+ ta, tb = _tokens(a["fact"]), _tokens(b["fact"])
216
+ if not ta or not tb:
217
+ continue
218
+ sim = len(ta & tb) / len(ta | tb)
219
+ if sim >= 0.6:
220
+ pairs.append((a, b, round(sim, 2)))
221
+ if len(pairs) >= 5:
222
+ break
223
+
224
+ lines = [f"[memory:review] {total} facts total"]
225
+
226
+ # Tier breakdown
227
+ if scorer:
228
+ lines.append(f" Tiers: core={tier_counts['core']} active={tier_counts['active']} "
229
+ f"dormant={tier_counts['dormant']} ephemeral={tier_counts['ephemeral']}")
230
+
231
+ # Graph stats
232
+ if graph:
233
+ gs = graph.stats()
234
+ lines.append(f" Graph: {gs['total_edges']} edges, {gs['total_nodes']} nodes, "
235
+ f"{gs['clusters']} clusters")
236
+
237
+ if pairs:
238
+ lines.append(f" Potential duplicates ({len(pairs)}):")
239
+ for a, b, sim in pairs[:5]:
240
+ lines.append(f" {a['id']} ≈ {b['id']} (sim={sim})")
241
+ lines.append(f" A: {a['fact'][:60]}")
242
+ lines.append(f" B: {b['fact'][:60]}")
243
+ if unused:
244
+ lines.append(f" Never-recalled facts ({len(unused)}) — consider deleting:")
245
+ for f in unused[:5]:
246
+ lines.append(f" {f['id']} [{f.get('category','?')}] {f['fact'][:70]}")
247
+ # Verbose facts: >500 chars, never recalled
248
+ verbose = [
249
+ f for f in facts
250
+ if len(f.get("fact", "")) > 500 and f.get("relevance_count", 0) == 0
251
+ ]
252
+ if verbose:
253
+ lines.append(f" Verbose never-recalled ({len(verbose)}):")
254
+ for f in verbose[:5]:
255
+ lines.append(f" {f['id']} {len(f['fact'])}ch — {f['fact'][:60]}...")
256
+
257
+ # Low-salience facts (ephemeral tier)
258
+ if scored_facts:
259
+ ephemeral = [(f, s) for f, s in scored_facts if s["tier"] == "ephemeral"]
260
+ if ephemeral:
261
+ lines.append(f" Ephemeral (auto-prune candidates): {len(ephemeral)}")
262
+ for f, s in ephemeral[:3]:
263
+ lines.append(f" {f['id']} sal={s['salience']:.2f} — {f['fact'][:60]}")
264
+
265
+ # Stale session summaries: auto:session older than 14 days
266
+ now_dt = datetime.now(timezone.utc)
267
+ stale_sessions = []
268
+ for f in facts:
269
+ if f.get("category") != "auto:session":
270
+ continue
271
+ try:
272
+ age = (now_dt - datetime.fromisoformat(f.get("timestamp", ""))).days
273
+ except (ValueError, TypeError):
274
+ age = 0
275
+ if age >= 14:
276
+ stale_sessions.append((f, age))
277
+ if stale_sessions:
278
+ lines.append(f" Stale sessions ({len(stale_sessions)}, >14d):")
279
+ for f, age in stale_sessions[:5]:
280
+ lines.append(f" {f['id']} ({age}d) — {f['fact'][:60]}")
281
+
282
+ if not pairs and not unused and not verbose and not stale_sessions and not ephemeral:
283
+ lines.append(" No issues found.")
284
+ lines.append(" Actions: consolidate, consolidate_deep, score, graph, ground, trends, lifespan")
285
+ return finalize("c3_memory", {"action": action},
286
+ "\n".join(lines), f"{total}f")
287
+
288
+ if action == "export":
289
+ facts = [f for f in svc.memory.facts if f.get("lifecycle") != "archived"]
290
+ if category:
291
+ facts = [f for f in facts if f.get("category") == category]
292
+ if not facts:
293
+ return finalize("c3_memory", {"action": action},
294
+ "[memory:export] 0 facts to export", "0")
295
+ # Sort by relevance_count desc, then recency
296
+ facts.sort(key=lambda f: (f.get("relevance_count", 0), f.get("last_accessed_at") or ""), reverse=True)
297
+ # Group by category
298
+ by_cat: dict = {}
299
+ for f in facts:
300
+ by_cat.setdefault(f.get("category", "general"), []).append(f)
301
+ lines = ["# C3 Memory Export", ""]
302
+ for cat, entries in sorted(by_cat.items()):
303
+ lines.append(f"## {cat}")
304
+ lines.append("")
305
+ for e in entries:
306
+ lines.append(f"- {e['fact']}")
307
+ lines.append("")
308
+ md = "\n".join(lines).rstrip() + "\n"
309
+ return finalize("c3_memory", {"action": action}, md, f"{len(facts)}f")
310
+
311
+ if action == "consolidate":
312
+ if not hasattr(svc, "auto_memory"):
313
+ return finalize("c3_memory", {"action": action},
314
+ "[memory:consolidate] auto_memory not available", "skip")
315
+ stats = svc.auto_memory.consolidate()
316
+ lines = [
317
+ "[memory:consolidate] done",
318
+ f" Merged: {stats['merged']} duplicate pairs",
319
+ f" Archived: {stats['archived']} stale auto-facts",
320
+ f" Remaining: {stats['total']} facts",
321
+ ]
322
+ return finalize("c3_memory", {"action": action},
323
+ "\n".join(lines), f"m{stats['merged']}a{stats['archived']}")
324
+
325
+ if action == "consolidate_deep":
326
+ consolidator = getattr(svc, "memory_consolidator", None)
327
+ if not consolidator:
328
+ return finalize("c3_memory", {"action": action},
329
+ "[memory:consolidate_deep] consolidator not available", "skip")
330
+ session = svc.session_mgr.current_session
331
+ stats = consolidator.run(current_session=session)
332
+ phases = stats.get("phases", {})
333
+ lines = ["[memory:consolidate_deep] 4-phase pipeline complete"]
334
+ for phase_name, phase_stats in phases.items():
335
+ lines.append(f" {phase_name}: {phase_stats}")
336
+ lines.append(f" Total active facts: {stats.get('total_facts', '?')}")
337
+ return finalize("c3_memory", {"action": action},
338
+ "\n".join(lines), f"deep:{stats.get('total_facts', 0)}")
339
+
340
+ if action == "score":
341
+ scorer = getattr(svc, "memory_scorer", None)
342
+ graph = getattr(svc, "memory_graph", None)
343
+ if not scorer:
344
+ return finalize("c3_memory", {"action": action},
345
+ "[memory:score] scorer not available", "skip")
346
+ facts = [f for f in svc.memory.facts if f.get("lifecycle") == "active"]
347
+ if fact_id:
348
+ facts = [f for f in facts if f["id"] == fact_id]
349
+ if not facts:
350
+ return finalize("c3_memory", {"action": action},
351
+ "[memory:score] no matching facts", "0")
352
+ scores = scorer.score_batch(facts[:20], graph)
353
+ lines = [f"[memory:score] {len(scores)} facts scored"]
354
+ for s in scores:
355
+ lines.append(f" {s['id']} sal={s['salience']:.3f} tier={s['tier']}")
356
+ sig = s.get("signals", {})
357
+ top_signals = sorted(sig.items(), key=lambda x: x[1], reverse=True)[:3]
358
+ lines.append(f" top: {', '.join(f'{k}={v:.2f}' for k, v in top_signals)}")
359
+ return finalize("c3_memory", {"action": action},
360
+ "\n".join(lines), f"{len(scores)}s")
361
+
362
+ if action == "graph":
363
+ graph = getattr(svc, "memory_graph", None)
364
+ if not graph:
365
+ return finalize("c3_memory", {"action": action},
366
+ "[memory:graph] graph not available", "skip")
367
+ if fact_id:
368
+ # Show edges for a specific fact
369
+ edges = graph.get_edges(fact_id)
370
+ neighbors = graph.get_neighbors(fact_id)
371
+ lines = [f"[memory:graph] node={fact_id} edges={len(edges)} neighbors={len(neighbors)}"]
372
+ for e in edges[:10]:
373
+ other = e["dst"] if e["src"] == fact_id else e["src"]
374
+ lines.append(f" --{e['type']}--> {other} (w={e['weight']:.2f}, hits={e.get('hit_count', 0)})")
375
+ # Show clusters containing this fact
376
+ clusters = graph.detect_clusters()
377
+ for i, c in enumerate(clusters):
378
+ if fact_id in c:
379
+ lines.append(f" Cluster #{i}: {len(c)} members — {', '.join(c[:5])}")
380
+ else:
381
+ # Show overall graph stats
382
+ gs = graph.stats()
383
+ clusters = graph.detect_clusters()
384
+ lines = [
385
+ f"[memory:graph] {gs['total_edges']} edges, {gs['total_nodes']} nodes",
386
+ f" Edge types: {gs['edge_types']}",
387
+ f" Clusters: {len(clusters)}",
388
+ ]
389
+ facts_by_id = {f["id"]: f for f in svc.memory.facts}
390
+ for i, c in enumerate(clusters[:5]):
391
+ member_facts = [facts_by_id.get(fid, {}).get("fact", "?")[:40] for fid in c[:3]]
392
+ lines.append(f" Cluster #{i} ({len(c)} nodes): {'; '.join(member_facts)}")
393
+ return finalize("c3_memory", {"action": action},
394
+ "\n".join(lines), f"g:{graph.stats()['total_edges']}e")
395
+
396
+ if action == "ground":
397
+ grounder = getattr(svc, "memory_grounder", None)
398
+ if not grounder:
399
+ return finalize("c3_memory", {"action": action},
400
+ "[memory:ground] grounder not available", "skip")
401
+ if fact_id:
402
+ fact = svc.memory._facts_by_id.get(fact_id)
403
+ if not fact:
404
+ return finalize("c3_memory", {"action": action},
405
+ f"[memory:ground] fact {fact_id} not found", "0")
406
+ gr = grounder.ground_fact(fact)
407
+ result = gr.to_dict()
408
+ lines = [f"[memory:ground] fact={fact_id} grounded={result['grounded']}"]
409
+ for fr in result["file_refs"]:
410
+ lines.append(f" file: {fr['path']} exists={fr['exists']}")
411
+ for sr in result["symbol_refs"]:
412
+ lines.append(f" symbol: {sr['name']} found={sr['found']} file={sr.get('file', '')}")
413
+ for issue in result["issues"]:
414
+ lines.append(f" issue: {issue}")
415
+ lines.append(f" confidence_delta: {result['confidence_delta']}")
416
+ else:
417
+ result = grounder.ground_all()
418
+ lines = [
419
+ f"[memory:ground] {result.get('total', 0)} facts checked",
420
+ f" Grounded: {result.get('grounded', 0)}",
421
+ f" Ungrounded: {result.get('ungrounded', 0)}",
422
+ f" Confidence updates: {result.get('confidence_updates', 0)}",
423
+ ]
424
+ for detail in result.get("ungrounded_details", [])[:5]:
425
+ lines.append(f" {detail['fact_id']}: {', '.join(detail.get('issues', []))}")
426
+ return finalize("c3_memory", {"action": action},
427
+ "\n".join(lines), f"g:{result.get('grounded', 0)}/{result.get('total', 0)}")
428
+
429
+ if action == "trends":
430
+ consolidator = getattr(svc, "memory_consolidator", None)
431
+ if not consolidator:
432
+ return finalize("c3_memory", {"action": action},
433
+ "[memory:trends] consolidator not available", "skip")
434
+ trends = consolidator.detect_trends()
435
+ lines = [f"[memory:trends] {trends['sessions_analyzed']} sessions analyzed"]
436
+ if trends["hot_files"]:
437
+ lines.append(" Hot files (active development):")
438
+ for hf in trends["hot_files"]:
439
+ lines.append(f" {hf['file']} — {hf['sessions']} sessions")
440
+ if trends["hot_facts"]:
441
+ lines.append(" Hot facts (frequently recalled):")
442
+ for hf in trends["hot_facts"]:
443
+ lines.append(f" {hf['fact_id']} ({hf['sessions']}x) — {hf.get('fact', '?')}")
444
+ if not trends["hot_files"] and not trends["hot_facts"]:
445
+ lines.append(" No significant trends detected yet.")
446
+ return finalize("c3_memory", {"action": action},
447
+ "\n".join(lines), f"t:{trends['sessions_analyzed']}")
448
+
449
+ if action == "lifespan":
450
+ consolidator = getattr(svc, "memory_consolidator", None)
451
+ if not consolidator:
452
+ return finalize("c3_memory", {"action": action},
453
+ "[memory:lifespan] consolidator not available", "skip")
454
+ analysis = consolidator.fact_lifespan_analysis()
455
+ lines = [f"[memory:lifespan] {analysis['total_facts']} facts analyzed"]
456
+ if analysis["foundational"]:
457
+ lines.append(f" Foundational ({len(analysis['foundational'])} — recalled across 3+ sessions):")
458
+ for f in analysis["foundational"][:5]:
459
+ lines.append(f" {f['id']} spread={f['session_spread']} sal={f['salience']:.2f} — {f['fact']}")
460
+ if analysis["contextual"]:
461
+ lines.append(f" Contextual ({len(analysis['contextual'])} — single-session, 7+ days old):")
462
+ for f in analysis["contextual"][:5]:
463
+ lines.append(f" {f['id']} sal={f['salience']:.2f} — {f['fact']}")
464
+ if not analysis["foundational"] and not analysis["contextual"]:
465
+ lines.append(" Not enough session history for classification yet.")
466
+ return finalize("c3_memory", {"action": action},
467
+ "\n".join(lines), f"f:{len(analysis['foundational'])}c:{len(analysis['contextual'])}")
468
+
469
+ return f"[memory:error] Unknown action: {action}"