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.
- package/.claude-plugin/marketplace.json +2 -2
- package/README.md +10 -43
- package/bin/claude-smart.js +105 -0
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.coverage +0 -0
- package/plugin/README.md +4 -3
- package/plugin/dashboard/app/api/reflexio/[...path]/route.ts +4 -2
- package/plugin/dashboard/app/dashboard/page.tsx +6 -1
- package/plugin/dashboard/app/preferences/[id]/page.tsx +18 -6
- package/plugin/dashboard/app/preferences/page.tsx +32 -35
- package/plugin/dashboard/app/sessions/[sessionId]/page.tsx +16 -1
- package/plugin/dashboard/app/sessions/page.tsx +2 -0
- package/plugin/dashboard/app/skills/page.tsx +65 -50
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +17 -8
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +1 -6
- package/plugin/dashboard/components/common/host-badge.tsx +118 -0
- package/plugin/dashboard/components/common/learning-application-badge.tsx +34 -0
- package/plugin/dashboard/components/common/learnings-badge.tsx +1 -1
- package/plugin/dashboard/components/common/page-header.tsx +3 -3
- package/plugin/dashboard/lib/config-file.ts +5 -1
- package/plugin/dashboard/lib/host-attribution.ts +62 -0
- package/plugin/dashboard/lib/session-reader.ts +40 -2
- package/plugin/dashboard/lib/types.ts +7 -1
- package/plugin/pyproject.toml +1 -1
- package/plugin/scripts/_lib.sh +197 -1
- package/plugin/scripts/backend-python-runner.py +46 -0
- package/plugin/scripts/backend-service.sh +757 -119
- package/plugin/scripts/codex-hook.js +63 -225
- package/plugin/scripts/dashboard-open.sh +6 -4
- package/plugin/scripts/dashboard-service.sh +117 -136
- package/plugin/scripts/hook_entry.sh +3 -0
- package/plugin/scripts/smart-install.sh +15 -1
- package/plugin/src/claude_smart/cli.py +14 -0
- package/plugin/src/claude_smart/context_inject.py +3 -0
- package/plugin/src/claude_smart/env_config.py +4 -1
- package/plugin/src/claude_smart/events/post_tool.py +2 -1
- package/plugin/src/claude_smart/events/session_end.py +2 -1
- package/plugin/src/claude_smart/events/stop.py +3 -0
- package/plugin/src/claude_smart/events/user_prompt.py +2 -1
- package/plugin/src/claude_smart/internal_call.py +5 -2
- package/plugin/src/claude_smart/optimizer_assistant.py +59 -13
- package/plugin/src/claude_smart/publish.py +59 -7
- package/plugin/src/claude_smart/reflexio_adapter.py +137 -14
- package/plugin/src/claude_smart/runtime.py +15 -6
- package/plugin/src/claude_smart/state.py +211 -52
- package/plugin/uv.lock +1 -1
- package/plugin/vendor/reflexio/.env.example +13 -0
- package/plugin/vendor/reflexio/reflexio/README.md +7 -3
- package/plugin/vendor/reflexio/reflexio/__init__.py +12 -0
- package/plugin/vendor/reflexio/reflexio/client/client.py +126 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +10 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +28 -0
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +41 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +177 -25
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +7 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +2 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +63 -4
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +8 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/README.md +22 -4
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +18 -8
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +6 -0
- package/plugin/vendor/reflexio/reflexio/server/api.py +66 -3
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +263 -3
- package/plugin/vendor/reflexio/reflexio/server/callback_executor.py +164 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_embedding.py +81 -81
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_subprocess.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/_litellm_text_generation.py +62 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -52
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +28 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +9 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedder_warmup.py +329 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +85 -10
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/local_embedding_provider.py +20 -5
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/nomic_embedding_provider.py +77 -9
- package/plugin/vendor/reflexio/reflexio/server/org_fanout.py +184 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/document_expansion/v1.1.0.prompt.md +32 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.4.0.prompt.md +63 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/query_reformulation/v2.0.0.prompt.md +30 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_impact/v1.0.0.prompt.md +51 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/retrieved_learning_relevance/v1.0.0.prompt.md +39 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.0.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/shadow_comparison/v1.1.0.prompt.md +43 -0
- package/plugin/vendor/reflexio/reflexio/server/routes/config.py +3 -3
- package/plugin/vendor/reflexio/reflexio/server/routes/evaluation.py +122 -28
- package/plugin/vendor/reflexio/reflexio/server/routes/system.py +22 -3
- package/plugin/vendor/reflexio/reflexio/server/scheduling.py +64 -3
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +6 -4
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/_eval_health.py +41 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/retrieved_learning_evaluator.py +554 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +35 -1
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/runner.py +253 -101
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/scheduler.py +5 -7
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/service.py +27 -0
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_extraction_lifecycle.py +11 -5
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_should_run.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation/_usage_billing.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +255 -74
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +72 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deferred_learning_plan.py +270 -0
- package/plugin/vendor/reflexio/reflexio/server/services/durable_learning/worker.py +85 -16
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/hero_state.py +2 -11
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/rule_attribution.py +6 -2
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +17 -13
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/agent_run_records.py +18 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/prior_answer_search.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +66 -0
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +510 -63
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +2 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +179 -99
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/vector_backfill_sweep.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +66 -27
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_trigger.py +177 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +68 -2
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/consolidator.py +360 -49
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/extractor.py +27 -30
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +8 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +137 -69
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +20 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/scheduler.py +13 -6
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/_query_reformulator.py +40 -25
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/consolidator.py +12 -39
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +30 -35
- package/plugin/vendor/reflexio/reflexio/server/services/profile/service.py +122 -44
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/service.py +457 -107
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/session_dedup.py +127 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/temporal.py +104 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/dispatcher.py +139 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/judge.py +16 -6
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/worker.py +137 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +12 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/lifecycle_filters.py +54 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +41 -3
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention_mixin.py +40 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +11 -4
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +28 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_learning_jobs.py +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/base/_deletion.py +16 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_erase_execution.py +104 -42
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/governance/_subject_barrier.py +7 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +43 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +430 -5
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +45 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_interaction_store.py +146 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/profiles/_profile_store.py +42 -7
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +9 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_learning_jobs.py +50 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +3 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +4 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/agent_run/_models.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/evaluation_state_keys.py +78 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +38 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +171 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +52 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_interaction_store.py +59 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/profiles/_profile_store.py +52 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/retrieved_learning_state.py +226 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +5 -6
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +209 -29
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +3 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +33 -0
- package/plugin/vendor/reflexio/reflexio/server/services/search/__init__.py +0 -0
|
@@ -101,6 +101,23 @@ class OperationStateManager:
|
|
|
101
101
|
parts.append(version)
|
|
102
102
|
return "::".join(parts)
|
|
103
103
|
|
|
104
|
+
def _normalize_lock_request_id(
|
|
105
|
+
self,
|
|
106
|
+
lock_request_id: str | None,
|
|
107
|
+
*,
|
|
108
|
+
request_id: str | None = None,
|
|
109
|
+
) -> str:
|
|
110
|
+
"""Accept legacy ``request_id`` callers while keeping local lock naming."""
|
|
111
|
+
if lock_request_id is not None:
|
|
112
|
+
if request_id is not None and request_id != lock_request_id:
|
|
113
|
+
raise TypeError(
|
|
114
|
+
"lock_request_id and request_id must match when both are provided"
|
|
115
|
+
)
|
|
116
|
+
return lock_request_id
|
|
117
|
+
if request_id is not None:
|
|
118
|
+
return request_id
|
|
119
|
+
raise TypeError("lock_request_id is required")
|
|
120
|
+
|
|
104
121
|
# ── Use Case 1: Progress Tracking ──
|
|
105
122
|
# (Batch operations: rerun + manual)
|
|
106
123
|
|
|
@@ -380,10 +397,11 @@ class OperationStateManager:
|
|
|
380
397
|
|
|
381
398
|
def acquire_lock(
|
|
382
399
|
self,
|
|
383
|
-
|
|
400
|
+
lock_request_id: str | None = None,
|
|
384
401
|
scope_id: str | None = None,
|
|
385
402
|
stale_seconds: int = GENERATION_STALE_LOCK_SECONDS,
|
|
386
403
|
payload: dict | None = None,
|
|
404
|
+
request_id: str | None = None,
|
|
387
405
|
) -> bool:
|
|
388
406
|
"""Atomically check and acquire in-progress lock.
|
|
389
407
|
|
|
@@ -396,7 +414,7 @@ class OperationStateManager:
|
|
|
396
414
|
no valid lock exists or the lock is stale, acquires the lock.
|
|
397
415
|
|
|
398
416
|
Args:
|
|
399
|
-
|
|
417
|
+
lock_request_id: Request ID for the current lock holder
|
|
400
418
|
scope_id: Optional scope identifier (e.g., user_id)
|
|
401
419
|
stale_seconds: Seconds after which a lock is considered stale
|
|
402
420
|
payload: Optional serialized request payload to enqueue with this
|
|
@@ -404,17 +422,22 @@ class OperationStateManager:
|
|
|
404
422
|
rerun loop to operate on the SAME interactions the original
|
|
405
423
|
publish enqueued, not whatever the bookmark currently points at
|
|
406
424
|
(R2).
|
|
425
|
+
request_id: Legacy alias for ``lock_request_id``. When both are
|
|
426
|
+
provided they must match.
|
|
407
427
|
|
|
408
428
|
Returns:
|
|
409
429
|
bool: True if lock acquired (proceed with generation), False if skipped
|
|
410
430
|
"""
|
|
431
|
+
lock_request_id = self._normalize_lock_request_id(
|
|
432
|
+
lock_request_id, request_id=request_id
|
|
433
|
+
)
|
|
411
434
|
state_key = self._lock_key(scope_id)
|
|
412
435
|
# Pass ``payload`` as a kwarg so storage backends that haven't been
|
|
413
436
|
# updated to the new signature still error loudly rather than silently
|
|
414
437
|
# dropping it on the floor.
|
|
415
438
|
result = self.storage.try_acquire_in_progress_lock(
|
|
416
439
|
state_key,
|
|
417
|
-
|
|
440
|
+
lock_request_id,
|
|
418
441
|
stale_seconds,
|
|
419
442
|
payload=payload,
|
|
420
443
|
)
|
|
@@ -423,25 +446,26 @@ class OperationStateManager:
|
|
|
423
446
|
|
|
424
447
|
if acquired:
|
|
425
448
|
logger.info(
|
|
426
|
-
"Acquired in-progress lock for %s: state_key=%s,
|
|
449
|
+
"Acquired in-progress lock for %s: state_key=%s, lock_request_id=%s",
|
|
427
450
|
self.service_name,
|
|
428
451
|
state_key,
|
|
429
|
-
|
|
452
|
+
lock_request_id,
|
|
430
453
|
)
|
|
431
454
|
else:
|
|
432
455
|
logger.info(
|
|
433
456
|
"Skipping %s - another operation is in progress (state_key=%s). "
|
|
434
|
-
"Enqueued
|
|
457
|
+
"Enqueued lock_request_id=%s for drain on release",
|
|
435
458
|
self.service_name,
|
|
436
459
|
state_key,
|
|
437
|
-
|
|
460
|
+
lock_request_id,
|
|
438
461
|
)
|
|
439
462
|
return acquired
|
|
440
463
|
|
|
441
464
|
def release_lock(
|
|
442
465
|
self,
|
|
443
|
-
|
|
466
|
+
lock_request_id: str | None = None,
|
|
444
467
|
scope_id: str | None = None,
|
|
468
|
+
request_id: str | None = None,
|
|
445
469
|
) -> str | None:
|
|
446
470
|
"""Release the in-progress lock and check if a new request came in.
|
|
447
471
|
|
|
@@ -449,12 +473,17 @@ class OperationStateManager:
|
|
|
449
473
|
the caller can re-run. Otherwise clears the lock.
|
|
450
474
|
|
|
451
475
|
Args:
|
|
452
|
-
|
|
476
|
+
lock_request_id: Request ID for the current lock holder
|
|
453
477
|
scope_id: Optional scope identifier (e.g., user_id)
|
|
478
|
+
request_id: Legacy alias for ``lock_request_id``. When both are
|
|
479
|
+
provided they must match.
|
|
454
480
|
|
|
455
481
|
Returns:
|
|
456
482
|
Optional[str]: pending_request_id if a new request needs processing, None otherwise
|
|
457
483
|
"""
|
|
484
|
+
lock_request_id = self._normalize_lock_request_id(
|
|
485
|
+
lock_request_id, request_id=request_id
|
|
486
|
+
)
|
|
458
487
|
state_key = self._lock_key(scope_id)
|
|
459
488
|
state_record = self.storage.get_operation_state(state_key)
|
|
460
489
|
|
|
@@ -473,8 +502,8 @@ class OperationStateManager:
|
|
|
473
502
|
current_request_id = state.get("current_request_id") or state.get("request_id")
|
|
474
503
|
|
|
475
504
|
# Only process if we still own the lock
|
|
476
|
-
if current_request_id ==
|
|
477
|
-
if pending_request_id and pending_request_id !=
|
|
505
|
+
if current_request_id == lock_request_id:
|
|
506
|
+
if pending_request_id and pending_request_id != lock_request_id:
|
|
478
507
|
# Another request came in, transfer ownership and signal re-run
|
|
479
508
|
self.storage.upsert_operation_state(
|
|
480
509
|
state_key,
|
|
@@ -502,10 +531,10 @@ class OperationStateManager:
|
|
|
502
531
|
},
|
|
503
532
|
)
|
|
504
533
|
logger.info(
|
|
505
|
-
"Released in-progress lock for %s: state_key=%s,
|
|
534
|
+
"Released in-progress lock for %s: state_key=%s, lock_request_id=%s",
|
|
506
535
|
self.service_name,
|
|
507
536
|
state_key,
|
|
508
|
-
|
|
537
|
+
lock_request_id,
|
|
509
538
|
)
|
|
510
539
|
|
|
511
540
|
return None
|
|
@@ -537,14 +566,18 @@ class OperationStateManager:
|
|
|
537
566
|
|
|
538
567
|
def clear_lock_if_owner(
|
|
539
568
|
self,
|
|
540
|
-
|
|
569
|
+
lock_request_id: str | None = None,
|
|
541
570
|
scope_id: str | None = None,
|
|
571
|
+
request_id: str | None = None,
|
|
542
572
|
) -> bool:
|
|
543
|
-
"""Atomically clear a lock only if ``
|
|
573
|
+
"""Atomically clear a lock only if ``lock_request_id`` still owns it."""
|
|
574
|
+
lock_request_id = self._normalize_lock_request_id(
|
|
575
|
+
lock_request_id, request_id=request_id
|
|
576
|
+
)
|
|
544
577
|
state_key = self._lock_key(scope_id)
|
|
545
578
|
cleared = self.storage.clear_in_progress_lock_if_owner(
|
|
546
579
|
state_key,
|
|
547
|
-
|
|
580
|
+
lock_request_id,
|
|
548
581
|
{
|
|
549
582
|
"in_progress": False,
|
|
550
583
|
"current_request_id": None,
|
|
@@ -554,17 +587,18 @@ class OperationStateManager:
|
|
|
554
587
|
)
|
|
555
588
|
if cleared:
|
|
556
589
|
logger.debug(
|
|
557
|
-
"Cleared in-progress lock for %s: state_key=%s,
|
|
590
|
+
"Cleared in-progress lock for %s: state_key=%s, lock_request_id=%s",
|
|
558
591
|
self.service_name,
|
|
559
592
|
state_key,
|
|
560
|
-
|
|
593
|
+
lock_request_id,
|
|
561
594
|
)
|
|
562
595
|
return cleared
|
|
563
596
|
|
|
564
597
|
def release_lock_pop_queue(
|
|
565
598
|
self,
|
|
566
|
-
|
|
599
|
+
lock_request_id: str | None = None,
|
|
567
600
|
scope_id: str | None = None,
|
|
601
|
+
request_id: str | None = None,
|
|
568
602
|
) -> dict | None:
|
|
569
603
|
"""Release the lock and pop the next queued pending request, if any.
|
|
570
604
|
|
|
@@ -579,14 +613,19 @@ class OperationStateManager:
|
|
|
579
613
|
to the original request rather than dropping it on the floor.
|
|
580
614
|
|
|
581
615
|
Args:
|
|
582
|
-
|
|
616
|
+
lock_request_id: Request ID for the current lock holder
|
|
583
617
|
scope_id: Optional scope identifier (e.g., user_id)
|
|
618
|
+
request_id: Legacy alias for ``lock_request_id``. When both are
|
|
619
|
+
provided they must match.
|
|
584
620
|
|
|
585
621
|
Returns:
|
|
586
622
|
dict | None: ``{"request_id": str, "payload": dict | None}`` for
|
|
587
623
|
the next queued request, or None if the queue is empty / we're
|
|
588
624
|
not the lock holder.
|
|
589
625
|
"""
|
|
626
|
+
lock_request_id = self._normalize_lock_request_id(
|
|
627
|
+
lock_request_id, request_id=request_id
|
|
628
|
+
)
|
|
590
629
|
state_key = self._lock_key(scope_id)
|
|
591
630
|
state_record = self.storage.get_operation_state(state_key)
|
|
592
631
|
|
|
@@ -603,7 +642,7 @@ class OperationStateManager:
|
|
|
603
642
|
current_request_id = state.get("current_request_id") or state.get("request_id")
|
|
604
643
|
|
|
605
644
|
# Only drain if we still own the lock.
|
|
606
|
-
if current_request_id !=
|
|
645
|
+
if current_request_id != lock_request_id:
|
|
607
646
|
return None
|
|
608
647
|
|
|
609
648
|
queue = list(state.get("pending_request_queue") or [])
|
|
@@ -621,7 +660,7 @@ class OperationStateManager:
|
|
|
621
660
|
# Legacy fallback: storage row lacks the queue field but the old
|
|
622
661
|
# pending_request_id slot was set by an in-flight pre-fix server.
|
|
623
662
|
legacy_pending = state.get("pending_request_id")
|
|
624
|
-
if legacy_pending and legacy_pending !=
|
|
663
|
+
if legacy_pending and legacy_pending != lock_request_id:
|
|
625
664
|
next_entry = {"request_id": legacy_pending, "payload": None}
|
|
626
665
|
|
|
627
666
|
if next_entry is None:
|
|
@@ -636,16 +675,16 @@ class OperationStateManager:
|
|
|
636
675
|
},
|
|
637
676
|
)
|
|
638
677
|
logger.info(
|
|
639
|
-
"Released in-progress lock for %s: state_key=%s,
|
|
678
|
+
"Released in-progress lock for %s: state_key=%s, lock_request_id=%s",
|
|
640
679
|
self.service_name,
|
|
641
680
|
state_key,
|
|
642
|
-
|
|
681
|
+
lock_request_id,
|
|
643
682
|
)
|
|
644
683
|
return None
|
|
645
684
|
|
|
646
|
-
# Transfer ownership to the popped request
|
|
647
|
-
#
|
|
648
|
-
#
|
|
685
|
+
# Transfer ownership to the popped request. The storage boundary still
|
|
686
|
+
# persists queued holders under legacy ``request_id`` keys, plus the
|
|
687
|
+
# mirrored ``pending_request_id`` slot during upgrade windows.
|
|
649
688
|
new_holder_request_id = next_entry["request_id"]
|
|
650
689
|
legacy_mirror = queue[0]["request_id"] if queue else None
|
|
651
690
|
self.storage.upsert_operation_state(
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import threading
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from reflexio.server.api_endpoints.request_context import RequestContext
|
|
10
|
+
from reflexio.server.extensions import get_service
|
|
11
|
+
from reflexio.server.operation_limiter import run_with_operation_limit
|
|
12
|
+
from reflexio.server.services.playbook.aggregation_prompt_processing import (
|
|
13
|
+
AGGREGATION_PROMPT_PROCESSOR,
|
|
14
|
+
)
|
|
15
|
+
from reflexio.server.services.playbook.components.aggregator import PlaybookAggregator
|
|
16
|
+
from reflexio.server.services.playbook.playbook_service_utils import (
|
|
17
|
+
PlaybookAggregatorRequest,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
AggregationTriggerStatus = Literal[
|
|
23
|
+
"queued",
|
|
24
|
+
"triggered",
|
|
25
|
+
"skipped_no_config",
|
|
26
|
+
"skipped_saturated",
|
|
27
|
+
"failed",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class AggregationTriggerResult:
|
|
33
|
+
status: AggregationTriggerStatus
|
|
34
|
+
reason: str
|
|
35
|
+
agent_version: str
|
|
36
|
+
error_type: str | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _failed_result(
|
|
40
|
+
*,
|
|
41
|
+
exc: Exception,
|
|
42
|
+
reason: str,
|
|
43
|
+
agent_version: str,
|
|
44
|
+
message: str,
|
|
45
|
+
) -> AggregationTriggerResult:
|
|
46
|
+
logger.exception(
|
|
47
|
+
"%s agent_version=%s reason=%s",
|
|
48
|
+
message,
|
|
49
|
+
agent_version,
|
|
50
|
+
reason,
|
|
51
|
+
)
|
|
52
|
+
return AggregationTriggerResult(
|
|
53
|
+
status="failed",
|
|
54
|
+
reason=reason,
|
|
55
|
+
agent_version=agent_version,
|
|
56
|
+
error_type=type(exc).__name__,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def has_user_playbook_aggregation_config(request_context: RequestContext) -> bool:
|
|
61
|
+
root_config = request_context.configurator.get_config()
|
|
62
|
+
playbook_config = getattr(root_config, "user_playbook_extractor_config", None)
|
|
63
|
+
return bool(playbook_config and playbook_config.aggregation_config)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _run_aggregation_task(
|
|
67
|
+
*,
|
|
68
|
+
request_context: RequestContext,
|
|
69
|
+
aggregator: PlaybookAggregator,
|
|
70
|
+
request: PlaybookAggregatorRequest,
|
|
71
|
+
reason: str,
|
|
72
|
+
) -> AggregationTriggerResult:
|
|
73
|
+
agent_version = request.agent_version
|
|
74
|
+
try:
|
|
75
|
+
run_with_operation_limit(
|
|
76
|
+
org_id=request_context.org_id,
|
|
77
|
+
operation="aggregation",
|
|
78
|
+
fn=lambda: aggregator.run(request),
|
|
79
|
+
)
|
|
80
|
+
except TimeoutError:
|
|
81
|
+
logger.info(
|
|
82
|
+
"Skipping background aggregation for agent_version=%s reason=%s: aggregation limiter is saturated",
|
|
83
|
+
agent_version,
|
|
84
|
+
reason,
|
|
85
|
+
)
|
|
86
|
+
return AggregationTriggerResult(
|
|
87
|
+
status="skipped_saturated",
|
|
88
|
+
reason=reason,
|
|
89
|
+
agent_version=agent_version,
|
|
90
|
+
error_type="TimeoutError",
|
|
91
|
+
)
|
|
92
|
+
except Exception as exc: # noqa: BLE001
|
|
93
|
+
return _failed_result(
|
|
94
|
+
exc=exc,
|
|
95
|
+
reason=reason,
|
|
96
|
+
agent_version=agent_version,
|
|
97
|
+
message="User playbook aggregation failed after successor commit",
|
|
98
|
+
)
|
|
99
|
+
return AggregationTriggerResult(
|
|
100
|
+
status="triggered",
|
|
101
|
+
reason=reason,
|
|
102
|
+
agent_version=agent_version,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _start_aggregation_thread(task: Callable[[], AggregationTriggerResult]) -> None:
|
|
107
|
+
thread = threading.Thread(
|
|
108
|
+
target=task,
|
|
109
|
+
name="reflexio-user-playbook-aggregation",
|
|
110
|
+
daemon=True,
|
|
111
|
+
)
|
|
112
|
+
thread.start()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def maybe_trigger_user_playbook_aggregation(
|
|
116
|
+
*,
|
|
117
|
+
request_context: RequestContext,
|
|
118
|
+
llm_client: Any,
|
|
119
|
+
agent_version: str,
|
|
120
|
+
reason: str,
|
|
121
|
+
) -> AggregationTriggerResult:
|
|
122
|
+
try:
|
|
123
|
+
if not has_user_playbook_aggregation_config(request_context):
|
|
124
|
+
return AggregationTriggerResult(
|
|
125
|
+
status="skipped_no_config",
|
|
126
|
+
reason=reason,
|
|
127
|
+
agent_version=agent_version,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
logger.info(
|
|
131
|
+
"Triggering user playbook aggregation agent_version=%s reason=%s",
|
|
132
|
+
agent_version,
|
|
133
|
+
reason,
|
|
134
|
+
)
|
|
135
|
+
aggregator_kwargs: dict[str, Any] = {}
|
|
136
|
+
aggregation_prompt_processor = get_service(AGGREGATION_PROMPT_PROCESSOR)
|
|
137
|
+
if aggregation_prompt_processor is not None:
|
|
138
|
+
aggregator_kwargs["aggregation_prompt_processor"] = (
|
|
139
|
+
aggregation_prompt_processor
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
aggregator = PlaybookAggregator(
|
|
143
|
+
llm_client=llm_client,
|
|
144
|
+
request_context=request_context,
|
|
145
|
+
agent_version=agent_version,
|
|
146
|
+
**aggregator_kwargs,
|
|
147
|
+
)
|
|
148
|
+
request = PlaybookAggregatorRequest(agent_version=agent_version)
|
|
149
|
+
try:
|
|
150
|
+
_start_aggregation_thread(
|
|
151
|
+
lambda: _run_aggregation_task(
|
|
152
|
+
request_context=request_context,
|
|
153
|
+
aggregator=aggregator,
|
|
154
|
+
request=request,
|
|
155
|
+
reason=reason,
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
except Exception as exc: # noqa: BLE001
|
|
159
|
+
return _failed_result(
|
|
160
|
+
exc=exc,
|
|
161
|
+
reason=reason,
|
|
162
|
+
agent_version=agent_version,
|
|
163
|
+
message="User playbook aggregation dispatch failed after successor commit",
|
|
164
|
+
)
|
|
165
|
+
except Exception as exc: # noqa: BLE001
|
|
166
|
+
return _failed_result(
|
|
167
|
+
exc=exc,
|
|
168
|
+
reason=reason,
|
|
169
|
+
agent_version=agent_version,
|
|
170
|
+
message="User playbook aggregation setup failed",
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return AggregationTriggerResult(
|
|
174
|
+
status="queued",
|
|
175
|
+
reason=reason,
|
|
176
|
+
agent_version=agent_version,
|
|
177
|
+
)
|
|
@@ -4,8 +4,8 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import time
|
|
6
6
|
import uuid
|
|
7
|
-
from collections.abc import Sequence
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
import numpy as np
|
|
@@ -728,6 +728,17 @@ class PlaybookAggregator:
|
|
|
728
728
|
duration_ms=int((time.perf_counter() - aggregation_start) * 1000),
|
|
729
729
|
metadata=stats,
|
|
730
730
|
)
|
|
731
|
+
self._record_learnings_generated(
|
|
732
|
+
learning_ids=[
|
|
733
|
+
str(saved.agent_playbook_id)
|
|
734
|
+
for saved in saved_playbook_list
|
|
735
|
+
if getattr(saved, "agent_playbook_id", None)
|
|
736
|
+
],
|
|
737
|
+
playbook_name=playbook_name,
|
|
738
|
+
request_id=_run_id,
|
|
739
|
+
metadata=stats,
|
|
740
|
+
total_count=len(saved_playbook_list),
|
|
741
|
+
)
|
|
731
742
|
return stats
|
|
732
743
|
|
|
733
744
|
except Exception as e:
|
|
@@ -760,6 +771,61 @@ class PlaybookAggregator:
|
|
|
760
771
|
# Re-raise the exception after restoring
|
|
761
772
|
raise
|
|
762
773
|
|
|
774
|
+
def _record_learnings_generated(
|
|
775
|
+
self,
|
|
776
|
+
*,
|
|
777
|
+
learning_ids: list[str],
|
|
778
|
+
playbook_name: str,
|
|
779
|
+
request_id: str,
|
|
780
|
+
metadata: Mapping[str, Any],
|
|
781
|
+
total_count: int | None = None,
|
|
782
|
+
) -> None:
|
|
783
|
+
"""Emit ``learnings_generated`` for a completed aggregation run.
|
|
784
|
+
|
|
785
|
+
Prefers one event per learning id (entity-backed) when every saved
|
|
786
|
+
playbook in this run carries a durable ``agent_playbook_id`` — the
|
|
787
|
+
common case, since ``save_agent_playbook_with_aggregate_event``
|
|
788
|
+
raises rather than returning a partial row. Falls back to the
|
|
789
|
+
count-based aggregate event when ``learning_ids`` is short of
|
|
790
|
+
``total_count`` (a falsy/unset id slipped through), mirroring
|
|
791
|
+
``ExtractionResumeWorker._record_finalized_learnings`` — this avoids
|
|
792
|
+
emitting a colliding ``learn:agent_playbook:0`` key. ``total_count``
|
|
793
|
+
defaults to ``len(learning_ids)`` so callers that already guarantee a
|
|
794
|
+
complete id list (e.g. existing tests) are unaffected.
|
|
795
|
+
"""
|
|
796
|
+
from reflexio.server.billing_meter import (
|
|
797
|
+
emit_learnings_generated,
|
|
798
|
+
emit_learnings_generated_records,
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
total = len(learning_ids) if total_count is None else total_count
|
|
802
|
+
if len(learning_ids) == total:
|
|
803
|
+
emit_learnings_generated_records(
|
|
804
|
+
org_id=self.request_context.org_id,
|
|
805
|
+
configurator=self.configurator,
|
|
806
|
+
learning_ids=learning_ids,
|
|
807
|
+
source="aggregation",
|
|
808
|
+
pipeline="playbook",
|
|
809
|
+
request_id=request_id,
|
|
810
|
+
agent_version=self.agent_version,
|
|
811
|
+
playbook_name=playbook_name,
|
|
812
|
+
entity_type="agent_playbook",
|
|
813
|
+
metadata=metadata,
|
|
814
|
+
)
|
|
815
|
+
return
|
|
816
|
+
emit_learnings_generated(
|
|
817
|
+
org_id=self.request_context.org_id,
|
|
818
|
+
configurator=self.configurator,
|
|
819
|
+
count=total,
|
|
820
|
+
source="aggregation",
|
|
821
|
+
pipeline="playbook",
|
|
822
|
+
request_id=request_id,
|
|
823
|
+
agent_version=self.agent_version,
|
|
824
|
+
playbook_name=playbook_name,
|
|
825
|
+
entity_type="agent_playbook",
|
|
826
|
+
metadata=metadata,
|
|
827
|
+
)
|
|
828
|
+
|
|
763
829
|
def get_clusters(
|
|
764
830
|
self,
|
|
765
831
|
user_playbooks: list[UserPlaybook],
|