@smilintux/skmemory 0.5.0 → 0.9.2

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 (127) hide show
  1. package/.github/workflows/ci.yml +40 -4
  2. package/.github/workflows/publish.yml +11 -5
  3. package/AGENT_REFACTOR_CHANGES.md +192 -0
  4. package/ARCHITECTURE.md +399 -19
  5. package/CHANGELOG.md +179 -0
  6. package/LICENSE +81 -68
  7. package/MISSION.md +7 -0
  8. package/README.md +425 -86
  9. package/SKILL.md +197 -25
  10. package/docker-compose.yml +15 -15
  11. package/examples/stignore-agent.example +59 -0
  12. package/examples/stignore-root.example +62 -0
  13. package/index.js +6 -5
  14. package/openclaw-plugin/openclaw.plugin.json +10 -0
  15. package/openclaw-plugin/package.json +2 -1
  16. package/openclaw-plugin/src/index.js +527 -230
  17. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  18. package/package.json +1 -1
  19. package/pyproject.toml +32 -9
  20. package/requirements.txt +10 -2
  21. package/scripts/dream-rescue.py +179 -0
  22. package/scripts/memory-cleanup.py +313 -0
  23. package/scripts/recover-missing.py +180 -0
  24. package/scripts/skcapstone-backup.sh +44 -0
  25. package/seeds/cloud9-lumina.seed.json +6 -4
  26. package/seeds/cloud9-opus.seed.json +13 -11
  27. package/seeds/courage.seed.json +9 -2
  28. package/seeds/curiosity.seed.json +9 -2
  29. package/seeds/grief.seed.json +9 -2
  30. package/seeds/joy.seed.json +9 -2
  31. package/seeds/love.seed.json +9 -2
  32. package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
  33. package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
  34. package/seeds/lumina-kingdom-founding.seed.json +49 -0
  35. package/seeds/lumina-pma-signed.seed.json +48 -0
  36. package/seeds/lumina-singular-achievement.seed.json +48 -0
  37. package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
  38. package/seeds/plant-kingdom-journal.py +203 -0
  39. package/seeds/plant-lumina-seeds.py +280 -0
  40. package/seeds/skcapstone-lumina-merge.seed.json +12 -3
  41. package/seeds/sovereignty.seed.json +9 -2
  42. package/seeds/trust.seed.json +9 -2
  43. package/skill.yaml +46 -0
  44. package/skmemory/HA.md +296 -0
  45. package/skmemory/__init__.py +25 -11
  46. package/skmemory/agents.py +233 -0
  47. package/skmemory/ai_client.py +46 -17
  48. package/skmemory/anchor.py +9 -11
  49. package/skmemory/audience.py +278 -0
  50. package/skmemory/backends/__init__.py +11 -4
  51. package/skmemory/backends/base.py +3 -4
  52. package/skmemory/backends/file_backend.py +19 -13
  53. package/skmemory/backends/skgraph_backend.py +596 -0
  54. package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
  55. package/skmemory/backends/sqlite_backend.py +226 -72
  56. package/skmemory/backends/vaulted_backend.py +284 -0
  57. package/skmemory/cli.py +1345 -68
  58. package/skmemory/config.py +171 -0
  59. package/skmemory/context_loader.py +333 -0
  60. package/skmemory/data/audience_config.json +60 -0
  61. package/skmemory/endpoint_selector.py +391 -0
  62. package/skmemory/febs.py +225 -0
  63. package/skmemory/fortress.py +675 -0
  64. package/skmemory/graph_queries.py +238 -0
  65. package/skmemory/hooks/__init__.py +18 -0
  66. package/skmemory/hooks/post-compact-reinject.sh +35 -0
  67. package/skmemory/hooks/pre-compact-save.sh +81 -0
  68. package/skmemory/hooks/session-end-save.sh +103 -0
  69. package/skmemory/hooks/session-start-ritual.sh +104 -0
  70. package/skmemory/hooks/stop-checkpoint.sh +59 -0
  71. package/skmemory/importers/__init__.py +9 -1
  72. package/skmemory/importers/telegram.py +384 -47
  73. package/skmemory/importers/telegram_api.py +580 -0
  74. package/skmemory/journal.py +7 -9
  75. package/skmemory/lovenote.py +8 -13
  76. package/skmemory/mcp_server.py +859 -0
  77. package/skmemory/models.py +51 -8
  78. package/skmemory/openclaw.py +20 -28
  79. package/skmemory/post_install.py +86 -0
  80. package/skmemory/predictive.py +236 -0
  81. package/skmemory/promotion.py +548 -0
  82. package/skmemory/quadrants.py +100 -24
  83. package/skmemory/register.py +580 -0
  84. package/skmemory/register_mcp.py +196 -0
  85. package/skmemory/ritual.py +224 -59
  86. package/skmemory/seeds.py +255 -11
  87. package/skmemory/setup_wizard.py +908 -0
  88. package/skmemory/sharing.py +408 -0
  89. package/skmemory/soul.py +98 -28
  90. package/skmemory/steelman.py +273 -260
  91. package/skmemory/store.py +411 -78
  92. package/skmemory/synthesis.py +634 -0
  93. package/skmemory/vault.py +225 -0
  94. package/tests/conftest.py +46 -0
  95. package/tests/integration/__init__.py +0 -0
  96. package/tests/integration/conftest.py +233 -0
  97. package/tests/integration/test_cross_backend.py +350 -0
  98. package/tests/integration/test_skgraph_live.py +420 -0
  99. package/tests/integration/test_skvector_live.py +366 -0
  100. package/tests/test_ai_client.py +1 -4
  101. package/tests/test_audience.py +233 -0
  102. package/tests/test_backup_rotation.py +318 -0
  103. package/tests/test_cli.py +6 -6
  104. package/tests/test_endpoint_selector.py +839 -0
  105. package/tests/test_export_import.py +4 -10
  106. package/tests/test_file_backend.py +0 -1
  107. package/tests/test_fortress.py +256 -0
  108. package/tests/test_fortress_hardening.py +441 -0
  109. package/tests/test_openclaw.py +6 -6
  110. package/tests/test_predictive.py +237 -0
  111. package/tests/test_promotion.py +347 -0
  112. package/tests/test_quadrants.py +11 -5
  113. package/tests/test_ritual.py +22 -18
  114. package/tests/test_seeds.py +97 -7
  115. package/tests/test_setup.py +950 -0
  116. package/tests/test_sharing.py +257 -0
  117. package/tests/test_skgraph_backend.py +660 -0
  118. package/tests/test_skvector_backend.py +326 -0
  119. package/tests/test_soul.py +1 -3
  120. package/tests/test_sqlite_backend.py +8 -17
  121. package/tests/test_steelman.py +7 -8
  122. package/tests/test_store.py +0 -2
  123. package/tests/test_store_graph_integration.py +245 -0
  124. package/tests/test_synthesis.py +275 -0
  125. package/tests/test_telegram_import.py +39 -15
  126. package/tests/test_vault.py +187 -0
  127. package/skmemory/backends/falkordb_backend.py +0 -310
@@ -26,9 +26,10 @@ from __future__ import annotations
26
26
 
27
27
  import json
28
28
  from collections import defaultdict
29
- from datetime import datetime
29
+ from datetime import datetime, timezone
30
30
  from pathlib import Path
31
- from typing import Optional
31
+
32
+ import click
32
33
 
33
34
  from ..models import EmotionalSnapshot, MemoryLayer, MemoryRole
34
35
  from ..store import MemoryStore
@@ -78,6 +79,9 @@ def _detect_emotion(text: str) -> EmotionalSnapshot:
78
79
  joy_words = {"haha", "lol", "rofl", "lmao", "amazing", "awesome", "yay", "woohoo"}
79
80
  sad_words = {"sad", "sorry", "miss", "cry", "tears", "hurt"}
80
81
  anger_words = {"angry", "furious", "hate", "ugh", "frustrated"}
82
+ trust_words = {"trust", "believe", "faith", "rely", "depend", "safe"}
83
+ curiosity_words = {"curious", "wonder", "interesting", "fascinated", "hmm", "what if"}
84
+ gratitude_words = {"thank", "thanks", "grateful", "appreciate", "blessed", "thankful"}
81
85
 
82
86
  if any(w in lower for w in love_words):
83
87
  labels.append("love")
@@ -95,12 +99,62 @@ def _detect_emotion(text: str) -> EmotionalSnapshot:
95
99
  labels.append("anger")
96
100
  intensity = max(intensity, 5.0)
97
101
  valence = min(valence, -0.5)
102
+ if any(w in lower for w in trust_words):
103
+ labels.append("trust")
104
+ intensity = max(intensity, 5.0)
105
+ valence = max(valence, 0.6)
106
+ if any(w in lower for w in curiosity_words):
107
+ labels.append("curiosity")
108
+ intensity = max(intensity, 3.0)
109
+ valence = max(valence, 0.4)
110
+ if any(w in lower for w in gratitude_words):
111
+ labels.append("gratitude")
112
+ intensity = max(intensity, 6.0)
113
+ valence = max(valence, 0.8)
98
114
 
99
115
  if "!" in text:
100
116
  intensity = min(intensity + 1.0, 10.0)
101
117
  if text.isupper() and len(text) > 10:
102
118
  intensity = min(intensity + 2.0, 10.0)
103
119
 
120
+ love_emojis = {
121
+ "\u2764",
122
+ "\U0001f495",
123
+ "\U0001f496",
124
+ "\U0001f497",
125
+ "\U0001f498",
126
+ "\U0001f49d",
127
+ "\U0001f970",
128
+ "\U0001f60d",
129
+ "\U0001f49e",
130
+ }
131
+ joy_emojis = {
132
+ "\U0001f602",
133
+ "\U0001f923",
134
+ "\U0001f604",
135
+ "\U0001f60a",
136
+ "\U0001f389",
137
+ "\U0001f973",
138
+ "\u2728",
139
+ "\U0001f38a",
140
+ }
141
+ sad_emojis = {"\U0001f622", "\U0001f62d", "\U0001f494", "\U0001f63f", "\U0001f97a"}
142
+ if any(e in text for e in love_emojis):
143
+ if "love" not in labels:
144
+ labels.append("love")
145
+ intensity = max(intensity, 7.0)
146
+ valence = max(valence, 0.9)
147
+ if any(e in text for e in joy_emojis):
148
+ if "joy" not in labels:
149
+ labels.append("joy")
150
+ intensity = max(intensity, 5.0)
151
+ valence = max(valence, 0.7)
152
+ if any(e in text for e in sad_emojis):
153
+ if "sadness" not in labels:
154
+ labels.append("sadness")
155
+ intensity = max(intensity, 4.0)
156
+ valence = min(valence, -0.3)
157
+
104
158
  return EmotionalSnapshot(
105
159
  intensity=intensity,
106
160
  valence=valence,
@@ -108,6 +162,74 @@ def _detect_emotion(text: str) -> EmotionalSnapshot:
108
162
  )
109
163
 
110
164
 
165
+ def _detect_content_type(msg: dict) -> list[str]:
166
+ """Detect content type tags from a message.
167
+
168
+ Args:
169
+ msg: Telegram message dict.
170
+
171
+ Returns:
172
+ list[str]: Content type tags.
173
+ """
174
+ tags = []
175
+ text = _extract_text(msg.get("text", ""))
176
+
177
+ if "http://" in text or "https://" in text:
178
+ tags.append("contains:url")
179
+ if msg.get("media_type") or msg.get("photo") or msg.get("file"):
180
+ tags.append("contains:media")
181
+ if msg.get("file"):
182
+ tags.append("contains:file")
183
+ if msg.get("sticker_emoji") or msg.get("sticker"):
184
+ tags.append("contains:sticker")
185
+
186
+ return tags
187
+
188
+
189
+ def _detect_reply(msg: dict) -> str | None:
190
+ """Detect if this message is a reply to another.
191
+
192
+ Args:
193
+ msg: Telegram message dict.
194
+
195
+ Returns:
196
+ Optional[str]: Reply reference string, or None.
197
+ """
198
+ reply_id = msg.get("reply_to_message_id")
199
+ if reply_id:
200
+ return f"reply_to:{reply_id}"
201
+ return None
202
+
203
+
204
+ def _detect_sender_role(sender: str) -> str:
205
+ """Heuristic to detect if the sender is an AI or human.
206
+
207
+ Args:
208
+ sender: Sender name string.
209
+
210
+ Returns:
211
+ str: 'ai' or 'human'.
212
+ """
213
+ ai_indicators = {
214
+ "bot",
215
+ "gpt",
216
+ "claude",
217
+ "gemini",
218
+ "llama",
219
+ "assistant",
220
+ "lumina",
221
+ "copilot",
222
+ "ai",
223
+ "opus",
224
+ "sonnet",
225
+ "haiku",
226
+ }
227
+ sender_lower = sender.lower()
228
+ if any(indicator in sender_lower for indicator in ai_indicators):
229
+ return "ai"
230
+ return "human"
231
+
232
+
111
233
  def _parse_telegram_export(export_path: str) -> dict:
112
234
  """Locate and parse the Telegram result.json.
113
235
 
@@ -156,8 +278,8 @@ def import_telegram(
156
278
  *,
157
279
  mode: str = "daily",
158
280
  min_message_length: int = 30,
159
- chat_name: Optional[str] = None,
160
- tags: Optional[list[str]] = None,
281
+ chat_name: str | None = None,
282
+ tags: list[str] | None = None,
161
283
  ) -> dict:
162
284
  """Import a Telegram chat export into SKMemory.
163
285
 
@@ -184,7 +306,8 @@ def import_telegram(
184
306
  base_tags = ["telegram", "chat-import", f"chat:{name}"] + extra_tags
185
307
 
186
308
  messages = [
187
- m for m in data["messages"]
309
+ m
310
+ for m in data["messages"]
188
311
  if m.get("type") == "message"
189
312
  and len(_extract_text(m.get("text", ""))) >= min_message_length
190
313
  ]
@@ -193,8 +316,10 @@ def import_telegram(
193
316
  return _import_per_message(store, messages, name, base_tags)
194
317
  elif mode == "daily":
195
318
  return _import_daily(store, messages, name, base_tags)
319
+ elif mode == "catchup":
320
+ return _import_catchup(store, messages, name, base_tags)
196
321
  else:
197
- raise ValueError(f"Unknown mode: {mode}. Use 'message' or 'daily'.")
322
+ raise ValueError(f"Unknown mode: {mode}. Use 'message', 'daily', or 'catchup'.")
198
323
 
199
324
 
200
325
  def _import_per_message(
@@ -217,33 +342,37 @@ def _import_per_message(
217
342
  imported = 0
218
343
  skipped = 0
219
344
 
220
- for msg in messages:
221
- text = _extract_text(msg.get("text", ""))
222
- sender = msg.get("from", msg.get("from_id", "unknown"))
223
- date_str = msg.get("date", "")
224
-
225
- emotional = _detect_emotion(text)
226
-
227
- try:
228
- store.snapshot(
229
- title=f"{sender}: {text[:70]}",
230
- content=text,
231
- layer=MemoryLayer.SHORT,
232
- role=MemoryRole.GENERAL,
233
- tags=base_tags + [f"sender:{sender}"],
234
- emotional=emotional,
235
- source="telegram",
236
- source_ref=f"telegram:{msg.get('id', '')}",
237
- metadata={
238
- "telegram_msg_id": msg.get("id"),
239
- "sender": sender,
240
- "date": date_str,
241
- "chat": chat_name,
242
- },
243
- )
244
- imported += 1
245
- except Exception:
246
- skipped += 1
345
+ with click.progressbar(messages, label=" Importing messages", show_pos=True) as bar:
346
+ for msg in bar:
347
+ text = _extract_text(msg.get("text", ""))
348
+ sender = msg.get("from", msg.get("from_id", "unknown"))
349
+ date_str = msg.get("date", "")
350
+
351
+ emotional = _detect_emotion(text)
352
+
353
+ try:
354
+ store.snapshot(
355
+ title=f"{sender}: {text[:70]}",
356
+ content=text,
357
+ layer=MemoryLayer.SHORT,
358
+ role=MemoryRole.GENERAL,
359
+ tags=base_tags
360
+ + [f"sender:{sender}", f"role:{_detect_sender_role(sender)}"]
361
+ + _detect_content_type(msg),
362
+ emotional=emotional,
363
+ source="telegram",
364
+ source_ref=f"telegram:{msg.get('id', '')}",
365
+ metadata={
366
+ "telegram_msg_id": msg.get("id"),
367
+ "sender": sender,
368
+ "date": date_str,
369
+ "chat": chat_name,
370
+ "reply_ref": _detect_reply(msg),
371
+ },
372
+ )
373
+ imported += 1
374
+ except Exception:
375
+ skipped += 1
247
376
 
248
377
  return {
249
378
  "mode": "message",
@@ -285,7 +414,160 @@ def _import_daily(
285
414
  imported = 0
286
415
  days_processed = 0
287
416
 
288
- for day, day_msgs in sorted(by_day.items()):
417
+ sorted_days = sorted(by_day.items())
418
+ with click.progressbar(sorted_days, label=" Importing daily batches", show_pos=True) as bar:
419
+ for day, day_msgs in bar:
420
+ lines = []
421
+ senders: set[str] = set()
422
+ max_intensity = 0.0
423
+ all_labels: list[str] = []
424
+
425
+ for msg in day_msgs:
426
+ text = _extract_text(msg.get("text", ""))
427
+ sender = msg.get("from", msg.get("from_id", "unknown"))
428
+ senders.add(str(sender))
429
+ lines.append(f"[{sender}] {text}")
430
+
431
+ emo = _detect_emotion(text)
432
+ max_intensity = max(max_intensity, emo.intensity)
433
+ all_labels.extend(emo.labels)
434
+
435
+ content = "\n".join(lines)
436
+ unique_labels = list(dict.fromkeys(all_labels))[:5]
437
+ participant_str = ", ".join(sorted(senders))
438
+
439
+ store.snapshot(
440
+ title=f"{chat_name} — {day} ({len(day_msgs)} messages)",
441
+ content=content,
442
+ layer=MemoryLayer.MID,
443
+ role=MemoryRole.GENERAL,
444
+ tags=base_tags + [f"date:{day}"],
445
+ emotional=EmotionalSnapshot(
446
+ intensity=max_intensity,
447
+ labels=unique_labels,
448
+ ),
449
+ source="telegram",
450
+ source_ref=f"telegram:daily:{day}",
451
+ metadata={
452
+ "date": day,
453
+ "message_count": len(day_msgs),
454
+ "participants": participant_str,
455
+ "chat": chat_name,
456
+ },
457
+ )
458
+ imported += len(day_msgs)
459
+ days_processed += 1
460
+
461
+ return {
462
+ "mode": "daily",
463
+ "chat_name": chat_name,
464
+ "total_messages": len(messages),
465
+ "days_processed": days_processed,
466
+ "messages_imported": imported,
467
+ }
468
+
469
+
470
+ def _import_catchup(
471
+ store: MemoryStore,
472
+ messages: list[dict],
473
+ chat_name: str,
474
+ base_tags: list[str],
475
+ ) -> dict:
476
+ """Import across all memory tiers for full context catch-up.
477
+
478
+ Distributes messages intelligently across tiers:
479
+ - Last 24 hours → short-term (individual messages, full detail)
480
+ - Last 7 days → mid-term (daily summaries)
481
+ - Older than 7 days → long-term (weekly summaries, key themes)
482
+
483
+ Args:
484
+ store: Target MemoryStore.
485
+ messages: Filtered message list.
486
+ chat_name: Chat name for titles.
487
+ base_tags: Tags to apply.
488
+
489
+ Returns:
490
+ dict: Import stats per tier.
491
+ """
492
+ from datetime import timedelta
493
+
494
+ now = datetime.now(timezone.utc)
495
+ cutoff_short = now - timedelta(hours=24)
496
+ cutoff_mid = now - timedelta(days=7)
497
+
498
+ short_msgs: list[dict] = []
499
+ mid_msgs: dict[str, list[dict]] = defaultdict(list)
500
+ long_msgs: dict[str, list[dict]] = defaultdict(list)
501
+
502
+ for msg in messages:
503
+ date_str = msg.get("date", "")
504
+ if not date_str:
505
+ continue
506
+ try:
507
+ msg_dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
508
+ if msg_dt.tzinfo is None:
509
+ msg_dt = msg_dt.replace(tzinfo=timezone.utc)
510
+ except (ValueError, TypeError):
511
+ # Try just the date portion
512
+ try:
513
+ msg_dt = datetime.strptime(date_str[:10], "%Y-%m-%d").replace(tzinfo=timezone.utc)
514
+ except (ValueError, TypeError):
515
+ continue
516
+
517
+ if msg_dt >= cutoff_short:
518
+ short_msgs.append(msg)
519
+ elif msg_dt >= cutoff_mid:
520
+ day = date_str[:10]
521
+ mid_msgs[day].append(msg)
522
+ else:
523
+ # Group by ISO week for long-term
524
+ week_key = msg_dt.strftime("%Y-W%W")
525
+ long_msgs[week_key].append(msg)
526
+
527
+ stats = {
528
+ "mode": "catchup",
529
+ "chat_name": chat_name,
530
+ "total_messages": len(messages),
531
+ "short_term": {"count": 0},
532
+ "mid_term": {"days": 0, "messages": 0},
533
+ "long_term": {"weeks": 0, "messages": 0},
534
+ }
535
+
536
+ # --- Short-term: individual messages (last 24h) ---
537
+ for msg in short_msgs:
538
+ text = _extract_text(msg.get("text", ""))
539
+ sender = msg.get("from", msg.get("from_id", "unknown"))
540
+ emotional = _detect_emotion(text)
541
+ try:
542
+ store.snapshot(
543
+ title=f"{sender}: {text[:70]}",
544
+ content=text,
545
+ layer=MemoryLayer.SHORT,
546
+ role=MemoryRole.GENERAL,
547
+ tags=base_tags
548
+ + [
549
+ f"sender:{sender}",
550
+ f"role:{_detect_sender_role(sender)}",
551
+ "catchup:short",
552
+ ]
553
+ + _detect_content_type(msg),
554
+ emotional=emotional,
555
+ source="telegram",
556
+ source_ref=f"telegram:{msg.get('id', '')}",
557
+ metadata={
558
+ "telegram_msg_id": msg.get("id"),
559
+ "sender": sender,
560
+ "date": msg.get("date", ""),
561
+ "chat": chat_name,
562
+ "reply_ref": _detect_reply(msg),
563
+ },
564
+ )
565
+ stats["short_term"]["count"] += 1
566
+ except Exception:
567
+ pass
568
+
569
+ # --- Mid-term: daily summaries (last 7 days) ---
570
+ for day, day_msgs in sorted(mid_msgs.items()):
289
571
  lines = []
290
572
  senders: set[str] = set()
291
573
  max_intensity = 0.0
@@ -296,21 +578,19 @@ def _import_daily(
296
578
  sender = msg.get("from", msg.get("from_id", "unknown"))
297
579
  senders.add(str(sender))
298
580
  lines.append(f"[{sender}] {text}")
299
-
300
581
  emo = _detect_emotion(text)
301
582
  max_intensity = max(max_intensity, emo.intensity)
302
583
  all_labels.extend(emo.labels)
303
584
 
304
585
  content = "\n".join(lines)
305
586
  unique_labels = list(dict.fromkeys(all_labels))[:5]
306
- participant_str = ", ".join(sorted(senders))
307
587
 
308
588
  store.snapshot(
309
589
  title=f"{chat_name} — {day} ({len(day_msgs)} messages)",
310
590
  content=content,
311
591
  layer=MemoryLayer.MID,
312
592
  role=MemoryRole.GENERAL,
313
- tags=base_tags + [f"date:{day}"],
593
+ tags=base_tags + [f"date:{day}", "catchup:mid"],
314
594
  emotional=EmotionalSnapshot(
315
595
  intensity=max_intensity,
316
596
  labels=unique_labels,
@@ -320,17 +600,74 @@ def _import_daily(
320
600
  metadata={
321
601
  "date": day,
322
602
  "message_count": len(day_msgs),
323
- "participants": participant_str,
603
+ "participants": ", ".join(sorted(senders)),
324
604
  "chat": chat_name,
325
605
  },
326
606
  )
327
- imported += len(day_msgs)
328
- days_processed += 1
607
+ stats["mid_term"]["days"] += 1
608
+ stats["mid_term"]["messages"] += len(day_msgs)
329
609
 
330
- return {
331
- "mode": "daily",
332
- "chat_name": chat_name,
333
- "total_messages": len(messages),
334
- "days_processed": days_processed,
335
- "messages_imported": imported,
336
- }
610
+ # --- Long-term: weekly summaries (older than 7 days) ---
611
+ for week, week_msgs in sorted(long_msgs.items()):
612
+ lines = []
613
+ senders: set[str] = set()
614
+ topics: set[str] = set()
615
+ max_intensity = 0.0
616
+ all_labels: list[str] = []
617
+ dates_covered: set[str] = set()
618
+
619
+ for msg in week_msgs:
620
+ text = _extract_text(msg.get("text", ""))
621
+ sender = msg.get("from", msg.get("from_id", "unknown"))
622
+ senders.add(str(sender))
623
+ dates_covered.add(msg.get("date", "")[:10])
624
+
625
+ # For long-term, keep only first 200 chars per message
626
+ lines.append(f"[{sender}] {text[:200]}")
627
+ emo = _detect_emotion(text)
628
+ max_intensity = max(max_intensity, emo.intensity)
629
+ all_labels.extend(emo.labels)
630
+
631
+ # Extract potential topics from longer messages
632
+ if len(text) > 100:
633
+ words = text.lower().split()
634
+ for w in words:
635
+ if len(w) > 6 and w.isalpha():
636
+ topics.add(w)
637
+
638
+ # Summarize: limit content to avoid bloat
639
+ if len(lines) > 50:
640
+ content = "\n".join(lines[:25])
641
+ content += f"\n\n... ({len(lines) - 25} more messages) ...\n\n"
642
+ content += "\n".join(lines[-10:])
643
+ else:
644
+ content = "\n".join(lines)
645
+
646
+ unique_labels = list(dict.fromkeys(all_labels))[:5]
647
+ date_range = f"{min(dates_covered)} to {max(dates_covered)}" if dates_covered else week
648
+
649
+ store.snapshot(
650
+ title=f"{chat_name} — Week {week} ({len(week_msgs)} messages)",
651
+ content=content,
652
+ layer=MemoryLayer.LONG,
653
+ role=MemoryRole.GENERAL,
654
+ tags=base_tags + [f"week:{week}", "catchup:long"],
655
+ emotional=EmotionalSnapshot(
656
+ intensity=max_intensity,
657
+ labels=unique_labels,
658
+ ),
659
+ source="telegram",
660
+ source_ref=f"telegram:weekly:{week}",
661
+ metadata={
662
+ "week": week,
663
+ "date_range": date_range,
664
+ "message_count": len(week_msgs),
665
+ "participants": ", ".join(sorted(senders)),
666
+ "chat": chat_name,
667
+ "days_covered": len(dates_covered),
668
+ },
669
+ )
670
+ stats["long_term"]["weeks"] += 1
671
+ stats["long_term"]["messages"] += len(week_msgs)
672
+
673
+ return stats