claude-smart 0.2.48 → 0.2.50

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 (193) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +11 -40
  3. package/bin/claude-smart.js +393 -55
  4. package/package.json +3 -2
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/.coverage +0 -0
  8. package/plugin/README.md +4 -3
  9. package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
  10. package/plugin/dashboard/app/dashboard/page.tsx +6 -1
  11. package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
  12. package/plugin/dashboard/app/preferences/page.tsx +32 -35
  13. package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
  14. package/plugin/dashboard/app/sessions/page.tsx +2 -0
  15. package/plugin/dashboard/app/skills/page.tsx +65 -50
  16. package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
  17. package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
  18. package/plugin/dashboard/components/common/host-badge.tsx +118 -0
  19. package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
  20. package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
  21. package/plugin/dashboard/components/common/page-header.tsx +3 -3
  22. package/plugin/dashboard/lib/config-file.ts +5 -1
  23. package/plugin/dashboard/lib/host-attribution.ts +62 -0
  24. package/plugin/dashboard/lib/session-reader.ts +40 -2
  25. package/plugin/dashboard/lib/types.ts +7 -1
  26. package/plugin/opencode/dist/server.mjs +60 -2
  27. package/plugin/opencode/server.mts +64 -2
  28. package/plugin/pyproject.toml +2 -2
  29. package/plugin/scripts/_lib.sh +255 -7
  30. package/plugin/scripts/backend-python-runner.py +46 -0
  31. package/plugin/scripts/backend-service.sh +766 -123
  32. package/plugin/scripts/codex-hook.js +79 -229
  33. package/plugin/scripts/dashboard-open.sh +6 -4
  34. package/plugin/scripts/dashboard-service.sh +118 -137
  35. package/plugin/scripts/ensure-plugin-root.sh +106 -8
  36. package/plugin/scripts/hook_entry.sh +3 -0
  37. package/plugin/scripts/opencode-claude-compat.js +8 -1
  38. package/plugin/scripts/smart-install.sh +116 -21
  39. package/plugin/src/README.md +1 -1
  40. package/plugin/src/claude_smart/cli.py +93 -29
  41. package/plugin/src/claude_smart/context_inject.py +3 -0
  42. package/plugin/src/claude_smart/env_config.py +56 -11
  43. package/plugin/src/claude_smart/events/post_tool.py +2 -1
  44. package/plugin/src/claude_smart/events/session_end.py +2 -1
  45. package/plugin/src/claude_smart/events/stop.py +3 -0
  46. package/plugin/src/claude_smart/events/user_prompt.py +2 -1
  47. package/plugin/src/claude_smart/internal_call.py +5 -2
  48. package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
  49. package/plugin/src/claude_smart/publish.py +59 -7
  50. package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
  51. package/plugin/src/claude_smart/runtime.py +15 -6
  52. package/plugin/src/claude_smart/state.py +211 -52
  53. package/plugin/uv.lock +5 -5
  54. package/plugin/vendor/reflexio/.env.example +13 -0
  55. package/plugin/vendor/reflexio/reflexio/README.md +7 -3
  56. package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
  57. package/plugin/vendor/reflexio/reflexio/cli/README.md +3 -0
  58. package/plugin/vendor/reflexio/reflexio/client/client.py +166 -9
  59. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
  60. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
  61. package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
  62. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +195 -25
  63. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
  64. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
  65. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
  66. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
  67. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
  68. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  69. package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
  70. package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
  71. package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
  72. package/plugin/vendor/reflexio/reflexio/server/api.py +79 -5
  73. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
  74. package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
  75. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
  76. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
  77. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +63 -6
  78. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
  79. package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +1 -0
  80. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
  81. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
  82. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
  83. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
  84. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +199 -2
  85. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
  86. package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
  87. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
  88. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
  89. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
  90. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
  91. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
  92. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
  93. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
  94. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
  95. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
  96. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
  97. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
  98. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
  99. package/plugin/vendor/reflexio/reflexio/server/routes/interactions.py +63 -2
  100. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
  101. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
  102. package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
  103. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
  104. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
  105. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
  106. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
  107. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
  108. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
  109. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
  110. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
  111. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
  112. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
  113. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
  114. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
  115. package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
  116. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/__init__.py +13 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/scheduler.py +142 -0
  118. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +262 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
  120. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
  121. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
  122. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
  123. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
  124. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
  125. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
  126. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_scheduler.py +1 -3
  127. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
  128. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +640 -80
  129. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
  130. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
  131. package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
  132. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
  133. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
  134. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
  135. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
  136. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
  137. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
  138. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
  139. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
  140. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
  141. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
  142. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
  143. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
  144. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +166 -66
  145. package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
  146. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
  147. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
  148. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
  149. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
  150. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +2 -0
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +190 -2
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +414 -0
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +11 -5
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +8 -3
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_fts_vec.py +86 -2
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +59 -7
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
  169. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +8 -3
  170. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +57 -12
  171. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +197 -12
  172. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +70 -11
  173. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +17 -0
  174. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_commit_scope.py +9 -0
  175. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
  176. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +269 -0
  177. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
  178. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
  179. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
  180. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
  181. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
  182. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
  183. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
  184. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
  185. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +82 -1
  186. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +74 -2
  187. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
  188. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
  189. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
  190. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
  191. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
  192. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
  193. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
@@ -7,8 +7,13 @@ Each Claude Code session gets one file at
7
7
  - ``{"role": "Assistant", ...}`` — a finalized assistant turn
8
8
  - ``{"role": "Assistant_tool", ...}`` — a single tool invocation, attached
9
9
  to the next assistant turn at ``Stop`` time
10
- - ``{"published_up_to": N}`` — high-water mark so Stop / SessionEnd don't
11
- re-publish rows already sent to reflexio
10
+ - ``{"published_up_to": N, "request_id": "..."}`` — high-water mark so Stop /
11
+ SessionEnd don't re-publish rows already sent to reflexio; a confirmed
12
+ request ID preserves local learning lineage for the dashboard when available
13
+ - ``{"retrieved_learning_refs": [...]}`` — identity pairs shown to the agent,
14
+ attached by event order to the next Assistant turn
15
+ - ``{"publish_attempt": {"start": N, "end": M}}`` — frozen retry boundary for
16
+ one in-flight publish
12
17
 
13
18
  The buffer exists for offline resilience: when reflexio is unreachable,
14
19
  Stop appends without publishing and the next successful hook drains.
@@ -19,8 +24,9 @@ from __future__ import annotations
19
24
  import json
20
25
  import logging
21
26
  import os
27
+ from collections.abc import Iterable
22
28
  from pathlib import Path
23
- from typing import Any, Iterable
29
+ from typing import Any
24
30
 
25
31
  try:
26
32
  import fcntl # POSIX only — Windows hooks fall back to append-without-lock.
@@ -34,7 +40,12 @@ _DEFAULT_STATE_DIR = Path.home() / ".claude-smart" / "sessions"
34
40
 
35
41
  _TOOL_DATA_FIELD_MAX_LEN = 256
36
42
 
37
- _VALID_CITATION_KINDS = frozenset({"playbook", "profile"})
43
+ _VALID_CITATION_KINDS = frozenset(
44
+ {"playbook", "profile", "user_playbook", "agent_playbook"}
45
+ )
46
+ _VALID_RETRIEVED_PLAYBOOK_KINDS = frozenset({"user_playbook", "agent_playbook"})
47
+ _VALID_RETRIEVED_KINDS = _VALID_RETRIEVED_PLAYBOOK_KINDS | {"profile"}
48
+ _RETRIEVED_LEARNINGS_PUBLISH_CAP = 1000
38
49
 
39
50
 
40
51
  def _truncate_tool_data_field(value: Any) -> Any:
@@ -81,17 +92,37 @@ def injected_path(session_id: str) -> Path:
81
92
  return state_dir() / f"{session_id}.injected.jsonl"
82
93
 
83
94
 
95
+ def _normalize_registry_ref(entry: dict[str, Any]) -> dict[str, str] | None:
96
+ """Return the identity pair carried by one citation-registry entry."""
97
+
98
+ learning_id = entry.get("real_id")
99
+ kind = entry.get("kind")
100
+ if not isinstance(learning_id, str) or not learning_id:
101
+ return None
102
+ if kind == "profile":
103
+ wire_kind = "profile"
104
+ elif (
105
+ kind == "playbook"
106
+ and entry.get("source_kind") in _VALID_RETRIEVED_PLAYBOOK_KINDS
107
+ ):
108
+ wire_kind = entry["source_kind"]
109
+ else:
110
+ return None
111
+ return {"kind": wire_kind, "learning_id": learning_id}
112
+
113
+
84
114
  def append_injected(session_id: str, entries: Iterable[dict[str, Any]]) -> None:
85
- """Append citation-registry entries to the per-session injected-items file.
115
+ """Record injected items for citations and the next Assistant publish.
86
116
 
87
- Each entry maps a short ``id`` (4-hex-char) back to the skill or
88
- preference it came from so the Stop hook can resolve citation ids into
89
- human-readable titles for the dashboard.
90
- Silently no-ops when ``entries`` is empty.
117
+ The citation sidecar keeps the display metadata used by Stop. The ordered
118
+ session event stores identity pairs only, so publishing never has to join
119
+ two files or infer event order from timestamps.
91
120
  """
121
+
92
122
  records = list(entries)
93
123
  if not records:
94
124
  return
125
+
95
126
  path = injected_path(session_id)
96
127
  path.parent.mkdir(parents=True, exist_ok=True)
97
128
  with path.open("a", encoding="utf-8") as fh:
@@ -100,8 +131,22 @@ def append_injected(session_id: str, entries: Iterable[dict[str, Any]]) -> None:
100
131
  fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
101
132
  except OSError as exc:
102
133
  _LOGGER.debug("flock failed on %s: %s", path, exc)
103
- for rec in records:
104
- fh.write(json.dumps(rec, ensure_ascii=False) + "\n")
134
+ for record in records:
135
+ fh.write(json.dumps(record, ensure_ascii=False) + "\n")
136
+
137
+ refs: list[dict[str, str]] = []
138
+ seen: set[tuple[str, str]] = set()
139
+ for record in records:
140
+ ref = _normalize_registry_ref(record)
141
+ if ref is None:
142
+ continue
143
+ key = (ref["kind"], ref["learning_id"])
144
+ if key in seen:
145
+ continue
146
+ seen.add(key)
147
+ refs.append(ref)
148
+ if refs:
149
+ append(session_id, {"retrieved_learning_refs": refs})
105
150
 
106
151
 
107
152
  def read_injected(session_id: str) -> dict[str, dict[str, Any]]:
@@ -111,6 +156,7 @@ def read_injected(session_id: str) -> dict[str, dict[str, Any]]:
111
156
  (identical content produces the same hash-derived id, so the extra
112
157
  record only refreshes metadata).
113
158
  """
159
+
114
160
  path = injected_path(session_id)
115
161
  if not path.exists():
116
162
  return {}
@@ -206,13 +252,18 @@ def _to_wire_citations(cited_items: Any) -> list[dict[str, str]]:
206
252
  kind = item.get("kind")
207
253
  if not isinstance(real_id, str) or not real_id:
208
254
  continue
209
- if kind not in _VALID_CITATION_KINDS:
255
+ wire_kind = kind
256
+ if kind == "playbook":
257
+ source_kind = item.get("source_kind")
258
+ if source_kind in {"user_playbook", "agent_playbook"}:
259
+ wire_kind = source_kind
260
+ if wire_kind not in _VALID_CITATION_KINDS:
210
261
  continue
211
262
  tag = item.get("id")
212
263
  title = item.get("title")
213
264
  out.append(
214
265
  {
215
- "kind": kind,
266
+ "kind": wire_kind,
216
267
  "real_id": real_id,
217
268
  "tag": tag if isinstance(tag, str) else "",
218
269
  "title": title if isinstance(title, str) else "",
@@ -221,44 +272,127 @@ def _to_wire_citations(cited_items: Any) -> list[dict[str, str]]:
221
272
  return out
222
273
 
223
274
 
275
+ def published_record_offset(records: list[dict[str, Any]]) -> int:
276
+ """Return the highest valid publish watermark in the session buffer."""
277
+
278
+ limit = len(records)
279
+ return max(
280
+ (
281
+ value
282
+ for record in records
283
+ if isinstance((value := record.get("published_up_to")), int)
284
+ and 0 <= value <= limit
285
+ ),
286
+ default=0,
287
+ )
288
+
289
+
290
+ def pending_publish_end(
291
+ records: list[dict[str, Any]], published: int
292
+ ) -> int | None:
293
+ """Return the frozen end of the current publish attempt, if any."""
294
+
295
+ for marker_index, record in enumerate(records[published:], start=published):
296
+ attempt = record.get("publish_attempt")
297
+ if not isinstance(attempt, dict):
298
+ continue
299
+ start = attempt.get("start")
300
+ end = attempt.get("end")
301
+ if (
302
+ start == published
303
+ and isinstance(end, int)
304
+ and published < end <= marker_index
305
+ ):
306
+ _, interactions = unpublished_slice(
307
+ records[:end], published_override=published
308
+ )
309
+ if interactions:
310
+ return end
311
+ return None
312
+
313
+
314
+ def _wire_retrieved_ref(value: Any) -> dict[str, str] | None:
315
+ """Validate one identity pair read from the ordered session buffer."""
316
+
317
+ if not isinstance(value, dict):
318
+ return None
319
+ kind = value.get("kind")
320
+ learning_id = value.get("learning_id")
321
+ if kind not in _VALID_RETRIEVED_KINDS:
322
+ return None
323
+ if not isinstance(learning_id, str) or not learning_id:
324
+ return None
325
+ return {"kind": kind, "learning_id": learning_id}
326
+
327
+
328
+ def safe_publish_end(records: list[dict[str, Any]], published: int) -> int:
329
+ """Stop before identity refs that do not yet have an Assistant target."""
330
+
331
+ unmatched_start: int | None = None
332
+ for index, record in enumerate(records[published:], start=published):
333
+ refs = record.get("retrieved_learning_refs")
334
+ if (
335
+ unmatched_start is None
336
+ and isinstance(refs, list)
337
+ and any(_wire_retrieved_ref(value) is not None for value in refs)
338
+ ):
339
+ unmatched_start = index
340
+ if record.get("role") == "Assistant":
341
+ unmatched_start = None
342
+ return len(records) if unmatched_start is None else unmatched_start
343
+
344
+
224
345
  def unpublished_slice(
225
346
  records: Iterable[dict[str, Any]],
347
+ *,
348
+ published_override: int | None = None,
226
349
  ) -> tuple[int, list[dict[str, Any]]]:
227
- """Split records into (last-published index, unpublished turn records).
350
+ """Return the current watermark and wire-ready unpublished interactions.
228
351
 
229
- Walks the records in order, tracking the most recent ``published_up_to``
230
- marker and collecting turn records (anything with a ``role``) that come
231
- after it. Tool records are folded into the closest following Assistant
232
- turn's ``tools_used``.
233
-
234
- Returns:
235
- tuple[int, list[dict]]: ``(published_up_to, interactions)``. The
236
- integer is the watermark after which all turns are unpublished;
237
- the list is formatted for ``InteractionData`` construction.
352
+ Metadata records are interpreted by their numeric boundary rather than
353
+ their physical position in the append-only file. This preserves turns that
354
+ arrive while an earlier network request is in flight.
238
355
  """
239
- published = 0
356
+
357
+ buffered = list(records)
358
+ if published_override is None:
359
+ published = published_record_offset(buffered)
360
+ elif 0 <= published_override <= len(buffered):
361
+ published = published_override
362
+ else:
363
+ raise ValueError("published_override is outside the supplied record range")
240
364
  pending_tools: list[dict[str, Any]] = []
365
+ pending_refs: list[dict[str, str]] = []
241
366
  turns: list[dict[str, Any]] = []
242
- for idx, rec in enumerate(records):
243
- if "published_up_to" in rec:
244
- published = rec["published_up_to"]
245
- pending_tools = []
246
- turns = []
367
+ attached_total = skipped = truncated = 0
368
+
369
+ for index, record in enumerate(buffered):
370
+ if index < published:
247
371
  continue
248
- if idx < published:
372
+
373
+ refs = record.get("retrieved_learning_refs")
374
+ if "role" not in record and isinstance(refs, list):
375
+ for value in refs:
376
+ ref = _wire_retrieved_ref(value)
377
+ if ref is None:
378
+ skipped += 1
379
+ else:
380
+ pending_refs.append(ref)
249
381
  continue
250
- role = rec.get("role")
382
+
383
+ role = record.get("role")
251
384
  if role == "Assistant_tool":
252
- tool_input = rec.get("tool_input") or {}
253
- tool_output = rec.get("tool_output") or ""
385
+ tool_input = record.get("tool_input") or {}
386
+ tool_output = record.get("tool_output") or ""
254
387
  tool_entry: dict[str, Any] = {
255
- "tool_name": rec.get("tool_name", ""),
256
- "status": rec.get("status", "success"),
388
+ "tool_name": record.get("tool_name", ""),
389
+ "status": record.get("status", "success"),
257
390
  }
258
391
  tool_data: dict[str, Any] = {}
259
392
  if tool_input:
260
393
  tool_data["input"] = {
261
- k: _truncate_tool_data_field(v) for k, v in tool_input.items()
394
+ key: _truncate_tool_data_field(value)
395
+ for key, value in tool_input.items()
262
396
  }
263
397
  if tool_output:
264
398
  tool_data["output"] = _truncate_tool_data_field(tool_output)
@@ -266,20 +400,45 @@ def unpublished_slice(
266
400
  tool_entry["tool_data"] = tool_data
267
401
  pending_tools.append(tool_entry)
268
402
  continue
269
- if role in {"User", "Assistant"}:
270
- # ``cited_items`` is local-only metadata (dashboard "used" badge);
271
- # map it onto the wire's ``citations`` field — reflexio uses those
272
- # to drive skill/preference reflection in the publish flow.
273
- turn = {
274
- k: v for k, v in rec.items() if k not in {"role", "ts", "cited_items"}
275
- }
276
- turn["role"] = role
277
- if role == "Assistant":
278
- citations = _to_wire_citations(rec.get("cited_items"))
279
- if citations:
280
- turn["citations"] = citations
281
- if pending_tools:
282
- turn["tools_used"] = pending_tools
283
- pending_tools = []
284
- turns.append(turn)
403
+
404
+ if role not in {"User", "Assistant"}:
405
+ continue
406
+
407
+ turn = {
408
+ key: value
409
+ for key, value in record.items()
410
+ if key not in {"role", "ts", "cited_items", "host"}
411
+ }
412
+ turn["role"] = role
413
+ if role == "Assistant":
414
+ citations = _to_wire_citations(record.get("cited_items"))
415
+ if citations:
416
+ turn["citations"] = citations
417
+ if pending_tools:
418
+ turn["tools_used"] = pending_tools
419
+ pending_tools = []
420
+
421
+ retrieved: list[dict[str, str]] = []
422
+ seen: set[tuple[str, str]] = set()
423
+ for ref in pending_refs:
424
+ key = (ref["kind"], ref["learning_id"])
425
+ if key in seen:
426
+ continue
427
+ seen.add(key)
428
+ if attached_total >= _RETRIEVED_LEARNINGS_PUBLISH_CAP:
429
+ truncated += 1
430
+ continue
431
+ retrieved.append(ref)
432
+ attached_total += 1
433
+ pending_refs = []
434
+ if retrieved:
435
+ turn["retrieved_learnings"] = retrieved
436
+ turns.append(turn)
437
+
438
+ if skipped:
439
+ _LOGGER.debug("Skipped %d malformed retrieved-learning refs", skipped)
440
+ if truncated:
441
+ _LOGGER.warning(
442
+ "Dropped %d retrieved-learning refs at the publish request cap", truncated
443
+ )
285
444
  return published, turns
package/plugin/uv.lock CHANGED
@@ -476,7 +476,7 @@ wheels = [
476
476
 
477
477
  [[package]]
478
478
  name = "claude-smart"
479
- version = "0.2.48"
479
+ version = "0.2.50"
480
480
  source = { editable = "." }
481
481
  dependencies = [
482
482
  { name = "chromadb" },
@@ -494,7 +494,7 @@ dev = [
494
494
  requires-dist = [
495
495
  { name = "chromadb", specifier = ">=0.5" },
496
496
  { name = "einops", specifier = ">=0.8.0" },
497
- { name = "reflexio-ai", specifier = ">=0.2.27" },
497
+ { name = "reflexio-ai", specifier = ">=0.2.28" },
498
498
  { name = "sqlite-vec", specifier = ">=0.1.6" },
499
499
  ]
500
500
 
@@ -2760,7 +2760,7 @@ wheels = [
2760
2760
 
2761
2761
  [[package]]
2762
2762
  name = "reflexio-ai"
2763
- version = "0.2.27"
2763
+ version = "0.2.28"
2764
2764
  source = { registry = "https://pypi.org/simple" }
2765
2765
  dependencies = [
2766
2766
  { name = "aiohttp" },
@@ -2800,9 +2800,9 @@ dependencies = [
2800
2800
  { name = "websocket-client" },
2801
2801
  { name = "xlsxwriter" },
2802
2802
  ]
2803
- sdist = { url = "https://files.pythonhosted.org/packages/85/22/64429eb84ae74aaa6d36f0a2e0d91a27b35ee33ef17c18411d54f08706ba/reflexio_ai-0.2.27.tar.gz", hash = "sha256:e433209ca848245e957943c6476f2bba32581de8804d535d25ca0596b0da0afd", size = 1112139, upload-time = "2026-06-30T06:26:21.88Z" }
2803
+ sdist = { url = "https://files.pythonhosted.org/packages/2a/e6/a59fc40136b37ea3e9b6878b600ef1f405c1baf7d1431a07e55c8e49c14e/reflexio_ai-0.2.28.tar.gz", hash = "sha256:49a35abf714ae293392277b278b895cde21ca50c14f10c97cbcf27c307e49fae", size = 1155554, upload-time = "2026-07-04T07:01:02.207Z" }
2804
2804
  wheels = [
2805
- { url = "https://files.pythonhosted.org/packages/68/22/d65332c75b9072e8838d51f68354d8d17c7636aa1cf73a20cfc1147b4364/reflexio_ai-0.2.27-py3-none-any.whl", hash = "sha256:4598f4aaa61e3c755a04573ab9711a7b5673ebf268029b2cef21cdcd5d4abfcf", size = 1461400, upload-time = "2026-06-30T06:26:19.965Z" },
2805
+ { url = "https://files.pythonhosted.org/packages/96/a6/c842d5e525f0f561fcd11ecf6543c9c52b3f4b665dbc2a8fe94adce9be1b/reflexio_ai-0.2.28-py3-none-any.whl", hash = "sha256:177e3a740afa2acb5a2a14a1e648c32c2d9e71ef9ff79b8ab2caec7e24ff5d88", size = 1558983, upload-time = "2026-07-04T07:01:00.69Z" },
2806
2806
  ]
2807
2807
 
2808
2808
  [[package]]
@@ -77,6 +77,19 @@ REFLEXIO_PUBLISH_LEARNING_WORKERS=
77
77
  # Inactivity delay in seconds before a quiet session is evaluated by agent-success
78
78
  # evaluation. (optional; default 600 = 10 minutes)
79
79
  GROUP_EVALUATION_DELAY_SECONDS=600
80
+ # Opt-in background sweep that re-embeds interactions whose embedding failed at
81
+ # ingest (empty vector, no vector row -> invisible to vector/hybrid search). Runs
82
+ # per-org on the shared lineage-GC scheduler cadence. Off by default so the GC
83
+ # scheduler's start conditions are unchanged unless you opt in.
84
+ # options: true | false (optional; default false)
85
+ REFLEXIO_MISSING_VECTOR_BACKFILL_ENABLED=false
86
+ # Max interactions re-embedded per org per sweep tick (bounds embedding cost and
87
+ # log volume). (optional; default 200; values <= 0 fall back to the default)
88
+ REFLEXIO_MISSING_VECTOR_BACKFILL_CAP=200
89
+ # Bounded per-tick org fan-out width for the lineage-GC / expiry-reclamation
90
+ # scheduler. SQLite storage is pinned to serial (1) regardless of this value.
91
+ # (optional; default 8; non-numeric or <=0 falls back to default)
92
+ REFLEXIO_SCHEDULER_ORG_WORKERS=
80
93
 
81
94
  # =============================================================================
82
95
  # 5. TESTING & LOGGING
@@ -56,8 +56,9 @@ Description: Python SDK for interacting with Reflexio API remotely
56
56
  Remote API client for applications to:
57
57
  1. **Publish interactions** - Send user interactions to server for processing
58
58
  2. **Search/retrieve data** - Query profiles, interactions, playbooks, evaluations, and context
59
- 3. **Manage profiles/playbooks** - Delete, regenerate, and update status where supported by API endpoints
60
- 4. **Configure** - Set/get organization configuration
59
+ 3. **Track deferred learning** - Poll `get_learning_status(request_id)` after `publish_interaction(..., wait_for_response=False)` queues extraction
60
+ 4. **Manage profiles/playbooks** - Delete, regenerate, and update status where supported by API endpoints
61
+ 5. **Configure** - Set/get organization configuration
61
62
 
62
63
  #### Architecture Pattern
63
64
  Async HTTP client wrapping typed models from `models/api_schema/`. Automatically handles authentication via Bearer tokens.
@@ -123,6 +124,7 @@ client (Python SDK)
123
124
  -> services/generation_service.py (orchestrator)
124
125
  ├─> services/profile/ -> storage (BaseStorage)
125
126
  ├─> services/playbook/ (playbook extraction) -> storage (BaseStorage)
127
+ ├─> services/durable_learning/ -> learning_jobs queue -> deferred extraction
126
128
  └─> services/agent_success_evaluation/ -> storage (BaseStorage)
127
129
  ```
128
130
 
@@ -138,6 +140,7 @@ client (Python SDK)
138
140
  - `profile/` - Profile extraction & updates
139
141
  - `playbook/` - Playbook extraction, consolidation, and aggregation
140
142
  - `agent_success_evaluation/` - Success evaluation
143
+ - `durable_learning/` - Claim/drain durable `learning_jobs` for deferred extraction when `REFLEXIO_DURABLE_LEARNING_QUEUE` is enabled
141
144
  - `reflection/` - Post-horizon reflection extraction
142
145
  - `extraction/` - Resumable async extraction agent infrastructure
143
146
  - `shadow_comparison/` - Per-turn regular vs shadow verdict judge
@@ -146,9 +149,10 @@ client (Python SDK)
146
149
  - `braintrust/` - Braintrust eval export/sync support
147
150
  - `lineage/` - Resolve current records and schedule tombstone garbage collection for superseded profile/playbook rows
148
151
  - `governance/` - Subject-reference contracts and retention/barrier helpers used by storage and lineage
149
- - `storage/` - Abstract layer (SQLite prod, LocalJSON test) with governance-aware write validation
152
+ - `storage/` - Abstract layer (SQLite prod, LocalJSON test) with governance-aware write validation and durable `learning_jobs` contracts
150
153
  - `pre_retrieval/` - Query rewriting and document expansion helpers
151
154
  - `configurator/` - YAML config loader
155
+ - **`billing_meter.py`**: OSS usage-event facade for learning/search metering; keep imports function-local at call sites so enterprise emitters remain optional
152
156
  - **`site_var/`**: Global settings singleton
153
157
 
154
158
  ### Architecture Patterns
@@ -5,6 +5,13 @@ try:
5
5
  except PackageNotFoundError:
6
6
  __version__ = "0.0.0-dev"
7
7
 
8
+ from reflexio.models.api_schema.eval_overview_schema import (
9
+ GradeOnDemandRequest,
10
+ GradeOnDemandResponse,
11
+ RegenerateRequest,
12
+ RegenerateStartResponse,
13
+ RegenerateStatusResponse,
14
+ )
8
15
  from reflexio.models.api_schema.retriever_schema import (
9
16
  ConversationTurn,
10
17
  GetAgentPlaybooksViewResponse,
@@ -125,6 +132,8 @@ __all__ = [
125
132
  "DeleteProfilesByIdsRequest",
126
133
  "DeleteAgentPlaybooksByIdsRequest",
127
134
  "DeleteUserPlaybooksByIdsRequest",
135
+ "GradeOnDemandRequest",
136
+ "RegenerateRequest",
128
137
  # Response types (internal)
129
138
  "PublishUserInteractionResponse",
130
139
  "DeleteUserProfileResponse",
@@ -135,6 +144,9 @@ __all__ = [
135
144
  "SearchInteractionResponse",
136
145
  "SearchUserProfileResponse",
137
146
  "BulkDeleteResponse",
147
+ "GradeOnDemandResponse",
148
+ "RegenerateStartResponse",
149
+ "RegenerateStatusResponse",
138
150
  # View response types (user-facing)
139
151
  "GetInteractionsViewResponse",
140
152
  "GetProfilesViewResponse",
@@ -78,6 +78,9 @@ When `REFLEXIO_EMBEDDING_PROVIDER=local_service` or
78
78
  `local/nomic-embed-text-v1.5`, and `local/minilm-l6-v2`. Local embedding
79
79
  requests allow extra time for model cold start; override with
80
80
  `REFLEXIO_EMBEDDING_SERVICE_TIMEOUT_MS` when needed.
81
+ MiniLM cache corruption is retried once automatically. If recovery still fails,
82
+ delete the cache directory named in the error message, restart Reflexio, and
83
+ retry local embedding.
81
84
 
82
85
  ## Publishing interactions
83
86