claude-smart 0.2.45 → 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.
- package/.claude-plugin/marketplace.json +3 -3
- package/README.md +47 -10
- package/bin/claude-smart.js +535 -23
- package/package.json +22 -3
- package/plugin/.claude-plugin/plugin.json +4 -2
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/README.md +27 -6
- package/plugin/commands/clear-all.md +1 -0
- package/plugin/commands/dashboard.md +1 -0
- package/plugin/commands/learn.md +1 -0
- package/plugin/commands/restart.md +1 -0
- package/plugin/commands/show.md +1 -0
- package/plugin/dashboard/app/configure/env/page.tsx +40 -14
- package/plugin/dashboard/app/configure/server/page.tsx +51 -1
- package/plugin/dashboard/app/layout.tsx +20 -0
- package/plugin/dashboard/app/preferences/[id]/page.tsx +8 -8
- package/plugin/dashboard/app/skills/project/[id]/page.tsx +7 -3
- package/plugin/dashboard/app/skills/shared/[id]/page.tsx +10 -3
- package/plugin/dashboard/lib/claude-settings-file.ts +20 -10
- package/plugin/dashboard/lib/reflexio-client.ts +16 -0
- package/plugin/dashboard/lib/status.ts +10 -3
- package/plugin/dashboard/lib/types.ts +18 -6
- package/plugin/hooks/codex-hooks.json +0 -1
- package/plugin/opencode/assistant-buffer.ts +108 -0
- package/plugin/opencode/dist/assistant-buffer.js +96 -0
- package/plugin/opencode/dist/internal.js +12 -0
- package/plugin/opencode/dist/payload.js +85 -0
- package/plugin/opencode/dist/server.mjs +232 -0
- package/plugin/opencode/internal.ts +18 -0
- package/plugin/opencode/package.json +3 -0
- package/plugin/opencode/payload.ts +90 -0
- package/plugin/opencode/server.mts +251 -0
- package/plugin/opencode/tsconfig.json +13 -0
- package/plugin/pyproject.toml +25 -4
- package/plugin/scripts/_lib.sh +5 -1
- package/plugin/scripts/backend-service.sh +22 -2
- package/plugin/scripts/ensure-plugin-root.sh +4 -4
- package/plugin/scripts/hook_entry.sh +1 -1
- package/plugin/scripts/opencode-claude-compat +4 -0
- package/plugin/scripts/opencode-claude-compat.cmd +3 -0
- package/plugin/scripts/opencode-claude-compat.js +225 -0
- package/plugin/scripts/smart-install.sh +17 -9
- package/plugin/src/README.md +1 -1
- package/plugin/src/claude_smart/cli.py +498 -12
- package/plugin/src/claude_smart/context_format.py +9 -9
- package/plugin/src/claude_smart/cs_cite.py +66 -19
- package/plugin/src/claude_smart/env_config.py +12 -4
- package/plugin/src/claude_smart/events/session_start.py +26 -7
- package/plugin/src/claude_smart/events/stop.py +1 -1
- package/plugin/src/claude_smart/ids.py +1 -1
- package/plugin/src/claude_smart/reflexio_adapter.py +5 -5
- package/plugin/src/claude_smart/runtime.py +7 -1
- package/plugin/uv.lock +5 -5
- package/plugin/vendor/reflexio/.env.example +54 -30
- package/plugin/vendor/reflexio/README.md +17 -11
- package/plugin/vendor/reflexio/pyproject.toml +13 -1
- package/plugin/vendor/reflexio/reflexio/README.md +4 -1
- package/plugin/vendor/reflexio/reflexio/cli/README.md +9 -7
- package/plugin/vendor/reflexio/reflexio/cli/__main__.py +10 -1
- package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +22 -5
- package/plugin/vendor/reflexio/reflexio/cli/commands/interactions.py +34 -4
- package/plugin/vendor/reflexio/reflexio/cli/commands/profiles.py +12 -7
- package/plugin/vendor/reflexio/reflexio/cli/commands/services.py +13 -6
- package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +212 -52
- package/plugin/vendor/reflexio/reflexio/cli/commands/shortcuts.py +15 -2
- package/plugin/vendor/reflexio/reflexio/cli/env_loader.py +292 -22
- package/plugin/vendor/reflexio/reflexio/cli/log_format.py +29 -4
- package/plugin/vendor/reflexio/reflexio/cli/run_services.py +17 -8
- package/plugin/vendor/reflexio/reflexio/cli/stop_services.py +13 -10
- package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
- package/plugin/vendor/reflexio/reflexio/client/client.py +154 -13
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/README.md +135 -257
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/bin/openclaw-smart.js +13 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/npm/openclaw-smart/package.json +15 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/README.md +38 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/index.ts +151 -110
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/openclaw.plugin.json +20 -10
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/package.json +28 -6
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/pyproject.toml +42 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/_lib.sh +371 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-log-runner.sh +33 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/backend-service.sh +271 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/cli.sh +68 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/dashboard-open.sh +15 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/ensure-plugin-root.sh +84 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/hook_entry.sh +106 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/npm-cli.js +219 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/scripts/smart-install.sh +269 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/clear-all/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/dashboard/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/learn/SKILL.md +10 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/reflexio/SKILL.md +15 -44
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/restart/SKILL.md +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/skills/show/SKILL.md +8 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/cli.py +634 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_format.py +224 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/context_inject.py +79 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/after_tool_call.py +160 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/agent_end.py +187 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_prompt_build.py +69 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/before_tool_call.py +30 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_end.py +36 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/events/session_start.py +130 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/hook.py +131 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/ids.py +94 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/internal_call.py +75 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/oc_cite.py +196 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/optimizer_assistant.py +272 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/publish.py +96 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/query_compose.py +66 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/reflexio_adapter.py +336 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/runtime.py +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/stall_banner.py +61 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/src/openclaw_smart/state.py +323 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_e2e_session_loop.py +190 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_publish_to_local_reflexio_integration.py +112 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_recursion_guard_integration.py +86 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/integration/test_search_inject_integration.py +144 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_cli.py +184 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_after_tool_call.py +142 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_agent_end.py +233 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_prompt_build.py +116 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_before_tool_call.py +35 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_end.py +47 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_events_session_start.py +109 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_hook_dispatch.py +117 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_ids.py +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_internal_call.py +50 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_oc_cite.py +88 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_publish.py +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_query_compose.py +48 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_reflexio_adapter.py +188 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_runtime.py +41 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests/test_state.py +235 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/__mocks__/plugin-entry-stub.ts +6 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_npm_cli.test.ts +56 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tests-ts/test_shim_dispatch.test.ts +171 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/tsconfig.build.json +18 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{tsconfig.json → plugin/tsconfig.json} +2 -2
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/uv.lock +3835 -0
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/vitest.config.ts +18 -0
- package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +246 -10
- package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
- package/plugin/vendor/reflexio/reflexio/lib/_config.py +11 -4
- package/plugin/vendor/reflexio/reflexio/lib/_generation.py +17 -5
- package/plugin/vendor/reflexio/reflexio/lib/_lineage_parity_readers.py +187 -0
- package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +224 -21
- package/plugin/vendor/reflexio/reflexio/lib/_reflection.py +1 -1
- package/plugin/vendor/reflexio/reflexio/lib/_search.py +52 -25
- package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +11 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/entities.py +127 -38
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/enums.py +4 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/eval_overview_schema.py +64 -41
- package/plugin/vendor/reflexio/reflexio/models/api_schema/internal_schema.py +19 -0
- package/plugin/vendor/reflexio/reflexio/models/api_schema/pending_tool_call_schema.py +12 -5
- package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +97 -3
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/converters.py +5 -1
- package/plugin/vendor/reflexio/reflexio/models/api_schema/ui/entities.py +5 -1
- package/plugin/vendor/reflexio/reflexio/models/config_schema.py +104 -7
- package/plugin/vendor/reflexio/reflexio/models/structured_output.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/OVERVIEW.md +3 -3
- package/plugin/vendor/reflexio/reflexio/server/README.md +58 -33
- package/plugin/vendor/reflexio/reflexio/server/__init__.py +29 -5
- package/plugin/vendor/reflexio/reflexio/server/__main__.py +2 -2
- package/plugin/vendor/reflexio/reflexio/server/api.py +899 -103
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/precondition_checks.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +4 -2
- package/plugin/vendor/reflexio/reflexio/server/auth.py +90 -0
- package/plugin/vendor/reflexio/reflexio/server/billing_meter.py +173 -0
- package/plugin/vendor/reflexio/reflexio/server/billing_signals.py +64 -0
- package/plugin/vendor/reflexio/reflexio/server/cache/reflexio_cache.py +46 -16
- package/plugin/vendor/reflexio/reflexio/server/env_utils.py +65 -0
- package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/embedding_service.py +19 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/litellm_client.py +264 -77
- package/plugin/vendor/reflexio/reflexio/server/llm/llm_utils.py +69 -7
- package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/embedding_service_provider.py +75 -16
- package/plugin/vendor/reflexio/reflexio/server/llm/providers/openclaw_provider.py +280 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/llm/token_accounting.py +48 -0
- package/plugin/vendor/reflexio/reflexio/server/llm/tools.py +107 -17
- package/plugin/vendor/reflexio/reflexio/server/operation_limiter.py +360 -4
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.6.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/memory_reflection/v1.7.0.prompt.md +85 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.2.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_aggregation/v2.3.0.prompt.md +253 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.2.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_consolidation/v2.3.3.prompt.md +74 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.2.3.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.3.0.prompt.md +266 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +278 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.3.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context_expert/v3.4.0.prompt.md +137 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.2.0.prompt.md +1 -1
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_main/v1.3.0.prompt.md +34 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.0.0.prompt.md +5 -12
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.1.0.prompt.md +5 -12
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/profile_update_instruction_start/v1.2.0.prompt.md +155 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/tagging/v1.0.0.prompt.md +23 -0
- package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_manager.py +89 -32
- package/plugin/vendor/reflexio/reflexio/server/services/README.md +28 -11
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/README.md +22 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_constants.py +4 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/agent_success_evaluation_utils.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/components/__init__.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluator.py → components/evaluator.py} +3 -16
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/regen_jobs.py +28 -72
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{group_evaluation_runner.py → runner.py} +12 -10
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{delayed_group_evaluator.py → scheduler.py} +10 -2
- package/plugin/vendor/reflexio/reflexio/server/services/agent_success_evaluation/{agent_success_evaluation_service.py → service.py} +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/base_generation_service.py +160 -0
- package/plugin/vendor/reflexio/reflexio/server/services/configurator/base_configurator.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/deduplication_utils.py +1 -1
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/README.md +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/components/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{rule_attribution.py → components/rule_attribution.py} +10 -9
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/eval_sampler.py +15 -24
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/service.py +238 -164
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/README.md +31 -0
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/outcome.py +13 -3
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/pending_tool_call_dispatch.py +3 -2
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resumable_agent.py +32 -118
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +47 -16
- package/plugin/vendor/reflexio/reflexio/server/services/generation_service.py +105 -23
- package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
- package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/__init__.py +0 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +219 -0
- package/plugin/vendor/reflexio/reflexio/server/services/lineage/resolve.py +98 -0
- package/plugin/vendor/reflexio/reflexio/server/services/operation_state_utils.py +26 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +18 -11
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/__init__.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_aggregator.py → components/aggregator.py} +334 -178
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_consolidator.py → components/consolidator.py} +168 -62
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_extractor.py → components/extractor.py} +5 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_edit_apply.py +74 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/playbook_service_utils.py +33 -3
- package/plugin/vendor/reflexio/reflexio/server/services/playbook/{playbook_generation_service.py → service.py} +121 -19
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/README.md +10 -0
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/models.py +2 -1
- package/plugin/vendor/reflexio/reflexio/server/services/playbook_optimizer/optimizer.py +178 -30
- package/plugin/vendor/reflexio/reflexio/server/services/pre_retrieval/README.md +9 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/components/__init__.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_deduplicator.py → components/consolidator.py} +7 -6
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_extractor.py → components/extractor.py} +31 -22
- package/plugin/vendor/reflexio/reflexio/server/services/profile/profile_generation_service_utils.py +6 -13
- package/plugin/vendor/reflexio/reflexio/server/services/profile/{profile_generation_service.py → service.py} +32 -50
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/__init__.py +4 -8
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/components/__init__.py +7 -0
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/reflection_service_utils.py +12 -1
- package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_service.py → service.py} +36 -16
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
- package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +99 -6
- package/plugin/vendor/reflexio/reflexio/server/services/service_utils.py +36 -1
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/README.md +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/shadow_comparison/outcome.py +1 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +31 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/retention.py +9 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +16 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_agent_run.py +4 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +644 -197
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +106 -119
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +630 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_operations.py +38 -6
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +31 -1369
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +756 -210
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +148 -34
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_shadow_verdicts.py +23 -9
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_stall_state.py +5 -2
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +150 -23
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_agent_run.py +2 -8
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_base.py +5 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +40 -57
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_lineage.py +215 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_operations.py +20 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +4 -677
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +134 -1
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +62 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_retrieval_log.py +51 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_shadow_verdicts.py +17 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
- package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/README.md +8 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/__init__.py +1 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/service.py +200 -0
- package/plugin/vendor/reflexio/reflexio/server/services/tagging/tagging_scheduler.py +149 -0
- package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +372 -34
- package/plugin/vendor/reflexio/reflexio/server/site_var/README.md +2 -1
- package/plugin/vendor/reflexio/reflexio/server/site_var/feature_flags.py +120 -1
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/feature_flags.json +4 -0
- package/plugin/vendor/reflexio/reflexio/server/site_var/site_var_sources/search_settings.json +5 -0
- package/plugin/vendor/reflexio/reflexio/server/tracing.py +30 -0
- package/plugin/vendor/reflexio/reflexio/server/usage_metrics.py +18 -0
- package/plugin/vendor/reflexio/reflexio/test_support/llm_mock.py +61 -26
- package/plugin/vendor/reflexio/reflexio/test_support/llm_model_registry.py +41 -4
- package/scripts/setup-claude-smart.sh +8 -3
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/TESTING.md +0 -517
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/hook/handler.js +0 -473
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package-lock.json +0 -2156
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/package.json +0 -18
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/handler.ts +0 -241
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/hook/setup.ts +0 -140
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/publish.ts +0 -113
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/search.ts +0 -52
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/server.ts +0 -103
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/sqlite-buffer.ts +0 -156
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/lib/user-id.ts +0 -134
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/plugin/rules/reflexio.md +0 -24
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/publish_clawhub.sh +0 -278
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/references/HOOK.md +0 -164
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/install.sh +0 -36
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/scripts/uninstall.sh +0 -35
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/publish.test.ts +0 -27
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/search.test.ts +0 -31
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/server.test.ts +0 -42
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/setup.test.ts +0 -49
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/sqlite-buffer.test.ts +0 -91
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/tests/user-id.test.ts +0 -50
- package/plugin/vendor/reflexio/reflexio/integrations/openclaw/vitest.config.ts +0 -13
- package/plugin/vendor/reflexio/reflexio/server/_auth.py +0 -25
- package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/group_aggregation.py +0 -192
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/invariants.py +0 -303
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/plan.py +0 -138
- package/plugin/vendor/reflexio/reflexio/server/services/extraction/tools.py +0 -1125
- /package/plugin/vendor/reflexio/reflexio/integrations/{__init__.py → openclaw/plugin/src/openclaw_smart/__init__.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/integrations/openclaw/{types → plugin/types}/openclaw.d.ts +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{distribution.py → components/distribution.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{hero_state.py → components/hero_state.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/evaluation_overview/{shadow_aggregation.py → components/shadow_aggregation.py} +0 -0
- /package/plugin/vendor/reflexio/reflexio/server/services/reflection/{reflection_extractor.py → components/extractor.py} +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
import sqlite3
|
|
5
|
+
import uuid
|
|
5
6
|
from concurrent.futures import ThreadPoolExecutor
|
|
6
7
|
from typing import Any
|
|
7
8
|
|
|
@@ -24,6 +25,7 @@ from reflexio.server.llm.providers.embedding_service_provider import (
|
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
from ._base import (
|
|
28
|
+
_TOMBSTONE_STATUS_VALUES,
|
|
27
29
|
SQLiteStorageBase,
|
|
28
30
|
_build_status_sql,
|
|
29
31
|
_effective_search_mode,
|
|
@@ -37,6 +39,44 @@ from ._base import (
|
|
|
37
39
|
_true_rrf_merge,
|
|
38
40
|
_vector_rank_rows,
|
|
39
41
|
)
|
|
42
|
+
from ._lineage import _GC_ELIGIBLE_STATUSES, _append_event_stmt
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _emit_hard_delete_profile(
|
|
46
|
+
conn: sqlite3.Connection,
|
|
47
|
+
*,
|
|
48
|
+
org_id: str,
|
|
49
|
+
entity_id: str,
|
|
50
|
+
request_id: str,
|
|
51
|
+
actor: str = "api",
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Emit a single hard_delete lineage event for a profile entity."""
|
|
54
|
+
_append_event_stmt(
|
|
55
|
+
conn,
|
|
56
|
+
org_id=org_id,
|
|
57
|
+
entity_type="profile",
|
|
58
|
+
entity_id=entity_id,
|
|
59
|
+
op="hard_delete",
|
|
60
|
+
prov="wasInvalidatedBy",
|
|
61
|
+
source_ids=[],
|
|
62
|
+
actor=actor,
|
|
63
|
+
request_id=request_id,
|
|
64
|
+
reason="erasure",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _build_tags_sql(alias: str, tags: list[str] | None) -> tuple[str, list[Any]]:
|
|
69
|
+
if not tags:
|
|
70
|
+
return "", []
|
|
71
|
+
placeholders = ",".join("?" for _ in tags)
|
|
72
|
+
return (
|
|
73
|
+
f"EXISTS (SELECT 1 FROM json_each({alias}.tags) WHERE value IN ({placeholders}))",
|
|
74
|
+
list(tags),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _escape_like_pattern(value: str) -> str:
|
|
79
|
+
return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
|
40
80
|
|
|
41
81
|
|
|
42
82
|
class ProfileMixin:
|
|
@@ -45,6 +85,7 @@ class ProfileMixin:
|
|
|
45
85
|
# Type hints for instance attributes/methods provided by SQLiteStorageBase via MRO
|
|
46
86
|
_lock: Any
|
|
47
87
|
conn: sqlite3.Connection
|
|
88
|
+
org_id: str
|
|
48
89
|
_execute: Any
|
|
49
90
|
_fetchone: Any
|
|
50
91
|
_fetchall: Any
|
|
@@ -57,25 +98,93 @@ class ProfileMixin:
|
|
|
57
98
|
_fts_delete_profile: Any
|
|
58
99
|
_vec_upsert: Any
|
|
59
100
|
_vec_delete: Any
|
|
101
|
+
_delete_profile_search_rows: Any
|
|
102
|
+
_delete_in_chunks: Any
|
|
60
103
|
_has_sqlite_vec: bool
|
|
61
104
|
llm_client: Any
|
|
62
105
|
embedding_model_name: str
|
|
63
106
|
embedding_dimensions: int
|
|
107
|
+
_subject_ref_for_user_id: Any
|
|
108
|
+
_assert_subject_writable_locked: Any
|
|
64
109
|
|
|
65
110
|
# ------------------------------------------------------------------
|
|
66
111
|
# CRUD — Profiles
|
|
67
112
|
# ------------------------------------------------------------------
|
|
68
113
|
|
|
114
|
+
def _subject_ref_from_profile_row(self, row: sqlite3.Row) -> str:
|
|
115
|
+
subject_ref = row["governance_subject_ref"]
|
|
116
|
+
return (
|
|
117
|
+
str(subject_ref)
|
|
118
|
+
if subject_ref
|
|
119
|
+
else self._subject_ref_for_user_id(row["user_id"])
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def _assert_profile_writable_locked(
|
|
123
|
+
self,
|
|
124
|
+
profile_id: str,
|
|
125
|
+
*,
|
|
126
|
+
user_id: str | None = None,
|
|
127
|
+
) -> sqlite3.Row | None:
|
|
128
|
+
if user_id is None:
|
|
129
|
+
row = self.conn.execute(
|
|
130
|
+
"SELECT user_id, governance_subject_ref FROM profiles WHERE profile_id = ?",
|
|
131
|
+
(profile_id,),
|
|
132
|
+
).fetchone()
|
|
133
|
+
else:
|
|
134
|
+
row = self.conn.execute(
|
|
135
|
+
"SELECT user_id, governance_subject_ref FROM profiles WHERE profile_id = ? AND user_id = ?",
|
|
136
|
+
(profile_id, user_id),
|
|
137
|
+
).fetchone()
|
|
138
|
+
if row is None:
|
|
139
|
+
return None
|
|
140
|
+
self._assert_subject_writable_locked(self._subject_ref_from_profile_row(row))
|
|
141
|
+
return row
|
|
142
|
+
|
|
69
143
|
@SQLiteStorageBase.handle_exceptions
|
|
70
144
|
def get_all_profiles(
|
|
71
145
|
self,
|
|
72
146
|
limit: int = 100,
|
|
73
147
|
status_filter: list[Status | None] | None = None,
|
|
148
|
+
user_id: str | None = None,
|
|
149
|
+
profile_id: str | None = None,
|
|
150
|
+
query: str | None = None,
|
|
151
|
+
source: str | None = None,
|
|
152
|
+
profile_time_to_live: str | None = None,
|
|
153
|
+
start_time: int | None = None,
|
|
154
|
+
end_time: int | None = None,
|
|
74
155
|
) -> list[UserProfile]:
|
|
75
156
|
if status_filter is None:
|
|
76
157
|
status_filter = [None]
|
|
77
158
|
frag, params = _build_status_sql(status_filter)
|
|
78
|
-
|
|
159
|
+
conditions = [frag]
|
|
160
|
+
if user_id:
|
|
161
|
+
conditions.append("user_id = ?")
|
|
162
|
+
params.append(user_id)
|
|
163
|
+
if profile_id:
|
|
164
|
+
conditions.append("LOWER(profile_id) = LOWER(?)")
|
|
165
|
+
params.append(profile_id)
|
|
166
|
+
if query:
|
|
167
|
+
like = f"%{_escape_like_pattern(query.lower())}%"
|
|
168
|
+
conditions.append(
|
|
169
|
+
"(LOWER(content) LIKE ? ESCAPE '\\' OR LOWER(profile_id) LIKE ? ESCAPE '\\' OR LOWER(user_id) LIKE ? ESCAPE '\\')"
|
|
170
|
+
)
|
|
171
|
+
params.extend([like, like, like])
|
|
172
|
+
if source is not None:
|
|
173
|
+
conditions.append("source = ?")
|
|
174
|
+
params.append(source)
|
|
175
|
+
if profile_time_to_live:
|
|
176
|
+
conditions.append("profile_time_to_live = ?")
|
|
177
|
+
params.append(profile_time_to_live)
|
|
178
|
+
if start_time is not None:
|
|
179
|
+
conditions.append("last_modified_timestamp >= ?")
|
|
180
|
+
params.append(start_time)
|
|
181
|
+
if end_time is not None:
|
|
182
|
+
conditions.append("last_modified_timestamp <= ?")
|
|
183
|
+
params.append(end_time)
|
|
184
|
+
sql = (
|
|
185
|
+
f"SELECT * FROM profiles WHERE {' AND '.join(conditions)} "
|
|
186
|
+
"ORDER BY last_modified_timestamp DESC LIMIT ?"
|
|
187
|
+
)
|
|
79
188
|
params.append(limit)
|
|
80
189
|
return [_row_to_profile(r) for r in self._fetchall(sql, params)]
|
|
81
190
|
|
|
@@ -84,18 +193,54 @@ class ProfileMixin:
|
|
|
84
193
|
self,
|
|
85
194
|
user_id: str,
|
|
86
195
|
status_filter: list[Status | None] | None = None,
|
|
196
|
+
tags: list[str] | None = None,
|
|
197
|
+
profile_id: str | None = None,
|
|
198
|
+
query: str | None = None,
|
|
199
|
+
source: str | None = None,
|
|
200
|
+
profile_time_to_live: str | None = None,
|
|
201
|
+
start_time: int | None = None,
|
|
202
|
+
end_time: int | None = None,
|
|
87
203
|
) -> list[UserProfile]:
|
|
88
204
|
if status_filter is None:
|
|
89
205
|
status_filter = [None]
|
|
90
206
|
current_ts = _epoch_now()
|
|
91
207
|
frag, params = _build_status_sql(status_filter)
|
|
92
|
-
|
|
208
|
+
conditions = ["user_id = ?", "expiration_timestamp >= ?", frag]
|
|
93
209
|
all_params: list[Any] = [user_id, current_ts, *params]
|
|
210
|
+
if profile_id:
|
|
211
|
+
conditions.append("LOWER(profile_id) = LOWER(?)")
|
|
212
|
+
all_params.append(profile_id)
|
|
213
|
+
if query:
|
|
214
|
+
like = f"%{_escape_like_pattern(query.lower())}%"
|
|
215
|
+
conditions.append(
|
|
216
|
+
"(LOWER(content) LIKE ? ESCAPE '\\' OR LOWER(profile_id) LIKE ? ESCAPE '\\' OR LOWER(user_id) LIKE ? ESCAPE '\\')"
|
|
217
|
+
)
|
|
218
|
+
all_params.extend([like, like, like])
|
|
219
|
+
if source is not None:
|
|
220
|
+
conditions.append("source = ?")
|
|
221
|
+
all_params.append(source)
|
|
222
|
+
if profile_time_to_live:
|
|
223
|
+
conditions.append("profile_time_to_live = ?")
|
|
224
|
+
all_params.append(profile_time_to_live)
|
|
225
|
+
if start_time is not None:
|
|
226
|
+
conditions.append("last_modified_timestamp >= ?")
|
|
227
|
+
all_params.append(start_time)
|
|
228
|
+
if end_time is not None:
|
|
229
|
+
conditions.append("last_modified_timestamp <= ?")
|
|
230
|
+
all_params.append(end_time)
|
|
231
|
+
tag_frag, tag_params = _build_tags_sql("profiles", tags)
|
|
232
|
+
if tag_frag:
|
|
233
|
+
conditions.append(tag_frag)
|
|
234
|
+
all_params.extend(tag_params)
|
|
235
|
+
sql = f"SELECT * FROM profiles WHERE {' AND '.join(conditions)}"
|
|
94
236
|
return [_row_to_profile(r) for r in self._fetchall(sql, all_params)]
|
|
95
237
|
|
|
96
238
|
@SQLiteStorageBase.handle_exceptions
|
|
97
239
|
def add_user_profile(self, user_id: str, user_profiles: list[UserProfile]) -> None: # noqa: ARG002
|
|
98
240
|
for profile in user_profiles:
|
|
241
|
+
subject_ref = self._subject_ref_for_user_id(profile.user_id)
|
|
242
|
+
with self._lock:
|
|
243
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
99
244
|
embedding_text = "\n".join([profile.content, str(profile.custom_features)])
|
|
100
245
|
if self._should_expand_documents():
|
|
101
246
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
@@ -106,34 +251,48 @@ class ProfileMixin:
|
|
|
106
251
|
else:
|
|
107
252
|
profile.embedding = self._get_embedding(embedding_text)
|
|
108
253
|
embedding = profile.embedding
|
|
109
|
-
self.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
254
|
+
with self._lock:
|
|
255
|
+
try:
|
|
256
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
257
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
258
|
+
self.conn.execute(
|
|
259
|
+
"""INSERT OR REPLACE INTO profiles
|
|
260
|
+
(profile_id, user_id, content, last_modified_timestamp,
|
|
261
|
+
generated_from_request_id, profile_time_to_live,
|
|
262
|
+
expiration_timestamp, custom_features, embedding, source,
|
|
263
|
+
status, extractor_names, expanded_terms,
|
|
264
|
+
source_span, notes, reader_angle, tags, source_interaction_ids, created_at,
|
|
265
|
+
merged_into, superseded_by, governance_subject_ref)
|
|
266
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
267
|
+
(
|
|
268
|
+
profile.profile_id,
|
|
269
|
+
profile.user_id,
|
|
270
|
+
profile.content,
|
|
271
|
+
profile.last_modified_timestamp,
|
|
272
|
+
profile.generated_from_request_id,
|
|
273
|
+
profile.profile_time_to_live.value,
|
|
274
|
+
profile.expiration_timestamp,
|
|
275
|
+
_json_dumps(profile.custom_features),
|
|
276
|
+
_json_dumps(profile.embedding),
|
|
277
|
+
profile.source,
|
|
278
|
+
profile.status.value if profile.status else None,
|
|
279
|
+
_json_dumps(profile.extractor_names),
|
|
280
|
+
profile.expanded_terms,
|
|
281
|
+
profile.source_span,
|
|
282
|
+
profile.notes,
|
|
283
|
+
profile.reader_angle,
|
|
284
|
+
_json_dumps(profile.tags),
|
|
285
|
+
_json_dumps(profile.source_interaction_ids),
|
|
286
|
+
_iso_now(),
|
|
287
|
+
profile.merged_into,
|
|
288
|
+
profile.superseded_by,
|
|
289
|
+
subject_ref,
|
|
290
|
+
),
|
|
291
|
+
)
|
|
292
|
+
self.conn.commit()
|
|
293
|
+
except Exception:
|
|
294
|
+
self.conn.rollback()
|
|
295
|
+
raise
|
|
137
296
|
fts_parts = [profile.content or ""]
|
|
138
297
|
if profile.custom_features:
|
|
139
298
|
fts_parts.extend(str(v) for v in profile.custom_features.values() if v)
|
|
@@ -152,88 +311,188 @@ class ProfileMixin:
|
|
|
152
311
|
def update_user_profile_by_id(
|
|
153
312
|
self, user_id: str, profile_id: str, new_profile: UserProfile
|
|
154
313
|
) -> None:
|
|
155
|
-
|
|
156
|
-
row = self._fetchone(
|
|
157
|
-
"SELECT profile_id FROM profiles WHERE user_id = ? AND profile_id = ? AND expiration_timestamp >= ?",
|
|
158
|
-
(user_id, profile_id, current_ts),
|
|
159
|
-
)
|
|
160
|
-
if not row:
|
|
161
|
-
import logging
|
|
314
|
+
"""Replace a profile's content in-place and emit a revise lineage event.
|
|
162
315
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
316
|
+
Each call generates a fresh request_id so every edit is a distinct audit
|
|
317
|
+
event (not collapsed by the idempotency key). The UPDATE, lineage event,
|
|
318
|
+
FTS sync, and vec sync are all executed inside a single lock acquisition;
|
|
319
|
+
self._lock is an RLock so the inner _fts_upsert_profile/_vec_upsert calls
|
|
320
|
+
that re-acquire it are safe.
|
|
321
|
+
"""
|
|
322
|
+
current_ts = _epoch_now()
|
|
323
|
+
with self._lock:
|
|
324
|
+
row = self.conn.execute(
|
|
325
|
+
"SELECT user_id, governance_subject_ref FROM profiles WHERE user_id = ? AND profile_id = ? AND expiration_timestamp >= ?",
|
|
326
|
+
(user_id, profile_id, current_ts),
|
|
327
|
+
).fetchone()
|
|
328
|
+
if not row:
|
|
329
|
+
logger.warning("User profile not found for user id: %s", user_id)
|
|
330
|
+
return
|
|
331
|
+
self._assert_subject_writable_locked(
|
|
332
|
+
self._subject_ref_from_profile_row(row)
|
|
333
|
+
)
|
|
166
334
|
embedding = self._get_embedding(
|
|
167
335
|
"\n".join([new_profile.content, str(new_profile.custom_features)])
|
|
168
336
|
)
|
|
169
337
|
new_profile.embedding = embedding
|
|
170
|
-
self.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
338
|
+
with self._lock:
|
|
339
|
+
if (
|
|
340
|
+
self._assert_profile_writable_locked(profile_id, user_id=user_id)
|
|
341
|
+
is None
|
|
342
|
+
):
|
|
343
|
+
logger.warning("User profile not found for user id: %s", user_id)
|
|
344
|
+
return
|
|
345
|
+
cur = self.conn.execute(
|
|
346
|
+
"""UPDATE profiles SET content=?, last_modified_timestamp=?,
|
|
347
|
+
generated_from_request_id=?, profile_time_to_live=?,
|
|
348
|
+
expiration_timestamp=?, custom_features=?, embedding=?,
|
|
349
|
+
source=?, status=?, extractor_names=?, expanded_terms=?,
|
|
350
|
+
source_span=?, notes=?, reader_angle=?, tags=?, source_interaction_ids=?
|
|
351
|
+
WHERE profile_id=?""",
|
|
352
|
+
(
|
|
353
|
+
new_profile.content,
|
|
354
|
+
new_profile.last_modified_timestamp,
|
|
355
|
+
new_profile.generated_from_request_id,
|
|
356
|
+
new_profile.profile_time_to_live.value,
|
|
357
|
+
new_profile.expiration_timestamp,
|
|
358
|
+
_json_dumps(new_profile.custom_features),
|
|
359
|
+
_json_dumps(new_profile.embedding),
|
|
360
|
+
new_profile.source,
|
|
361
|
+
new_profile.status.value if new_profile.status else None,
|
|
362
|
+
_json_dumps(new_profile.extractor_names),
|
|
363
|
+
new_profile.expanded_terms,
|
|
364
|
+
new_profile.source_span,
|
|
365
|
+
new_profile.notes,
|
|
366
|
+
new_profile.reader_angle,
|
|
367
|
+
_json_dumps(new_profile.tags),
|
|
368
|
+
_json_dumps(new_profile.source_interaction_ids),
|
|
369
|
+
profile_id,
|
|
370
|
+
),
|
|
371
|
+
)
|
|
372
|
+
if cur.rowcount > 0:
|
|
373
|
+
_append_event_stmt(
|
|
374
|
+
self.conn,
|
|
375
|
+
org_id=self.org_id,
|
|
376
|
+
entity_type="profile",
|
|
377
|
+
entity_id=str(profile_id),
|
|
378
|
+
op="revise",
|
|
379
|
+
prov="wasRevisionOf",
|
|
380
|
+
source_ids=[],
|
|
381
|
+
actor="api",
|
|
382
|
+
request_id=uuid.uuid4().hex,
|
|
383
|
+
reason="in-place update",
|
|
384
|
+
)
|
|
385
|
+
self.conn.commit()
|
|
386
|
+
fts_parts = [new_profile.content or ""]
|
|
387
|
+
if new_profile.custom_features:
|
|
388
|
+
fts_parts.extend(
|
|
389
|
+
str(v) for v in new_profile.custom_features.values() if v
|
|
390
|
+
)
|
|
391
|
+
if new_profile.expanded_terms:
|
|
392
|
+
fts_parts.append(new_profile.expanded_terms)
|
|
393
|
+
self._fts_upsert_profile(profile_id, " ".join(fts_parts))
|
|
394
|
+
rowid_row = self._fetchone(
|
|
395
|
+
"SELECT rowid FROM profiles WHERE profile_id = ?", (profile_id,)
|
|
396
|
+
)
|
|
397
|
+
if rowid_row and embedding:
|
|
398
|
+
self._vec_upsert("profiles_vec", rowid_row["rowid"], embedding)
|
|
399
|
+
|
|
400
|
+
@SQLiteStorageBase.handle_exceptions
|
|
401
|
+
def update_user_profile_tags(
|
|
402
|
+
self, user_id: str, profile_id: str, tags: list[str]
|
|
403
|
+
) -> None:
|
|
404
|
+
with self._lock:
|
|
405
|
+
if (
|
|
406
|
+
self._assert_profile_writable_locked(profile_id, user_id=user_id)
|
|
407
|
+
is None
|
|
408
|
+
):
|
|
409
|
+
return
|
|
410
|
+
self.conn.execute(
|
|
411
|
+
"UPDATE profiles SET tags=? WHERE user_id=? AND profile_id=?",
|
|
412
|
+
(_json_dumps(tags), user_id, profile_id),
|
|
413
|
+
)
|
|
414
|
+
self.conn.commit()
|
|
206
415
|
|
|
207
416
|
@SQLiteStorageBase.handle_exceptions
|
|
208
417
|
def delete_user_profile(self, request: DeleteUserProfileRequest) -> None:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
)
|
|
213
|
-
self.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
418
|
+
# Atomic: fts + vec + row + lineage in ONE lock/commit to prevent rowid reuse
|
|
419
|
+
# race. profiles uses implicit (reusable) rowid keyed by TEXT PK — a cleanup
|
|
420
|
+
# running after commit could race with a concurrent INSERT reusing the freed
|
|
421
|
+
# rowid and delete the NEW profile's vec row. (#196)
|
|
422
|
+
with self._lock:
|
|
423
|
+
rowid_row = self.conn.execute(
|
|
424
|
+
"SELECT rowid FROM profiles WHERE user_id = ? AND profile_id = ?",
|
|
425
|
+
(request.user_id, request.profile_id),
|
|
426
|
+
).fetchone()
|
|
427
|
+
if rowid_row is None:
|
|
428
|
+
return
|
|
429
|
+
self.conn.execute(
|
|
430
|
+
"DELETE FROM profiles_fts WHERE profile_id = ?",
|
|
431
|
+
(request.profile_id,),
|
|
432
|
+
)
|
|
433
|
+
if self._has_sqlite_vec and rowid_row:
|
|
434
|
+
self.conn.execute(
|
|
435
|
+
"DELETE FROM profiles_vec WHERE rowid = ?",
|
|
436
|
+
(rowid_row["rowid"],),
|
|
437
|
+
)
|
|
438
|
+
cur = self.conn.execute(
|
|
439
|
+
"DELETE FROM profiles WHERE user_id = ? AND profile_id = ?",
|
|
440
|
+
(request.user_id, request.profile_id),
|
|
441
|
+
)
|
|
442
|
+
if cur.rowcount > 0:
|
|
443
|
+
_emit_hard_delete_profile(
|
|
444
|
+
self.conn,
|
|
445
|
+
org_id=self.org_id,
|
|
446
|
+
entity_id=str(request.profile_id),
|
|
447
|
+
request_id=uuid.uuid4().hex,
|
|
448
|
+
)
|
|
449
|
+
self.conn.commit()
|
|
220
450
|
|
|
221
451
|
@SQLiteStorageBase.handle_exceptions
|
|
222
452
|
def delete_all_profiles_for_user(self, user_id: str) -> None:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
453
|
+
# Atomic: fts + vec + row + lineage in ONE lock/commit — rowid reuse race
|
|
454
|
+
# prevention (see delete_user_profile comment, #196).
|
|
455
|
+
batch_request_id = uuid.uuid4().hex
|
|
456
|
+
with self._lock:
|
|
457
|
+
rows = self.conn.execute(
|
|
458
|
+
"SELECT rowid, profile_id FROM profiles WHERE user_id = ?", (user_id,)
|
|
459
|
+
).fetchall()
|
|
460
|
+
if not rows:
|
|
461
|
+
return
|
|
462
|
+
pids = [r["profile_id"] for r in rows]
|
|
463
|
+
rowids = [r["rowid"] for r in rows]
|
|
464
|
+
self._delete_in_chunks("profiles_fts", "profile_id", pids)
|
|
465
|
+
if self._has_sqlite_vec and rowids:
|
|
466
|
+
self._delete_in_chunks("profiles_vec", "rowid", rowids)
|
|
467
|
+
self.conn.execute("DELETE FROM profiles WHERE user_id = ?", (user_id,))
|
|
468
|
+
for pid in pids:
|
|
469
|
+
_emit_hard_delete_profile(
|
|
470
|
+
self.conn,
|
|
471
|
+
org_id=self.org_id,
|
|
472
|
+
entity_id=str(pid),
|
|
473
|
+
request_id=batch_request_id,
|
|
474
|
+
)
|
|
475
|
+
self.conn.commit()
|
|
232
476
|
|
|
233
477
|
@SQLiteStorageBase.handle_exceptions
|
|
234
478
|
def delete_all_profiles(self) -> None:
|
|
479
|
+
# Also wipe profiles_vec (full-wipe variant of the rowid-race fix, #196).
|
|
480
|
+
batch_request_id = uuid.uuid4().hex
|
|
235
481
|
with self._lock:
|
|
482
|
+
pids = [
|
|
483
|
+
r["profile_id"]
|
|
484
|
+
for r in self.conn.execute("SELECT profile_id FROM profiles").fetchall()
|
|
485
|
+
]
|
|
486
|
+
for pid in pids:
|
|
487
|
+
_emit_hard_delete_profile(
|
|
488
|
+
self.conn,
|
|
489
|
+
org_id=self.org_id,
|
|
490
|
+
entity_id=str(pid),
|
|
491
|
+
request_id=batch_request_id,
|
|
492
|
+
)
|
|
236
493
|
self.conn.execute("DELETE FROM profiles_fts")
|
|
494
|
+
if self._has_sqlite_vec:
|
|
495
|
+
self.conn.execute("DELETE FROM profiles_vec")
|
|
237
496
|
self.conn.execute("DELETE FROM profiles")
|
|
238
497
|
self.conn.commit()
|
|
239
498
|
|
|
@@ -251,25 +510,64 @@ class ProfileMixin:
|
|
|
251
510
|
) -> int:
|
|
252
511
|
new_val = new_status.value if new_status else None
|
|
253
512
|
now_ts = _epoch_now()
|
|
254
|
-
|
|
513
|
+
old_val_str = old_status.value if old_status else "None"
|
|
514
|
+
new_val_str = new_status.value if new_status else "None"
|
|
515
|
+
reason = f"{old_val_str}->{new_val_str}"
|
|
255
516
|
|
|
256
517
|
if old_status is None or (
|
|
257
518
|
hasattr(old_status, "value") and old_status.value is None
|
|
258
519
|
):
|
|
259
520
|
where = "status IS NULL"
|
|
521
|
+
select_params: list[Any] = []
|
|
260
522
|
else:
|
|
261
523
|
where = "status = ?"
|
|
262
|
-
|
|
524
|
+
select_params = [old_status.value]
|
|
263
525
|
|
|
526
|
+
extra_params: list[Any] = []
|
|
264
527
|
if user_ids is not None:
|
|
265
528
|
placeholders = ",".join("?" for _ in user_ids)
|
|
266
529
|
where += f" AND user_id IN ({placeholders})"
|
|
267
|
-
|
|
530
|
+
extra_params.extend(user_ids)
|
|
268
531
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
)
|
|
532
|
+
# Set retired_at = now when transitioning to a GC-eligible status; clear to NULL otherwise.
|
|
533
|
+
retired_at_val = now_ts if new_val in _GC_ELIGIBLE_STATUSES else None
|
|
534
|
+
|
|
535
|
+
batch_request_id = uuid.uuid4().hex
|
|
536
|
+
with self._lock:
|
|
537
|
+
affected = list(
|
|
538
|
+
self.conn.execute(
|
|
539
|
+
f"SELECT profile_id, user_id, governance_subject_ref FROM profiles WHERE {where}",
|
|
540
|
+
select_params + extra_params,
|
|
541
|
+
).fetchall()
|
|
542
|
+
)
|
|
543
|
+
for row in affected:
|
|
544
|
+
self._assert_subject_writable_locked(
|
|
545
|
+
self._subject_ref_from_profile_row(row)
|
|
546
|
+
)
|
|
547
|
+
cur = self.conn.execute(
|
|
548
|
+
f"UPDATE profiles SET status = ?, last_modified_timestamp = ?, retired_at = ? WHERE {where}",
|
|
549
|
+
[new_val, now_ts, retired_at_val] + select_params + extra_params,
|
|
550
|
+
)
|
|
551
|
+
from_val = old_status.value if old_status else None
|
|
552
|
+
to_val = new_status.value if new_status else None
|
|
553
|
+
for row in affected:
|
|
554
|
+
pid = row["profile_id"]
|
|
555
|
+
_append_event_stmt(
|
|
556
|
+
self.conn,
|
|
557
|
+
org_id=self.org_id,
|
|
558
|
+
entity_type="profile",
|
|
559
|
+
entity_id=str(pid),
|
|
560
|
+
op="status_change",
|
|
561
|
+
prov="wasInvalidatedBy",
|
|
562
|
+
source_ids=[],
|
|
563
|
+
actor="api",
|
|
564
|
+
request_id=batch_request_id,
|
|
565
|
+
reason=reason,
|
|
566
|
+
from_status=from_val,
|
|
567
|
+
to_status=to_val,
|
|
568
|
+
status_namespace="lifecycle_status",
|
|
569
|
+
)
|
|
570
|
+
self.conn.commit()
|
|
273
571
|
return cur.rowcount
|
|
274
572
|
|
|
275
573
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -295,26 +593,214 @@ class ProfileMixin:
|
|
|
295
593
|
return [_row_to_profile(r) for r in self._fetchall(sql, params)]
|
|
296
594
|
|
|
297
595
|
@SQLiteStorageBase.handle_exceptions
|
|
298
|
-
def
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
596
|
+
def get_profile_by_id(
|
|
597
|
+
self, profile_id: str, *, include_tombstones: bool = False
|
|
598
|
+
) -> UserProfile | None:
|
|
599
|
+
"""Fetch a single profile by primary key.
|
|
600
|
+
|
|
601
|
+
Args:
|
|
602
|
+
profile_id: The profile's primary key.
|
|
603
|
+
include_tombstones: When False (default), MERGED/SUPERSEDED profiles
|
|
604
|
+
return None. Set to True for lineage resolution (resolve_current).
|
|
605
|
+
|
|
606
|
+
Returns:
|
|
607
|
+
The UserProfile if found and not filtered, otherwise None.
|
|
608
|
+
"""
|
|
609
|
+
sql = "SELECT * FROM profiles WHERE profile_id = ?"
|
|
610
|
+
if not include_tombstones:
|
|
611
|
+
sql += " AND (status IS NULL OR status NOT IN (?, ?))"
|
|
612
|
+
row = self._fetchone(sql, (profile_id, *_TOMBSTONE_STATUS_VALUES))
|
|
613
|
+
else:
|
|
614
|
+
row = self._fetchone(sql, (profile_id,))
|
|
615
|
+
return _row_to_profile(row) if row else None
|
|
616
|
+
|
|
617
|
+
@SQLiteStorageBase.handle_exceptions
|
|
618
|
+
def get_distinct_generated_from_request_ids(self) -> list[str]:
|
|
619
|
+
"""Return DISTINCT non-empty generated_from_request_id values, including tombstones.
|
|
620
|
+
|
|
621
|
+
Returns:
|
|
622
|
+
list[str]: Distinct non-empty ``generated_from_request_id`` values.
|
|
623
|
+
"""
|
|
624
|
+
rows = self._fetchall(
|
|
625
|
+
"SELECT DISTINCT generated_from_request_id FROM profiles"
|
|
626
|
+
" WHERE generated_from_request_id IS NOT NULL"
|
|
627
|
+
" AND generated_from_request_id != ''",
|
|
628
|
+
(),
|
|
303
629
|
)
|
|
630
|
+
return [row[0] for row in rows]
|
|
631
|
+
|
|
632
|
+
@SQLiteStorageBase.handle_exceptions
|
|
633
|
+
def get_profiles_by_generated_from_request_id(
|
|
634
|
+
self,
|
|
635
|
+
request_id: str,
|
|
636
|
+
) -> list[UserProfile]:
|
|
637
|
+
"""Return all profiles for a generated_from_request_id, including tombstones.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
request_id (str): The generated_from_request_id to filter on.
|
|
641
|
+
|
|
642
|
+
Returns:
|
|
643
|
+
list[UserProfile]: All matching profiles (any status).
|
|
644
|
+
"""
|
|
645
|
+
rows = self._fetchall(
|
|
646
|
+
"SELECT * FROM profiles WHERE generated_from_request_id = ?",
|
|
647
|
+
(request_id,),
|
|
648
|
+
)
|
|
649
|
+
return [_row_to_profile(r) for r in rows]
|
|
650
|
+
|
|
651
|
+
def get_all_generated_profiles(self) -> list[UserProfile]:
|
|
652
|
+
"""All profiles (any status) with a non-empty generated_from_request_id."""
|
|
653
|
+
rows = self._fetchall(
|
|
654
|
+
"SELECT * FROM profiles "
|
|
655
|
+
"WHERE generated_from_request_id IS NOT NULL "
|
|
656
|
+
"AND generated_from_request_id <> ''",
|
|
657
|
+
)
|
|
658
|
+
return [_row_to_profile(r) for r in rows]
|
|
659
|
+
|
|
660
|
+
@SQLiteStorageBase.handle_exceptions
|
|
661
|
+
def archive_profile_by_id(self, user_id: str, profile_id: str) -> bool:
|
|
662
|
+
with self._lock:
|
|
663
|
+
if (
|
|
664
|
+
self._assert_profile_writable_locked(profile_id, user_id=user_id)
|
|
665
|
+
is None
|
|
666
|
+
):
|
|
667
|
+
return False
|
|
668
|
+
now_ts = _epoch_now()
|
|
669
|
+
cur = self.conn.execute(
|
|
670
|
+
"UPDATE profiles SET status = ?, last_modified_timestamp = ?, retired_at = ? "
|
|
671
|
+
"WHERE profile_id = ? AND user_id = ? AND status IS NULL",
|
|
672
|
+
(Status.ARCHIVED.value, now_ts, now_ts, profile_id, user_id),
|
|
673
|
+
)
|
|
674
|
+
if cur.rowcount > 0:
|
|
675
|
+
_append_event_stmt(
|
|
676
|
+
self.conn,
|
|
677
|
+
org_id=self.org_id,
|
|
678
|
+
entity_type="profile",
|
|
679
|
+
entity_id=str(profile_id),
|
|
680
|
+
op="status_change",
|
|
681
|
+
prov="wasInvalidatedBy",
|
|
682
|
+
source_ids=[],
|
|
683
|
+
actor="api",
|
|
684
|
+
request_id=uuid.uuid4().hex,
|
|
685
|
+
reason="None->archived",
|
|
686
|
+
from_status=None,
|
|
687
|
+
to_status="archived",
|
|
688
|
+
status_namespace="lifecycle_status",
|
|
689
|
+
)
|
|
690
|
+
self.conn.commit()
|
|
304
691
|
return cur.rowcount > 0
|
|
305
692
|
|
|
693
|
+
@SQLiteStorageBase.handle_exceptions
|
|
694
|
+
def supersede_profiles_by_ids(
|
|
695
|
+
self,
|
|
696
|
+
user_id: str,
|
|
697
|
+
profile_ids: list[str],
|
|
698
|
+
request_id: str,
|
|
699
|
+
) -> list[str]:
|
|
700
|
+
"""Soft-delete profiles by setting status to SUPERSEDED, emitting set-based lineage.
|
|
701
|
+
|
|
702
|
+
For each matching id (user_id scoped, currently CURRENT), updates status to
|
|
703
|
+
SUPERSEDED and emits one ``status_change`` event under the shared ``request_id``.
|
|
704
|
+
Atomic: one ``conn.commit()`` at the end, guarded on rowcount per id.
|
|
705
|
+
FTS/vec rows are NOT removed — reads exclude tombstones by status filter.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
user_id (str): Owning user id.
|
|
709
|
+
profile_ids (list[str]): Profile ids to supersede.
|
|
710
|
+
request_id (str): Shared request id for all emitted lineage events.
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
list[str]: The profile ids actually superseded by this call, in input order
|
|
714
|
+
(already-superseded or absent ids are omitted).
|
|
715
|
+
"""
|
|
716
|
+
if not profile_ids:
|
|
717
|
+
return []
|
|
718
|
+
if not request_id:
|
|
719
|
+
raise ValueError("request_id must be non-empty for supersede")
|
|
720
|
+
now_ts = _epoch_now()
|
|
721
|
+
# Eligibility: CURRENT (NULL) or PENDING — the two live statuses dedup can target.
|
|
722
|
+
eligible = (None, Status.PENDING.value)
|
|
723
|
+
committed_ids: list[str] = []
|
|
724
|
+
with self._lock:
|
|
725
|
+
for pid in profile_ids:
|
|
726
|
+
# Read current status for from_status derivation (user_id scoped)
|
|
727
|
+
row = self.conn.execute(
|
|
728
|
+
"SELECT status, user_id, governance_subject_ref FROM profiles WHERE profile_id = ? AND user_id = ?",
|
|
729
|
+
(pid, user_id),
|
|
730
|
+
).fetchone()
|
|
731
|
+
if row is None:
|
|
732
|
+
continue
|
|
733
|
+
self._assert_subject_writable_locked(
|
|
734
|
+
self._subject_ref_from_profile_row(row)
|
|
735
|
+
)
|
|
736
|
+
old_status_val = (
|
|
737
|
+
row[0] if isinstance(row, (tuple, list)) else row["status"]
|
|
738
|
+
)
|
|
739
|
+
if old_status_val not in eligible:
|
|
740
|
+
continue
|
|
741
|
+
cur = self.conn.execute(
|
|
742
|
+
"UPDATE profiles SET status = ?, last_modified_timestamp = ?, retired_at = ? "
|
|
743
|
+
"WHERE profile_id = ? AND user_id = ? "
|
|
744
|
+
"AND (status IS NULL OR status = ?)",
|
|
745
|
+
(
|
|
746
|
+
Status.SUPERSEDED.value,
|
|
747
|
+
now_ts,
|
|
748
|
+
now_ts,
|
|
749
|
+
pid,
|
|
750
|
+
user_id,
|
|
751
|
+
Status.PENDING.value,
|
|
752
|
+
),
|
|
753
|
+
)
|
|
754
|
+
if cur.rowcount > 0:
|
|
755
|
+
_append_event_stmt(
|
|
756
|
+
self.conn,
|
|
757
|
+
org_id=self.org_id,
|
|
758
|
+
entity_type="profile",
|
|
759
|
+
entity_id=str(pid),
|
|
760
|
+
op="status_change",
|
|
761
|
+
prov="wasInvalidatedBy",
|
|
762
|
+
source_ids=[],
|
|
763
|
+
actor="dedup",
|
|
764
|
+
request_id=request_id,
|
|
765
|
+
reason=f"{old_status_val}->superseded",
|
|
766
|
+
from_status=old_status_val,
|
|
767
|
+
to_status=Status.SUPERSEDED.value,
|
|
768
|
+
status_namespace="lifecycle_status",
|
|
769
|
+
)
|
|
770
|
+
committed_ids.append(pid)
|
|
771
|
+
self.conn.commit()
|
|
772
|
+
return committed_ids
|
|
773
|
+
|
|
306
774
|
@SQLiteStorageBase.handle_exceptions
|
|
307
775
|
def delete_all_profiles_by_status(self, status: Status) -> int:
|
|
308
|
-
#
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
776
|
+
# Atomic: fts + vec + row + lineage in ONE lock/commit — rowid reuse race
|
|
777
|
+
# prevention (see delete_user_profile comment, #196).
|
|
778
|
+
batch_request_id = uuid.uuid4().hex
|
|
779
|
+
with self._lock:
|
|
780
|
+
rows = self.conn.execute(
|
|
781
|
+
"SELECT rowid, profile_id FROM profiles WHERE status = ?",
|
|
782
|
+
(status.value,),
|
|
783
|
+
).fetchall()
|
|
784
|
+
if not rows:
|
|
785
|
+
return 0
|
|
786
|
+
pids = [r["profile_id"] for r in rows]
|
|
787
|
+
rowids = [r["rowid"] for r in rows]
|
|
788
|
+
self._delete_in_chunks("profiles_fts", "profile_id", pids)
|
|
789
|
+
if self._has_sqlite_vec and rowids:
|
|
790
|
+
self._delete_in_chunks("profiles_vec", "rowid", rowids)
|
|
791
|
+
ph = ",".join("?" for _ in pids)
|
|
792
|
+
cur = self.conn.execute(
|
|
793
|
+
f"DELETE FROM profiles WHERE profile_id IN ({ph})",
|
|
794
|
+
pids, # noqa: S608
|
|
313
795
|
)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
796
|
+
for pid in pids:
|
|
797
|
+
_emit_hard_delete_profile(
|
|
798
|
+
self.conn,
|
|
799
|
+
org_id=self.org_id,
|
|
800
|
+
entity_id=str(pid),
|
|
801
|
+
request_id=batch_request_id,
|
|
802
|
+
)
|
|
803
|
+
self.conn.commit()
|
|
318
804
|
return cur.rowcount
|
|
319
805
|
|
|
320
806
|
@SQLiteStorageBase.handle_exceptions
|
|
@@ -331,15 +817,41 @@ class ProfileMixin:
|
|
|
331
817
|
return [r["user_id"] for r in rows]
|
|
332
818
|
|
|
333
819
|
@SQLiteStorageBase.handle_exceptions
|
|
334
|
-
def delete_profiles_by_ids(
|
|
820
|
+
def delete_profiles_by_ids(
|
|
821
|
+
self, profile_ids: list[str], *, emit_hard_delete: bool = True
|
|
822
|
+
) -> int:
|
|
335
823
|
if not profile_ids:
|
|
336
824
|
return 0
|
|
337
|
-
|
|
338
|
-
|
|
825
|
+
# Atomic: fts + vec + row + lineage in ONE lock/commit — rowid reuse race
|
|
826
|
+
# prevention (see delete_user_profile comment, #196).
|
|
339
827
|
ph = ",".join("?" for _ in profile_ids)
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
828
|
+
batch_request_id = uuid.uuid4().hex
|
|
829
|
+
with self._lock:
|
|
830
|
+
pre_rows = self.conn.execute(
|
|
831
|
+
f"SELECT rowid, profile_id FROM profiles WHERE profile_id IN ({ph})",
|
|
832
|
+
profile_ids,
|
|
833
|
+
).fetchall()
|
|
834
|
+
if not pre_rows:
|
|
835
|
+
return 0
|
|
836
|
+
existing = [r["profile_id"] for r in pre_rows]
|
|
837
|
+
rowids = [r["rowid"] for r in pre_rows]
|
|
838
|
+
self._delete_in_chunks("profiles_fts", "profile_id", existing)
|
|
839
|
+
if self._has_sqlite_vec and rowids:
|
|
840
|
+
self._delete_in_chunks("profiles_vec", "rowid", rowids)
|
|
841
|
+
cur = self.conn.execute(
|
|
842
|
+
f"DELETE FROM profiles WHERE profile_id IN ({ph})",
|
|
843
|
+
profile_ids, # noqa: S608
|
|
844
|
+
)
|
|
845
|
+
if emit_hard_delete:
|
|
846
|
+
for pid in existing:
|
|
847
|
+
_emit_hard_delete_profile(
|
|
848
|
+
self.conn,
|
|
849
|
+
org_id=self.org_id,
|
|
850
|
+
entity_id=str(pid),
|
|
851
|
+
request_id=batch_request_id,
|
|
852
|
+
actor="system",
|
|
853
|
+
)
|
|
854
|
+
self.conn.commit()
|
|
343
855
|
return cur.rowcount
|
|
344
856
|
|
|
345
857
|
# ------------------------------------------------------------------
|
|
@@ -370,60 +882,79 @@ class ProfileMixin:
|
|
|
370
882
|
|
|
371
883
|
def _insert_interaction(self, interaction: Interaction) -> int:
|
|
372
884
|
created_at_iso = _epoch_to_iso(interaction.created_at)
|
|
885
|
+
subject_ref = self._subject_ref_for_user_id(interaction.user_id)
|
|
373
886
|
with self._lock:
|
|
374
|
-
|
|
375
|
-
self.conn.execute(
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
887
|
+
try:
|
|
888
|
+
self.conn.execute("BEGIN IMMEDIATE")
|
|
889
|
+
self._assert_subject_writable_locked(subject_ref)
|
|
890
|
+
if interaction.interaction_id:
|
|
891
|
+
self.conn.execute(
|
|
892
|
+
"""INSERT OR REPLACE INTO interactions
|
|
893
|
+
(interaction_id, user_id, content, request_id, created_at,
|
|
894
|
+
role, user_action, user_action_description,
|
|
895
|
+
interacted_image_url, image_encoding, shadow_content,
|
|
896
|
+
expert_content, tools_used, citations, embedding, governance_subject_ref)
|
|
897
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
898
|
+
(
|
|
899
|
+
interaction.interaction_id,
|
|
900
|
+
interaction.user_id,
|
|
901
|
+
interaction.content,
|
|
902
|
+
interaction.request_id,
|
|
903
|
+
created_at_iso,
|
|
904
|
+
interaction.role,
|
|
905
|
+
interaction.user_action.value,
|
|
906
|
+
interaction.user_action_description,
|
|
907
|
+
interaction.interacted_image_url,
|
|
908
|
+
interaction.image_encoding,
|
|
909
|
+
interaction.shadow_content,
|
|
910
|
+
interaction.expert_content,
|
|
911
|
+
_json_dumps(
|
|
912
|
+
[t.model_dump() for t in interaction.tools_used]
|
|
913
|
+
),
|
|
914
|
+
_json_dumps(
|
|
915
|
+
[c.model_dump() for c in interaction.citations]
|
|
916
|
+
),
|
|
917
|
+
_json_dumps(interaction.embedding),
|
|
918
|
+
subject_ref,
|
|
919
|
+
),
|
|
920
|
+
)
|
|
921
|
+
iid = interaction.interaction_id
|
|
922
|
+
else:
|
|
923
|
+
cur = self.conn.execute(
|
|
924
|
+
"""INSERT INTO interactions
|
|
925
|
+
(user_id, content, request_id, created_at,
|
|
926
|
+
role, user_action, user_action_description,
|
|
927
|
+
interacted_image_url, image_encoding, shadow_content,
|
|
928
|
+
expert_content, tools_used, citations, embedding, governance_subject_ref)
|
|
929
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
|
|
930
|
+
(
|
|
931
|
+
interaction.user_id,
|
|
932
|
+
interaction.content,
|
|
933
|
+
interaction.request_id,
|
|
934
|
+
created_at_iso,
|
|
935
|
+
interaction.role,
|
|
936
|
+
interaction.user_action.value,
|
|
937
|
+
interaction.user_action_description,
|
|
938
|
+
interaction.interacted_image_url,
|
|
939
|
+
interaction.image_encoding,
|
|
940
|
+
interaction.shadow_content,
|
|
941
|
+
interaction.expert_content,
|
|
942
|
+
_json_dumps(
|
|
943
|
+
[t.model_dump() for t in interaction.tools_used]
|
|
944
|
+
),
|
|
945
|
+
_json_dumps(
|
|
946
|
+
[c.model_dump() for c in interaction.citations]
|
|
947
|
+
),
|
|
948
|
+
_json_dumps(interaction.embedding),
|
|
949
|
+
subject_ref,
|
|
950
|
+
),
|
|
951
|
+
)
|
|
952
|
+
iid = cur.lastrowid or 0
|
|
953
|
+
interaction.interaction_id = iid
|
|
954
|
+
self.conn.commit()
|
|
955
|
+
except Exception:
|
|
956
|
+
self.conn.rollback()
|
|
957
|
+
raise
|
|
427
958
|
# Update FTS and vec
|
|
428
959
|
self._fts_upsert(
|
|
429
960
|
"interactions_fts",
|
|
@@ -464,34 +995,49 @@ class ProfileMixin:
|
|
|
464
995
|
|
|
465
996
|
@SQLiteStorageBase.handle_exceptions
|
|
466
997
|
def delete_user_interaction(self, request: DeleteUserInteractionRequest) -> None:
|
|
467
|
-
self.
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
998
|
+
with self._lock:
|
|
999
|
+
row = self.conn.execute(
|
|
1000
|
+
"SELECT interaction_id FROM interactions WHERE user_id = ? AND interaction_id = ?",
|
|
1001
|
+
(request.user_id, request.interaction_id),
|
|
1002
|
+
).fetchone()
|
|
1003
|
+
if row is None:
|
|
1004
|
+
return
|
|
1005
|
+
self.conn.execute(
|
|
1006
|
+
"DELETE FROM interactions_fts WHERE rowid = ?",
|
|
1007
|
+
(request.interaction_id,),
|
|
1008
|
+
)
|
|
1009
|
+
if self._has_sqlite_vec:
|
|
1010
|
+
self.conn.execute(
|
|
1011
|
+
"DELETE FROM interactions_vec WHERE rowid = ?",
|
|
1012
|
+
(request.interaction_id,),
|
|
1013
|
+
)
|
|
1014
|
+
self.conn.execute(
|
|
1015
|
+
"DELETE FROM interactions WHERE user_id = ? AND interaction_id = ?",
|
|
1016
|
+
(request.user_id, request.interaction_id),
|
|
1017
|
+
)
|
|
1018
|
+
self.conn.commit()
|
|
472
1019
|
|
|
473
1020
|
@SQLiteStorageBase.handle_exceptions
|
|
474
1021
|
def delete_all_interactions_for_user(self, user_id: str) -> None:
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
r["interaction_id"]
|
|
478
|
-
for r in self._fetchall(
|
|
1022
|
+
with self._lock:
|
|
1023
|
+
rows = self.conn.execute(
|
|
479
1024
|
"SELECT interaction_id FROM interactions WHERE user_id = ?", (user_id,)
|
|
480
|
-
)
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
self._execute("DELETE FROM interactions WHERE user_id = ?", (user_id,))
|
|
1025
|
+
).fetchall()
|
|
1026
|
+
if not rows:
|
|
1027
|
+
return
|
|
1028
|
+
ids = [r["interaction_id"] for r in rows]
|
|
1029
|
+
self._delete_in_chunks("interactions_fts", "rowid", ids)
|
|
1030
|
+
if self._has_sqlite_vec:
|
|
1031
|
+
self._delete_in_chunks("interactions_vec", "rowid", ids)
|
|
1032
|
+
self.conn.execute("DELETE FROM interactions WHERE user_id = ?", (user_id,))
|
|
1033
|
+
self.conn.commit()
|
|
490
1034
|
|
|
491
1035
|
@SQLiteStorageBase.handle_exceptions
|
|
492
1036
|
def delete_all_interactions(self) -> None:
|
|
493
1037
|
with self._lock:
|
|
494
1038
|
self.conn.execute("DELETE FROM interactions_fts")
|
|
1039
|
+
if self._has_sqlite_vec:
|
|
1040
|
+
self.conn.execute("DELETE FROM interactions_vec")
|
|
495
1041
|
self.conn.execute("DELETE FROM interactions")
|
|
496
1042
|
self.conn.commit()
|
|
497
1043
|
|
|
@@ -504,22 +1050,18 @@ class ProfileMixin:
|
|
|
504
1050
|
def delete_oldest_interactions(self, count: int) -> int:
|
|
505
1051
|
if count <= 0:
|
|
506
1052
|
return 0
|
|
507
|
-
rows = self._fetchall(
|
|
508
|
-
"SELECT interaction_id FROM interactions ORDER BY created_at ASC LIMIT ?",
|
|
509
|
-
(count,),
|
|
510
|
-
)
|
|
511
|
-
if not rows:
|
|
512
|
-
return 0
|
|
513
|
-
ids = [r["interaction_id"] for r in rows]
|
|
514
|
-
placeholders = ",".join("?" for _ in ids)
|
|
515
1053
|
with self._lock:
|
|
516
|
-
self.conn.execute(
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
1054
|
+
rows = self.conn.execute(
|
|
1055
|
+
"SELECT interaction_id FROM interactions ORDER BY created_at ASC LIMIT ?",
|
|
1056
|
+
(count,),
|
|
1057
|
+
).fetchall()
|
|
1058
|
+
if not rows:
|
|
1059
|
+
return 0
|
|
1060
|
+
ids = [r["interaction_id"] for r in rows]
|
|
1061
|
+
self._delete_in_chunks("interactions_fts", "rowid", ids)
|
|
1062
|
+
if self._has_sqlite_vec:
|
|
1063
|
+
self._delete_in_chunks("interactions_vec", "rowid", ids)
|
|
1064
|
+
self._delete_in_chunks("interactions", "interaction_id", ids)
|
|
523
1065
|
self.conn.commit()
|
|
524
1066
|
return len(ids)
|
|
525
1067
|
|
|
@@ -657,6 +1199,10 @@ class ProfileMixin:
|
|
|
657
1199
|
frag, sparams = _build_status_sql(status_filter)
|
|
658
1200
|
conditions.append(frag)
|
|
659
1201
|
params.extend(sparams)
|
|
1202
|
+
tag_frag, tag_params = _build_tags_sql("p", req.tags)
|
|
1203
|
+
if tag_frag:
|
|
1204
|
+
conditions.append(tag_frag)
|
|
1205
|
+
params.extend(tag_params)
|
|
660
1206
|
|
|
661
1207
|
where_clause = " AND ".join(conditions)
|
|
662
1208
|
overfetch = match_count * 5 if mode != SearchMode.FTS else match_count
|