claude-smart 0.2.46 → 0.2.47

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 (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +19 -11
  3. package/bin/claude-smart.js +290 -68
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +11 -10
  8. package/plugin/dashboard/app/layout.tsx +20 -0
  9. package/plugin/opencode/dist/server.mjs +76 -2
  10. package/plugin/opencode/server.mts +79 -2
  11. package/plugin/pyproject.toml +6 -2
  12. package/plugin/scripts/smart-install.sh +7 -1
  13. package/plugin/src/claude_smart/cli.py +210 -22
  14. package/plugin/src/claude_smart/context_format.py +9 -9
  15. package/plugin/src/claude_smart/cs_cite.py +66 -19
  16. package/plugin/uv.lock +5 -5
  17. package/plugin/vendor/reflexio/README.md +3 -3
  18. package/plugin/vendor/reflexio/pyproject.toml +1 -1
  19. package/plugin/vendor/reflexio/reflexio/README.md +3 -1
  20. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  21. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  22. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  23. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  24. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  25. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  26. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  27. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  28. package/plugin/vendor/reflexio/reflexio/lib/_search.py +23 -5
  29. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  30. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  31. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  32. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  33. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +16 -0
  34. package/plugin/vendor/reflexio/reflexio/server/README.md +14 -2
  35. package/plugin/vendor/reflexio/reflexio/server/api.py +176 -29
  36. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  37. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  38. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  39. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  40. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
  41. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  42. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  43. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -1
  44. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  45. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  46. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  47. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  48. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +45 -19
  49. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  50. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  51. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +129 -111
  52. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  53. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  54. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  55. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  56. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  57. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  58. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +14 -2
  59. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +2 -0
  60. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  61. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
  62. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +5 -3
  63. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  64. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +262 -107
  65. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  66. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
  68. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  70. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  71. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
  72. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +19 -3
  73. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  74. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
  75. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  76. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +13 -0
  77. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  85. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -38,22 +38,23 @@ class GenerationMixin(ReflexioBase):
38
38
  """
39
39
  if not self._is_storage_configured():
40
40
  raise ValueError(STORAGE_NOT_CONFIGURED_MSG)
41
+ from reflexio.server.extensions import get_service
42
+ from reflexio.server.services.playbook.aggregation_prompt_processing import (
43
+ AGGREGATION_PROMPT_PROCESSOR,
44
+ )
41
45
  from reflexio.server.services.playbook.components.aggregator import (
42
46
  PlaybookAggregator,
43
47
  )
44
48
  from reflexio.server.services.playbook.playbook_service_utils import (
45
49
  PlaybookAggregatorRequest,
46
50
  )
47
- from reflexio.server.services.playbook.user_detail_stripping import (
48
- create_aggregation_user_detail_stripper,
49
- )
50
51
 
51
- user_detail_stripper = create_aggregation_user_detail_stripper(
52
- self.request_context.configurator
53
- )
52
+ aggregation_prompt_processor = get_service(AGGREGATION_PROMPT_PROCESSOR)
54
53
  aggregator_kwargs = {}
55
- if user_detail_stripper is not None:
56
- aggregator_kwargs["user_detail_stripper"] = user_detail_stripper
54
+ if aggregation_prompt_processor is not None:
55
+ aggregator_kwargs["aggregation_prompt_processor"] = (
56
+ aggregation_prompt_processor
57
+ )
57
58
 
58
59
  playbook_aggregator = PlaybookAggregator(
59
60
  llm_client=self.llm_client,
@@ -414,26 +414,22 @@ class ProfilesMixin(ReflexioBase):
414
414
  status_filter = [None] # Default to current profiles
415
415
 
416
416
  profiles = self._get_storage().get_user_profile(
417
- request.user_id, status_filter=status_filter, tags=request.tags
417
+ request.user_id,
418
+ status_filter=status_filter,
419
+ tags=request.tags,
420
+ profile_id=request.profile_id,
421
+ query=request.query,
422
+ source=request.source,
423
+ profile_time_to_live=request.profile_time_to_live,
424
+ start_time=(
425
+ int(request.start_time.timestamp()) if request.start_time else None
426
+ ),
427
+ end_time=int(request.end_time.timestamp()) if request.end_time else None,
418
428
  )
419
429
  profiles = sorted(
420
430
  profiles, key=lambda x: x.last_modified_timestamp, reverse=True
421
431
  )
422
432
 
423
- # Apply time filters
424
- if request.start_time:
425
- profiles = [
426
- p
427
- for p in profiles
428
- if p.last_modified_timestamp >= int(request.start_time.timestamp())
429
- ]
430
- if request.end_time:
431
- profiles = [
432
- p
433
- for p in profiles
434
- if p.last_modified_timestamp <= int(request.end_time.timestamp())
435
- ]
436
-
437
433
  # Apply top_k limit
438
434
  if request.top_k:
439
435
  profiles = sorted(
@@ -450,6 +446,13 @@ class ProfilesMixin(ReflexioBase):
450
446
  self,
451
447
  limit: int = 100,
452
448
  status_filter: list[Status | None] | None = None,
449
+ user_id: str | None = None,
450
+ profile_id: str | None = None,
451
+ query: str | None = None,
452
+ source: str | None = None,
453
+ profile_time_to_live: str | None = None,
454
+ start_time: int | None = None,
455
+ end_time: int | None = None,
453
456
  ) -> GetUserProfilesResponse:
454
457
  """Get all user profiles across all users.
455
458
 
@@ -467,7 +470,15 @@ class ProfilesMixin(ReflexioBase):
467
470
  if status_filter is None:
468
471
  status_filter = [None] # Default to current profiles
469
472
  profiles = self._get_storage().get_all_profiles(
470
- limit=limit, status_filter=status_filter
473
+ limit=limit,
474
+ status_filter=status_filter,
475
+ user_id=user_id,
476
+ profile_id=profile_id,
477
+ query=query,
478
+ source=source,
479
+ profile_time_to_live=profile_time_to_live,
480
+ start_time=start_time,
481
+ end_time=end_time,
471
482
  )
472
483
  profiles = sorted(
473
484
  profiles, key=lambda x: x.last_modified_timestamp, reverse=True
@@ -41,9 +41,23 @@ class SearchMixin(ReflexioBase):
41
41
  request = GetAgentSuccessEvaluationResultsRequest(**request)
42
42
 
43
43
  try:
44
- results = self._get_storage().get_agent_success_evaluation_results(
45
- limit=request.limit or 100, agent_version=request.agent_version
46
- )
44
+ if request.start_time or request.end_time:
45
+ results = self._get_storage().get_agent_success_evaluation_results_in_window(
46
+ from_ts=(
47
+ int(request.start_time.timestamp()) if request.start_time else 0
48
+ ),
49
+ to_ts=(
50
+ int(request.end_time.timestamp())
51
+ if request.end_time
52
+ else 2**31 - 1
53
+ ),
54
+ limit=request.limit or 100,
55
+ agent_version=request.agent_version,
56
+ )
57
+ else:
58
+ results = self._get_storage().get_agent_success_evaluation_results(
59
+ limit=request.limit or 100, agent_version=request.agent_version
60
+ )
47
61
  return GetAgentSuccessEvaluationResultsResponse(
48
62
  success=True,
49
63
  agent_success_evaluation_results=results,
@@ -79,6 +93,7 @@ class SearchMixin(ReflexioBase):
79
93
  user_id=request.user_id,
80
94
  request_id=request.request_id,
81
95
  session_id=request.session_id,
96
+ source=request.source,
82
97
  start_time=(
83
98
  int(request.start_time.timestamp()) if request.start_time else None
84
99
  ),
@@ -104,10 +119,11 @@ class SearchMixin(ReflexioBase):
104
119
  Session(session_id=group_name, requests=request_data_list)
105
120
  )
106
121
 
107
- # Determine has_more: count total requests returned across all groups
122
+ # Pagination is session-based (top_k counts sessions): there may be
123
+ # more pages when this page filled the session limit.
108
124
  total_returned = sum(len(s.requests) for s in sessions)
109
125
  effective_limit = request.top_k or 100
110
- has_more = total_returned >= effective_limit
126
+ has_more = len(sessions) >= effective_limit
111
127
 
112
128
  return GetRequestsResponse(
113
129
  success=True,
@@ -162,6 +178,7 @@ class SearchMixin(ReflexioBase):
162
178
  storage = self._get_storage()
163
179
  prompt_manager = self.request_context.prompt_manager
164
180
  retrieval_floor = config.retrieval_floor if config else None
181
+ recency = getattr(storage, "recency", None)
165
182
 
166
183
  from reflexio.server.services.unified_search_service import run_unified_search
167
184
 
@@ -173,4 +190,5 @@ class SearchMixin(ReflexioBase):
173
190
  prompt_manager=prompt_manager,
174
191
  pre_retrieval_model_name=pre_retrieval_model_name,
175
192
  retrieval_floor=retrieval_floor,
193
+ recency=recency,
176
194
  )
@@ -53,10 +53,19 @@ class UserPlaybookMixin(ReflexioBase):
53
53
  try:
54
54
  user_playbooks = self._get_storage().get_user_playbooks(
55
55
  limit=request.limit or 100,
56
+ user_playbook_id=request.user_playbook_id,
56
57
  user_id=request.user_id,
58
+ request_id=request.request_id,
59
+ query=request.query,
57
60
  playbook_name=request.playbook_name,
58
61
  agent_version=request.agent_version,
59
62
  status_filter=request.status_filter,
63
+ start_time=(
64
+ int(request.start_time.timestamp()) if request.start_time else None
65
+ ),
66
+ end_time=(
67
+ int(request.end_time.timestamp()) if request.end_time else None
68
+ ),
60
69
  tags=request.tags,
61
70
  )
62
71
  return GetUserPlaybooksResponse(
@@ -1,3 +1,4 @@
1
1
  from ..common import * # noqa: F401, F403
2
2
  from .entities import * # noqa: F401, F403
3
3
  from .enums import * # noqa: F401, F403
4
+ from .governance import * # noqa: F401, F403
@@ -0,0 +1,117 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ AuditActorType = Literal["api_token", "jwt", "system"]
9
+ AuditOperation = Literal["READ", "EXPORT", "ERASE", "CREATE", "UPDATE", "DELETE"]
10
+ AuditEntityType = Literal[
11
+ "profile",
12
+ "user_playbook",
13
+ "agent_playbook",
14
+ "interaction",
15
+ "request",
16
+ "session",
17
+ "agent_success_evaluation_result",
18
+ "playbook_retrieval_log",
19
+ "org",
20
+ ]
21
+ AuditStatus = Literal["ok", "error"]
22
+ PurgeOperationType = Literal["user_erasure", "org_purge"]
23
+ PurgeScopeType = Literal["user", "org"]
24
+ PurgeStatus = Literal["pending", "running", "failed", "complete"]
25
+ PurgeTargetStatus = Literal["pending", "running", "failed", "complete"]
26
+ SubjectBarrierStatus = Literal["erasing", "erased", "failed"]
27
+
28
+ __all__ = [
29
+ "AuditActorType",
30
+ "AuditOperation",
31
+ "AuditEntityType",
32
+ "AuditStatus",
33
+ "PurgeOperationType",
34
+ "PurgeScopeType",
35
+ "PurgeStatus",
36
+ "PurgeTargetStatus",
37
+ "SubjectBarrierStatus",
38
+ "AuditEvent",
39
+ "PurgeOperation",
40
+ "PurgeOperationTarget",
41
+ "SubjectWriteBarrier",
42
+ "UserExportResult",
43
+ "UserEraseResult",
44
+ ]
45
+
46
+
47
+ def _now_epoch() -> int:
48
+ return int(datetime.now(UTC).timestamp())
49
+
50
+
51
+ class AuditEvent(BaseModel):
52
+ org_id: str
53
+ actor_type: AuditActorType = "system"
54
+ actor_ref: str | None = None
55
+ operation: AuditOperation
56
+ entity_type: AuditEntityType
57
+ entity_id: str | None = None
58
+ subject_ref: str | None = None
59
+ request_ref: str
60
+ idempotency_key: str | None = None
61
+ status: AuditStatus = "ok"
62
+ detail: dict[str, Any] | None = None
63
+ created_at: int = Field(default_factory=_now_epoch)
64
+
65
+
66
+ class PurgeOperation(BaseModel):
67
+ purge_id: str
68
+ org_id: str
69
+ operation_type: PurgeOperationType
70
+ scope_type: PurgeScopeType
71
+ subject_ref: str | None = None
72
+ request_ref: str
73
+ idempotency_key: str
74
+ status: PurgeStatus = "pending"
75
+ error_code: str | None = None
76
+ error_detail: str | None = None
77
+ created_at: int = Field(default_factory=_now_epoch)
78
+ updated_at: int = Field(default_factory=_now_epoch)
79
+ completed_at: int | None = None
80
+
81
+
82
+ class PurgeOperationTarget(BaseModel):
83
+ purge_id: str
84
+ target_name: str
85
+ target_ref: str = ""
86
+ phase: str
87
+ status: PurgeTargetStatus = "pending"
88
+ detail: dict[str, Any] | None = None
89
+ deleted_count: int = 0
90
+ error_detail: str | None = None
91
+ started_at: int | None = None
92
+ completed_at: int | None = None
93
+
94
+
95
+ class SubjectWriteBarrier(BaseModel):
96
+ org_id: str
97
+ subject_ref: str
98
+ purge_id: str
99
+ status: SubjectBarrierStatus
100
+ error_code: str | None = None
101
+ error_detail: str | None = None
102
+ created_at: int = Field(default_factory=_now_epoch)
103
+ updated_at: int = Field(default_factory=_now_epoch)
104
+
105
+
106
+ class UserExportResult(BaseModel):
107
+ subject_ref: str
108
+ export_id: str
109
+ bundle: dict[str, Any]
110
+
111
+
112
+ class UserEraseResult(BaseModel):
113
+ subject_ref: str
114
+ purge_id: str
115
+ status: PurgeStatus
116
+ deleted_counts: dict[str, int] = Field(default_factory=dict)
117
+ rebuilt_agent_playbook_ids: list[int] = Field(default_factory=list)
@@ -187,9 +187,13 @@ class GetInteractionsResponse(BaseModel):
187
187
 
188
188
  class GetUserProfilesRequest(BaseModel):
189
189
  user_id: NonEmptyStr
190
+ profile_id: str | None = None
191
+ query: str | None = None
190
192
  start_time: datetime | None = None
191
193
  end_time: datetime | None = None
192
194
  top_k: int | None = Field(default=30, gt=0)
195
+ source: str | None = None
196
+ profile_time_to_live: str | None = None
193
197
  status_filter: list[Status | None] | None = None
194
198
  tags: list[str] | None = None
195
199
 
@@ -222,12 +226,23 @@ class SetConfigResponse(BaseModel):
222
226
 
223
227
  class GetUserPlaybooksRequest(BaseModel):
224
228
  limit: int | None = Field(default=100, gt=0)
229
+ user_playbook_id: int | None = Field(default=None, gt=0)
225
230
  user_id: str | None = None
231
+ request_id: str | None = None
232
+ query: str | None = None
226
233
  playbook_name: str | None = None
227
234
  agent_version: str | None = None
235
+ start_time: datetime | None = None
236
+ end_time: datetime | None = None
228
237
  status_filter: list[Status | None] | None = None
229
238
  tags: list[str] | None = None
230
239
 
240
+ @model_validator(mode="after")
241
+ def check_time_range(self) -> Self:
242
+ """Validate that end_time is after start_time."""
243
+ TimeRangeValidatorMixin.validate_time_range(self.start_time, self.end_time)
244
+ return self
245
+
231
246
 
232
247
  class GetUserPlaybooksResponse(BaseModel):
233
248
  success: bool
@@ -237,8 +252,12 @@ class GetUserPlaybooksResponse(BaseModel):
237
252
 
238
253
  class GetAgentPlaybooksRequest(BaseModel):
239
254
  limit: int | None = Field(default=100, gt=0)
255
+ agent_playbook_id: int | None = Field(default=None, gt=0)
256
+ query: str | None = None
240
257
  playbook_name: str | None = None
241
258
  agent_version: str | None = None
259
+ start_time: datetime | None = None
260
+ end_time: datetime | None = None
242
261
  status_filter: list[Status | None] | None = None
243
262
  playbook_status_filter: PlaybookStatus | None = None
244
263
  tags: list[str] | None = None
@@ -247,6 +266,12 @@ class GetAgentPlaybooksRequest(BaseModel):
247
266
  request_id: str | None = None
248
267
  session_id: str | None = None
249
268
 
269
+ @model_validator(mode="after")
270
+ def check_time_range(self) -> Self:
271
+ """Validate that end_time is after start_time."""
272
+ TimeRangeValidatorMixin.validate_time_range(self.start_time, self.end_time)
273
+ return self
274
+
250
275
 
251
276
  class GetAgentPlaybooksResponse(BaseModel):
252
277
  success: bool
@@ -392,6 +417,14 @@ class SearchAgentPlaybookResponse(BaseModel):
392
417
  class GetAgentSuccessEvaluationResultsRequest(BaseModel):
393
418
  limit: int | None = Field(default=100, gt=0)
394
419
  agent_version: str | None = None
420
+ start_time: datetime | None = None
421
+ end_time: datetime | None = None
422
+
423
+ @model_validator(mode="after")
424
+ def check_time_range(self) -> Self:
425
+ """Validate that end_time is after start_time."""
426
+ TimeRangeValidatorMixin.validate_time_range(self.start_time, self.end_time)
427
+ return self
395
428
 
396
429
 
397
430
  class GetAgentSuccessEvaluationResultsResponse(BaseModel):
@@ -404,10 +437,18 @@ class GetRequestsRequest(BaseModel):
404
437
  user_id: str | None = None
405
438
  request_id: str | None = None
406
439
  session_id: str | None = None
440
+ source: str | None = None
407
441
  start_time: datetime | None = None
408
442
  end_time: datetime | None = None
409
- top_k: int | None = Field(default=30, gt=0)
410
- offset: int | None = Field(default=0, ge=0)
443
+ top_k: int | None = Field(
444
+ default=30,
445
+ gt=0,
446
+ description="Maximum number of sessions to return. Pagination is "
447
+ "per-session: every returned session includes all of its requests.",
448
+ )
449
+ offset: int | None = Field(
450
+ default=0, ge=0, description="Number of sessions to skip for pagination."
451
+ )
411
452
 
412
453
  @model_validator(mode="after")
413
454
  def check_time_range(self) -> Self:
@@ -497,7 +538,8 @@ class TimeSeriesDataPoint(BaseModel):
497
538
  """A single data point in a time series."""
498
539
 
499
540
  timestamp: int = Field(gt=0) # Unix timestamp
500
- value: int = Field(ge=0) # Count or metric value
541
+ value: float = Field(ge=0) # Count or metric value
542
+ count: int | None = Field(default=None, ge=0) # Optional weight for rate metrics
501
543
 
502
544
 
503
545
  class PeriodStats(BaseModel):
@@ -708,6 +708,18 @@ class LineageGCConfig(BaseModel):
708
708
  poll_interval_seconds: int = Field(default=86400, gt=0)
709
709
 
710
710
 
711
+ class GovernanceRetentionConfig(BaseModel):
712
+ """Audit-event retention policy. **Enterprise-only:** reclamation is performed
713
+ by the reflexio_ext GovernanceRetentionCapability. In an OSS-only deployment
714
+ these knobs are accepted but inert (the OSS lineage scheduler does not reclaim
715
+ audit events) and the server logs a startup warning when retention is enabled.
716
+ """
717
+
718
+ audit_events_retention_enabled: bool = False
719
+ audit_events_retention_days: int = Field(default=365, gt=0)
720
+ audit_events_delete_batch_limit: int = Field(default=500, gt=0)
721
+
722
+
711
723
  @dataclass(frozen=True)
712
724
  class EffectivePendingToolCallConfig:
713
725
  """Resolved pending-tool-call settings after applying tool overrides."""
@@ -847,6 +859,9 @@ class Config(BaseModel):
847
859
  )
848
860
  # Tombstone GC job gate (opt-in, off by default — see LineageGCConfig)
849
861
  lineage_gc: LineageGCConfig = Field(default_factory=LineageGCConfig)
862
+ governance_retention: GovernanceRetentionConfig = Field(
863
+ default_factory=GovernanceRetentionConfig
864
+ )
850
865
  # Optional non-blocking async information tools for classic extraction.
851
866
  pending_tool_call_config: PendingToolCallConfig = Field(
852
867
  default_factory=PendingToolCallConfig
@@ -905,6 +920,7 @@ class Config(BaseModel):
905
920
  "reflection_config",
906
921
  "playbook_optimizer_config",
907
922
  "lineage_gc",
923
+ "governance_retention",
908
924
  "pending_tool_call_config",
909
925
  "retrieval_floor",
910
926
  ):
@@ -6,6 +6,7 @@ Description: FastAPI backend server that processes user interactions to generate
6
6
  - [Main Entry Points](#main-entry-points)
7
7
  - [Cache](#cache)
8
8
  - [API Endpoints](#api-endpoints)
9
+ - [Extension Registry](#extension-registry)
9
10
  - [LLM Client](#llm-client)
10
11
  - [Prompts](#prompts)
11
12
  - [Site Variables](#site-variables)
@@ -34,6 +35,7 @@ Description: FastAPI backend server that processes user interactions to generate
34
35
 
35
36
  - **API**: `api.py` - FastAPI routes (only place to expose endpoints)
36
37
  - **Endpoint Helpers**: `api_endpoints/` - Bridge between routes and business logic
38
+ - **Extension Registry**: `extensions.py` - Capability and service registry for optional OSS/enterprise integrations
37
39
  - **Core Service**: `services/generation_service.py` - Main orchestrator
38
40
 
39
41
  ## Cache
@@ -84,6 +86,14 @@ Description: FastAPI backend server that processes user interactions to generate
84
86
 
85
87
  **Pattern**: Core route handlers call `Reflexio` through `get_reflexio(org_id)`; endpoint helper files should not instantiate `Reflexio` directly.
86
88
 
89
+ ## Extension Registry
90
+
91
+ **File**: `extensions.py`
92
+
93
+ `CapabilityRegistry` lets deployments register optional routers, startup/shutdown hooks, and cross-cutting services without hardcoding enterprise-only imports into the OSS app. `create_app()` builds the active registry, stores it on `app.state.capability_registry`, installs capability routers/startup/shutdown hooks, and exposes typed service lookup through `ServiceKey`.
94
+
95
+ **Pattern**: Optional integrations should register capabilities/services at app construction time and consume them through the registry; avoid importing enterprise implementations directly in OSS modules.
96
+
87
97
  ## LLM Client
88
98
 
89
99
  **Directory**: `llm/`
@@ -188,6 +198,7 @@ python -m reflexio.server.scripts.manage_invitation_codes list --show-used
188
198
  - **Search preparation**: `pre_retrieval/` and `unified_search_service.py` handle query reformulation, document expansion, embeddings, and cross-entity search orchestration.
189
199
  - **Optimization/integrations**: `playbook_optimizer/` and `braintrust/` run candidate playbook optimization, rollout support, and Braintrust export/sync.
190
200
  - **Lineage**: `lineage/` resolves active records across superseded chains and schedules tombstone garbage collection for profile/playbook storage.
201
+ - **Governance**: `governance/` defines subject-reference contracts and retention/barrier policy helpers used by storage and lineage paths.
191
202
  - **Persistence/config**: `storage/`, `configurator/`, and `operation_state_utils.py` provide storage abstractions, config loading, locks, bookmarks, progress, and cancellation.
192
203
 
193
204
  ### Orchestrator
@@ -478,7 +489,8 @@ Pre-computed embeddings passed to storage methods via `query_embedding` paramete
478
489
  | File | Purpose |
479
490
  |------|---------|
480
491
  | `storage_base/` | BaseStorage interface split by domain (`_profiles.py`, `_playbook.py`, `_requests.py`, `_operations.py`, `_agent_run.py`, `_lineage.py`, `_shadow_verdicts.py`, `_stall_state.py`, `_share_links.py`) |
481
- | `sqlite_storage/` | SQLite-backed implementation split across the same domains, including lineage/tombstone support in `_lineage.py` |
492
+ | `sqlite_storage/` | SQLite-backed implementation split across the same domains, including governance-aware retention/barrier handling and lineage/tombstone support in `_governance.py` / `_lineage.py` |
493
+ | `governance_validation.py` | Shared validation helpers for subject references and governance contracts before storage writes. |
482
494
  | `retention.py`, `retention_mixin.py` | Data retention and cleanup helpers |
483
495
  | `constants.py`, `error.py` | Storage constants and shared errors |
484
496
 
@@ -486,7 +498,7 @@ Pre-computed embeddings passed to storage methods via `query_embedding` paramete
486
498
 
487
499
  **Key Methods**:
488
500
  - CRUD: profiles, interactions, playbooks, results, requests, playbook aggregation change logs
489
- - `get_sessions(offset, top_k, session_id)` → `dict[str, list[RequestInteractionDataModel]]` (groups by session_id, supports offset/limit pagination)
501
+ - `get_sessions(offset, top_k, session_id)` → `dict[str, list[RequestInteractionDataModel]]` (groups by session_id; paginates per-session — `top_k`/`offset` count sessions, and each returned session includes all of its requests)
490
502
  - `get_rerun_user_ids(user_id, start_time, end_time, source, agent_version)` → `list[str]` - Get distinct user IDs matching filters for rerun workflows (pushes filtering to storage layer)
491
503
  - `get_feedbacks(status_filter, feedback_status_filter)` - Filter by playbook status and approval status
492
504
  - `save_feedbacks()` → returns `list[Feedback]` with `feedback_id` populated (callers can ignore return)