claude-smart 0.2.49 → 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 (177) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +10 -43
  3. package/bin/claude-smart.js +105 -0
  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/pyproject.toml +1 -1
  27. package/plugin/scripts/_lib.sh +197 -1
  28. package/plugin/scripts/backend-python-runner.py +46 -0
  29. package/plugin/scripts/backend-service.sh +757 -119
  30. package/plugin/scripts/codex-hook.js +63 -225
  31. package/plugin/scripts/dashboard-open.sh +6 -4
  32. package/plugin/scripts/dashboard-service.sh +117 -136
  33. package/plugin/scripts/hook_entry.sh +3 -0
  34. package/plugin/scripts/smart-install.sh +15 -1
  35. package/plugin/src/claude_smart/cli.py +14 -0
  36. package/plugin/src/claude_smart/context_inject.py +3 -0
  37. package/plugin/src/claude_smart/env_config.py +4 -1
  38. package/plugin/src/claude_smart/events/post_tool.py +2 -1
  39. package/plugin/src/claude_smart/events/session_end.py +2 -1
  40. package/plugin/src/claude_smart/events/stop.py +3 -0
  41. package/plugin/src/claude_smart/events/user_prompt.py +2 -1
  42. package/plugin/src/claude_smart/internal_call.py +5 -2
  43. package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
  44. package/plugin/src/claude_smart/publish.py +59 -7
  45. package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
  46. package/plugin/src/claude_smart/runtime.py +15 -6
  47. package/plugin/src/claude_smart/state.py +211 -52
  48. package/plugin/uv.lock +1 -1
  49. package/plugin/vendor/reflexio/.env.example +13 -0
  50. package/plugin/vendor/reflexio/reflexio/README.md +7 -3
  51. package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
  52. package/plugin/vendor/reflexio/reflexio/client/client.py +126 -3
  53. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
  54. package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
  55. package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
  56. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +177 -25
  57. package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
  58. package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
  59. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
  60. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
  61. package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
  62. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
  63. package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
  64. package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
  65. package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
  66. package/plugin/vendor/reflexio/reflexio/server/api.py +66 -3
  67. package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
  68. package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
  69. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
  70. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
  71. package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +62 -5
  72. package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
  73. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
  74. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
  75. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
  76. package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
  77. package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +20 -5
  78. package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
  79. package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
  80. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
  81. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
  82. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
  83. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
  84. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
  85. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
  86. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
  87. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
  88. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
  89. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
  90. package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
  91. package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
  92. package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
  93. package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
  94. package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
  95. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
  96. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
  97. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
  98. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
  99. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
  100. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
  101. package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
  102. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
  103. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
  104. package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
  105. package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
  106. package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
  107. package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
  108. package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +85 -16
  109. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
  110. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
  111. package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
  112. package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
  113. package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
  114. package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
  115. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
  116. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
  117. package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
  118. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
  119. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
  120. package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
  121. package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
  122. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
  123. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
  124. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
  125. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
  126. package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
  127. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
  128. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
  129. package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
  130. package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
  131. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
  132. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
  133. package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +122 -44
  134. package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
  135. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
  136. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
  137. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
  138. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
  139. package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
  140. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
  141. package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
  142. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
  143. package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
  144. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +1 -1
  145. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
  146. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
  147. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
  148. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +36 -1
  149. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
  150. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
  151. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
  152. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
  153. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +43 -0
  154. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
  155. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +45 -7
  156. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
  157. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
  158. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
  159. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
  160. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +50 -0
  161. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
  162. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
  163. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
  164. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
  165. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
  166. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
  167. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
  168. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
  169. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +59 -0
  170. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -2
  171. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
  172. package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
  173. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
  174. package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
  175. package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
  176. package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
  177. package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
@@ -8,7 +8,8 @@ messaging without peeking at the adapter.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from typing import Literal
11
+ import uuid
12
+ from typing import Any, Literal
12
13
 
13
14
  from claude_smart import state
14
15
  from claude_smart.reflexio_adapter import Adapter
@@ -16,6 +17,50 @@ from claude_smart.reflexio_adapter import Adapter
16
17
  PublishStatus = Literal["nothing", "ok", "failed"]
17
18
 
18
19
 
20
+ def _publish_request_id(session_id: str, start: int, end: int) -> str:
21
+ """Return a stable request ID for one buffered publish range."""
22
+
23
+ name = f"claude-smart:{session_id}:{start}:{end}"
24
+ return str(uuid.uuid5(uuid.NAMESPACE_URL, name))
25
+
26
+
27
+ def _prepare_publish_batch(
28
+ session_id: str,
29
+ ) -> tuple[int, int, list[dict[str, Any]]] | None:
30
+ """Freeze and return one canonical unpublished record range."""
31
+
32
+ while True:
33
+ records = state.read_all(session_id)
34
+ published, available = state.unpublished_slice(records)
35
+ if not available:
36
+ return None
37
+
38
+ publish_end = state.pending_publish_end(records, published)
39
+ if publish_end is None:
40
+ publish_end = state.safe_publish_end(records, published)
41
+ _, proposed = state.unpublished_slice(
42
+ records[:publish_end], published_override=published
43
+ )
44
+ if not proposed:
45
+ return None
46
+ state.append(
47
+ session_id,
48
+ {"publish_attempt": {"start": published, "end": publish_end}},
49
+ )
50
+
51
+ canonical = state.read_all(session_id)
52
+ if state.published_record_offset(canonical) != published:
53
+ continue
54
+ canonical_end = state.pending_publish_end(canonical, published)
55
+ if canonical_end is None:
56
+ continue
57
+ _, interactions = state.unpublished_slice(
58
+ canonical[:canonical_end], published_override=published
59
+ )
60
+ if interactions:
61
+ return published, canonical_end, interactions
62
+
63
+
19
64
  def publish_unpublished(
20
65
  *,
21
66
  session_id: str,
@@ -56,20 +101,27 @@ def publish_unpublished(
56
101
  was unreachable. On ``"failed"`` the watermark is not advanced,
57
102
  so the next hook retries the same batch.
58
103
  """
59
- records = state.read_all(session_id)
60
- _, interactions = state.unpublished_slice(records)
61
- if not interactions:
104
+ batch = _prepare_publish_batch(session_id)
105
+ if batch is None:
62
106
  return ("nothing", 0)
107
+ published_record_offset, publish_end, interactions = batch
108
+
63
109
  client = adapter if adapter is not None else Adapter()
64
- ok = client.publish(
110
+ request_id = _publish_request_id(session_id, published_record_offset, publish_end)
111
+ result = client.publish(
65
112
  session_id=session_id,
66
113
  project_id=project_id,
114
+ request_id=request_id,
67
115
  interactions=interactions,
68
116
  force_extraction=force_extraction,
69
117
  override_learning_stall=override_learning_stall,
70
118
  skip_aggregation=skip_aggregation,
71
119
  )
72
- if ok:
73
- state.append(session_id, {"published_up_to": len(records)})
120
+ if result:
121
+ marker: dict[str, Any] = {"published_up_to": publish_end}
122
+ confirmed_request_id = result.request_id
123
+ if isinstance(confirmed_request_id, str) and confirmed_request_id:
124
+ marker["request_id"] = confirmed_request_id
125
+ state.append(session_id, marker)
74
126
  return ("ok", len(interactions))
75
127
  return ("failed", len(interactions))
@@ -20,7 +20,6 @@ _LOGGER = logging.getLogger(__name__)
20
20
 
21
21
  _ENV_URL = "REFLEXIO_URL"
22
22
  _ENV_API_KEY = "REFLEXIO_API_KEY"
23
- _DEFAULT_URL = "http://localhost:8071/"
24
23
  # Cap every HTTP round-trip from a hook so a hung backend can't stall the
25
24
  # Claude Code / Codex session. reflexio's client default is 300s, which is
26
25
  # fine for batch workloads but unacceptable on the hook path — a single
@@ -28,18 +27,48 @@ _DEFAULT_URL = "http://localhost:8071/"
28
27
  # in ``ids.py`` for short-lived hook HTTP calls.
29
28
  _HTTP_TIMEOUT_SECONDS = 5
30
29
  _SEARCH_MODE_HYBRID = "hybrid" # reflexio.models.config_schema.SearchMode.HYBRID
30
+ _SOURCE = "claude-smart"
31
31
  _UNIFIED_ENTITY_TYPES = ("profiles", "user_playbooks", "agent_playbooks")
32
32
  _AGENT_PLAYBOOK_APPROVAL_STATUSES = ("pending", "approved")
33
33
  _REJECTED_AGENT_PLAYBOOK_STATUS = "rejected"
34
+ _LOCAL_8071_URLS = {
35
+ "http://localhost:8071",
36
+ "http://localhost:8071/",
37
+ "http://127.0.0.1:8071",
38
+ "http://127.0.0.1:8071/",
39
+ }
40
+
41
+
42
+ def _default_url() -> str:
43
+ return f"http://localhost:{os.environ.get('BACKEND_PORT', '8071')}/"
44
+
45
+
46
+ def _configured_url() -> str:
47
+ url = os.environ.get(_ENV_URL, "")
48
+ backend_port = os.environ.get("BACKEND_PORT", "")
49
+ if url in _LOCAL_8071_URLS and backend_port not in {"", "8071"}:
50
+ return _default_url()
51
+ return url or _default_url()
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class PublishResult:
56
+ """Outcome and server-confirmed lineage for one publish call."""
57
+
58
+ ok: bool
59
+ request_id: str | None = None
60
+
61
+ def __bool__(self) -> bool:
62
+ return self.ok
34
63
 
35
64
 
36
65
  @dataclass
37
66
  class Adapter:
38
67
  """Wraps the reflexio client and absorbs connection errors.
39
68
 
40
- All methods degrade to a neutral no-op return (empty list / False) on
41
- connection failure so a missing or down reflexio server never crashes
42
- a Claude Code hook.
69
+ All methods degrade to a neutral no-op result (an empty list or a falsey
70
+ result) on connection failure so a missing or down reflexio server never
71
+ crashes a host hook.
43
72
  """
44
73
 
45
74
  url: str = ""
@@ -48,7 +77,7 @@ class Adapter:
48
77
 
49
78
  def __post_init__(self) -> None:
50
79
  env_config.load_reflexio_env()
51
- self.url = self.url or os.environ.get(_ENV_URL, _DEFAULT_URL)
80
+ self.url = self.url or _configured_url()
52
81
  self.api_key = self.api_key or os.environ.get(_ENV_API_KEY, "")
53
82
  self._client: Any | None = None
54
83
 
@@ -92,38 +121,96 @@ class Adapter:
92
121
  *,
93
122
  session_id: str,
94
123
  project_id: str,
124
+ request_id: str | None = None,
95
125
  interactions: Sequence[dict[str, Any]],
96
126
  force_extraction: bool = False,
97
127
  override_learning_stall: bool = False,
98
128
  skip_aggregation: bool = False,
99
- ) -> bool:
100
- """Publish buffered interactions to reflexio. Returns True on success."""
129
+ ) -> PublishResult:
130
+ """Publish interactions and return this call's confirmed request ID."""
101
131
  if not interactions:
102
- return True
132
+ return PublishResult(True)
103
133
  client = self._get_client()
104
134
  if client is None:
105
- return False
135
+ return PublishResult(False)
106
136
  try:
137
+ interaction_list = list(interactions)
138
+ raw_request = getattr(client, "_make_request", None)
139
+ supports_source = _supports_keyword(client.publish_interaction, "source")
140
+ if request_id is not None and callable(raw_request):
141
+ payload: dict[str, Any] = {
142
+ "request_id": request_id,
143
+ "user_id": project_id,
144
+ "interaction_data_list": interaction_list,
145
+ "agent_version": runtime.agent_version(),
146
+ "session_id": session_id,
147
+ "skip_aggregation": skip_aggregation,
148
+ "force_extraction": force_extraction,
149
+ "evaluation_only": False,
150
+ "override_learning_stall": override_learning_stall,
151
+ }
152
+ if supports_source:
153
+ payload["source"] = _SOURCE
154
+ try:
155
+ raw_request(
156
+ "POST",
157
+ "/api/publish_interaction",
158
+ json=payload,
159
+ params=None,
160
+ )
161
+ return PublishResult(True, request_id)
162
+ except Exception as exc: # noqa: BLE001
163
+ if not _needs_raw_retrieved_learning_publish(interaction_list):
164
+ raise
165
+ _LOGGER.warning(
166
+ "Could not confirm retrieved-learning links; retrying "
167
+ "the base interaction with the same request ID: %s",
168
+ exc,
169
+ )
170
+ fallback_payload = {
171
+ **payload,
172
+ "interaction_data_list": _without_retrieved_learnings(
173
+ interaction_list
174
+ ),
175
+ }
176
+ raw_request(
177
+ "POST",
178
+ "/api/publish_interaction",
179
+ json=fallback_payload,
180
+ params=None,
181
+ )
182
+ return PublishResult(True, request_id)
183
+ if _needs_raw_retrieved_learning_publish(interaction_list):
184
+ _LOGGER.warning(
185
+ "Stable raw publishing is unavailable; publishing "
186
+ "without optional retrieved-learning links"
187
+ )
188
+ interaction_list = _without_retrieved_learnings(interaction_list)
107
189
  kwargs = {
108
190
  "user_id": project_id,
109
- "interactions": list(interactions),
191
+ "interactions": interaction_list,
110
192
  "agent_version": runtime.agent_version(),
111
193
  "session_id": session_id,
112
194
  "wait_for_response": False,
113
195
  "force_extraction": force_extraction,
114
196
  "skip_aggregation": skip_aggregation,
115
197
  }
198
+ if supports_source:
199
+ kwargs["source"] = _SOURCE
116
200
  if _supports_keyword(client.publish_interaction, "override_learning_stall"):
117
201
  kwargs["override_learning_stall"] = override_learning_stall
118
202
  elif override_learning_stall:
119
203
  _LOGGER.debug(
120
204
  "publish_interaction client does not support override_learning_stall"
121
205
  )
122
- client.publish_interaction(**kwargs)
123
- return True
206
+ response = client.publish_interaction(**kwargs)
207
+ response_request_id = getattr(response, "request_id", None)
208
+ if isinstance(response_request_id, str) and response_request_id:
209
+ return PublishResult(True, response_request_id)
210
+ return PublishResult(True)
124
211
  except Exception as exc: # noqa: BLE001
125
212
  _LOGGER.warning("publish_interaction failed: %s", exc)
126
- return False
213
+ return PublishResult(False)
127
214
 
128
215
  def apply_extraction_defaults(self, *, window_size: int, stride_size: int) -> bool:
129
216
  """Push claude-smart's preferred extraction defaults to the reflexio server.
@@ -340,7 +427,12 @@ class Adapter:
340
427
  # -----------------------------------------------------------------
341
428
 
342
429
  def search_all(
343
- self, *, project_id: str, query: str, top_k: int = 5
430
+ self,
431
+ *,
432
+ project_id: str,
433
+ query: str,
434
+ top_k: int = 5,
435
+ session_id: str | None = None,
344
436
  ) -> tuple[list[Any], list[Any], list[Any]]:
345
437
  """Unified hybrid search → ``(user_playbooks, agent_playbooks, preferences)``.
346
438
 
@@ -354,6 +446,10 @@ class Adapter:
354
446
  project_id (str): reflexio ``user_id`` for this repo.
355
447
  query (str): Free-text query routed through BM25 + vector RRF.
356
448
  top_k (int): Cap on results per entity type.
449
+ session_id (str | None): Claude Code session id. When set, the
450
+ server skips results it already returned to this session
451
+ and backfills next-best matches, so repeated hook searches
452
+ within one session stop re-injecting the same rules.
357
453
 
358
454
  Returns:
359
455
  tuple[list[Any], list[Any], list[Any]]: ``(user_playbooks,
@@ -374,6 +470,7 @@ class Adapter:
374
470
  enable_agent_answer=False,
375
471
  top_k=top_k,
376
472
  search_mode=_SEARCH_MODE_HYBRID,
473
+ session_id=session_id,
377
474
  )
378
475
  except Exception as exc: # noqa: BLE001
379
476
  self._record_read_error("unified search", exc)
@@ -449,6 +546,32 @@ def _supports_keyword(callable_obj: Any, keyword: str) -> bool:
449
546
  return keyword in signature.parameters
450
547
 
451
548
 
549
+ def _needs_raw_retrieved_learning_publish(
550
+ interactions: Sequence[dict[str, Any]],
551
+ ) -> bool:
552
+ """Return whether links require a caller-stable raw request.
553
+
554
+ The public client does not accept a caller-supplied request ID, and older
555
+ clients also strip this field. The authenticated helper preserves both.
556
+ """
557
+ return any(item.get("retrieved_learnings") for item in interactions)
558
+
559
+
560
+ def _without_retrieved_learnings(
561
+ interactions: Sequence[dict[str, Any]],
562
+ ) -> list[dict[str, Any]]:
563
+ """Copy interactions without the optional compatibility-only field."""
564
+
565
+ return [
566
+ {
567
+ key: value
568
+ for key, value in interaction.items()
569
+ if key != "retrieved_learnings"
570
+ }
571
+ for interaction in interactions
572
+ ]
573
+
574
+
452
575
  def _filter_rejected_agent_playbooks(items: list[Any]) -> list[Any]:
453
576
  """Drop rejected shared skills defensively, even if an older backend ignores filters."""
454
577
  return [
@@ -15,27 +15,36 @@ INTERNAL_ENV = "CLAUDE_SMART_INTERNAL"
15
15
  HOST_CLAUDE_CODE = "claude-code"
16
16
  HOST_CODEX = "codex"
17
17
  HOST_OPENCODE = "opencode"
18
+ HOST_UNKNOWN = "unknown"
18
19
  VALID_HOSTS = frozenset({HOST_CLAUDE_CODE, HOST_CODEX, HOST_OPENCODE})
19
20
 
20
21
  _SHARED_AGENT_VERSION = "claude-code"
21
22
  _current_host: str | None = None
22
23
 
23
24
 
25
+ def _resolve_host(value: str | None, fallback: str) -> str:
26
+ return value if value in VALID_HOSTS else fallback
27
+
28
+
24
29
  def set_host(value: str | None) -> str:
25
30
  """Set the current host, returning the normalized value."""
26
31
  global _current_host
27
- host = value if value in VALID_HOSTS else HOST_CLAUDE_CODE
28
- _current_host = host
32
+ _current_host = value if value is not None else HOST_UNKNOWN
33
+ host = _resolve_host(_current_host, HOST_CLAUDE_CODE)
29
34
  os.environ[HOST_ENV] = host
30
35
  return host
31
36
 
32
37
 
33
38
  def host() -> str:
34
39
  """Return the current host, defaulting to Claude Code for compatibility."""
35
- if _current_host is not None:
36
- return _current_host
37
- value = os.environ.get(HOST_ENV)
38
- return value if value in VALID_HOSTS else HOST_CLAUDE_CODE
40
+ value = _current_host if _current_host is not None else os.environ.get(HOST_ENV)
41
+ return _resolve_host(value, HOST_CLAUDE_CODE)
42
+
43
+
44
+ def attribution_host() -> str:
45
+ """Return the explicitly selected record host, or unknown when unset."""
46
+ value = _current_host if _current_host is not None else os.environ.get(HOST_ENV)
47
+ return _resolve_host(value, HOST_UNKNOWN)
39
48
 
40
49
 
41
50
  def is_codex() -> bool: