superlocalmemory 3.7.8 → 3.8.1
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 +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +129 -0
- package/README.md +205 -123
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +2 -1
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +228 -17
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +187 -0
- package/src/superlocalmemory/cli/main.py +49 -8
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +21 -7
- package/src/superlocalmemory/core/embeddings.py +131 -46
- package/src/superlocalmemory/core/engine.py +41 -22
- package/src/superlocalmemory/core/engine_ingestion.py +359 -43
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -3
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +186 -29
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +148 -3
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +263 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +32 -79
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +12 -4
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +221 -47
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +317 -70
- package/src/superlocalmemory/server/routes/behavioral.py +349 -71
- package/src/superlocalmemory/server/routes/brain.py +69 -12
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +438 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +108 -26
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +189 -68
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +289 -118
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +200 -31
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +103 -158
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +247 -89
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +827 -72
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +132 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +136 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +303 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +100 -41
- package/src/superlocalmemory/ui/js/dashboard.js +403 -65
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +871 -0
- package/src/superlocalmemory/ui/js/od-brain.js +816 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +600 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +929 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
- package/src/superlocalmemory/ui/js/od-shell.js +809 -0
- package/src/superlocalmemory/ui/js/od-skills.js +600 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +17 -3
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -23
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -16
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -283,22 +283,22 @@ class EntityResolver:
|
|
|
283
283
|
entity = self._db.get_entity_by_name(name, profile_id)
|
|
284
284
|
if entity is not None:
|
|
285
285
|
resolution[raw] = entity.entity_id
|
|
286
|
-
self._touch_last_seen(entity.entity_id)
|
|
286
|
+
self._touch_last_seen(entity.entity_id, profile_id)
|
|
287
287
|
continue
|
|
288
288
|
|
|
289
289
|
# Tier b: alias match (case-insensitive, indexed)
|
|
290
290
|
entity_id = self._alias_lookup(name, profile_id)
|
|
291
291
|
if entity_id is not None:
|
|
292
292
|
resolution[raw] = entity_id
|
|
293
|
-
self._touch_last_seen(entity_id)
|
|
293
|
+
self._touch_last_seen(entity_id, profile_id)
|
|
294
294
|
continue
|
|
295
295
|
|
|
296
296
|
# Tier c: fuzzy match via Jaro-Winkler
|
|
297
297
|
match_id, score = self._fuzzy_match(name, profile_id)
|
|
298
298
|
if match_id is not None and score >= JARO_WINKLER_AUTO_MERGE:
|
|
299
299
|
resolution[raw] = match_id
|
|
300
|
-
self._persist_alias(match_id, name, score, "jaro_winkler")
|
|
301
|
-
self._touch_last_seen(match_id)
|
|
300
|
+
self._persist_alias(match_id, name, score, "jaro_winkler", profile_id)
|
|
301
|
+
self._touch_last_seen(match_id, profile_id)
|
|
302
302
|
continue
|
|
303
303
|
|
|
304
304
|
# Candidate zone (0.70–0.85): queue for LLM in Mode B/C
|
|
@@ -384,7 +384,7 @@ class EntityResolver:
|
|
|
384
384
|
and removes the merged entity record.
|
|
385
385
|
"""
|
|
386
386
|
# Move aliases from merge -> keep
|
|
387
|
-
aliases = self._db.get_aliases_for_entity(entity_id_merge)
|
|
387
|
+
aliases = self._db.get_aliases_for_entity(entity_id_merge, profile_id)
|
|
388
388
|
for alias in aliases:
|
|
389
389
|
new_alias = EntityAlias(
|
|
390
390
|
alias_id=_new_id(),
|
|
@@ -393,7 +393,7 @@ class EntityResolver:
|
|
|
393
393
|
confidence=alias.confidence,
|
|
394
394
|
source=f"merge_from:{entity_id_merge}",
|
|
395
395
|
)
|
|
396
|
-
self._db.store_alias(new_alias)
|
|
396
|
+
self._db.store_alias(new_alias, profile_id)
|
|
397
397
|
|
|
398
398
|
# Also add the merged entity's canonical name as an alias of keep
|
|
399
399
|
merged = self._db.get_entity_by_name("", "") # placeholder
|
|
@@ -403,7 +403,7 @@ class EntityResolver:
|
|
|
403
403
|
)
|
|
404
404
|
if rows:
|
|
405
405
|
merged_name = str(dict(rows[0])["canonical_name"])
|
|
406
|
-
self._persist_alias(entity_id_keep, merged_name, 1.0, "merge")
|
|
406
|
+
self._persist_alias(entity_id_keep, merged_name, 1.0, "merge", profile_id)
|
|
407
407
|
|
|
408
408
|
# Update atomic_facts: replace entity_id_merge with entity_id_keep
|
|
409
409
|
# in canonical_entities_json column
|
|
@@ -435,14 +435,14 @@ class EntityResolver:
|
|
|
435
435
|
except (json.JSONDecodeError, TypeError):
|
|
436
436
|
continue
|
|
437
437
|
|
|
438
|
-
# Delete the merged entity
|
|
438
|
+
# Delete the merged entity (tenant-scoped)
|
|
439
439
|
self._db.execute(
|
|
440
|
-
"DELETE FROM entity_aliases WHERE entity_id = ?",
|
|
441
|
-
(entity_id_merge,),
|
|
440
|
+
"DELETE FROM entity_aliases WHERE entity_id = ? AND profile_id = ?",
|
|
441
|
+
(entity_id_merge, profile_id),
|
|
442
442
|
)
|
|
443
443
|
self._db.execute(
|
|
444
|
-
"DELETE FROM canonical_entities WHERE entity_id = ?",
|
|
445
|
-
(entity_id_merge,),
|
|
444
|
+
"DELETE FROM canonical_entities WHERE entity_id = ? AND profile_id = ?",
|
|
445
|
+
(entity_id_merge, profile_id),
|
|
446
446
|
)
|
|
447
447
|
logger.info(
|
|
448
448
|
"Merged entity %s into %s (profile=%s)",
|
|
@@ -526,7 +526,7 @@ class EntityResolver:
|
|
|
526
526
|
self._db.store_entity(entity)
|
|
527
527
|
|
|
528
528
|
# Store name as its own alias for uniform lookup
|
|
529
|
-
self._persist_alias(entity.entity_id, name, 1.0, "canonical")
|
|
529
|
+
self._persist_alias(entity.entity_id, name, 1.0, "canonical", profile_id)
|
|
530
530
|
|
|
531
531
|
logger.debug(
|
|
532
532
|
"Created entity '%s' [%s] (type=%s, profile=%s)",
|
|
@@ -540,13 +540,16 @@ class EntityResolver:
|
|
|
540
540
|
alias_text: str,
|
|
541
541
|
confidence: float,
|
|
542
542
|
source: str,
|
|
543
|
+
profile_id: str,
|
|
543
544
|
) -> None:
|
|
544
|
-
"""Store an alias, skipping duplicates."""
|
|
545
|
-
# Check if alias already exists for this entity
|
|
545
|
+
"""Store an alias under a profile, skipping duplicates within it."""
|
|
546
|
+
# Check if alias already exists for this entity IN THIS PROFILE. The
|
|
547
|
+
# dedup must be profile-scoped or one profile's alias suppresses
|
|
548
|
+
# another profile's identical alias for a same-id entity.
|
|
546
549
|
existing = self._db.execute(
|
|
547
550
|
"SELECT alias_id FROM entity_aliases "
|
|
548
|
-
"WHERE entity_id = ? AND LOWER(alias) = LOWER(?)",
|
|
549
|
-
(entity_id, alias_text),
|
|
551
|
+
"WHERE entity_id = ? AND profile_id = ? AND LOWER(alias) = LOWER(?)",
|
|
552
|
+
(entity_id, profile_id, alias_text),
|
|
550
553
|
)
|
|
551
554
|
if existing:
|
|
552
555
|
return
|
|
@@ -557,13 +560,20 @@ class EntityResolver:
|
|
|
557
560
|
confidence=confidence,
|
|
558
561
|
source=source,
|
|
559
562
|
)
|
|
560
|
-
self._db.store_alias(alias)
|
|
563
|
+
self._db.store_alias(alias, profile_id)
|
|
561
564
|
|
|
562
|
-
def _touch_last_seen(self, entity_id: str) -> None:
|
|
563
|
-
"""Update last_seen timestamp on a canonical entity.
|
|
565
|
+
def _touch_last_seen(self, entity_id: str, profile_id: str = "default") -> None:
|
|
566
|
+
"""Update last_seen timestamp on a canonical entity scoped to profile.
|
|
567
|
+
|
|
568
|
+
L-01 fix: the original query had no profile_id guard. Since entity_ids are
|
|
569
|
+
UUIDs today the blast radius is theoretical, but the guard is required for
|
|
570
|
+
defense-in-depth against future import/sharing features that could introduce
|
|
571
|
+
UUID collisions across profiles.
|
|
572
|
+
"""
|
|
564
573
|
self._db.execute(
|
|
565
|
-
"UPDATE canonical_entities SET last_seen = ?
|
|
566
|
-
|
|
574
|
+
"UPDATE canonical_entities SET last_seen = ? "
|
|
575
|
+
"WHERE entity_id = ? AND profile_id = ?",
|
|
576
|
+
(_now(), entity_id, profile_id),
|
|
567
577
|
)
|
|
568
578
|
|
|
569
579
|
# -- Internal: LLM disambiguation (Mode B/C) ---------------------------
|
|
@@ -621,9 +631,9 @@ class EntityResolver:
|
|
|
621
631
|
entity_id = known[name_str]
|
|
622
632
|
resolved[mention_str] = entity_id
|
|
623
633
|
self._persist_alias(
|
|
624
|
-
entity_id, mention_str, 0.9, "llm",
|
|
634
|
+
entity_id, mention_str, 0.9, "llm", profile_id,
|
|
625
635
|
)
|
|
626
|
-
self._touch_last_seen(entity_id)
|
|
636
|
+
self._touch_last_seen(entity_id, profile_id)
|
|
627
637
|
# If LLM says it's itself, leave for caller to create
|
|
628
638
|
return resolved
|
|
629
639
|
|
|
@@ -685,11 +685,36 @@ class FactExtractor:
|
|
|
685
685
|
temperature=0.0,
|
|
686
686
|
max_tokens=1024,
|
|
687
687
|
)
|
|
688
|
-
|
|
688
|
+
facts = self._parse_llm_response(raw, session_id, session_date)
|
|
689
|
+
return self._reflexion_refine(conversation_text, facts)
|
|
689
690
|
except Exception as exc:
|
|
690
691
|
logger.warning("LLM fact extraction failed: %s", exc)
|
|
691
692
|
return []
|
|
692
693
|
|
|
694
|
+
def _reflexion_refine(
|
|
695
|
+
self, source_text: str, facts: list[AtomicFact],
|
|
696
|
+
) -> list[AtomicFact]:
|
|
697
|
+
"""Wave Q1: bounded, fail-open entity self-review (Mode B/C only).
|
|
698
|
+
|
|
699
|
+
Reached only from the LLM extraction path, so Mode A never runs this.
|
|
700
|
+
Any failure returns the facts unchanged.
|
|
701
|
+
"""
|
|
702
|
+
if not facts or not getattr(
|
|
703
|
+
self._config, "enable_entity_reflexion", True,
|
|
704
|
+
):
|
|
705
|
+
return facts
|
|
706
|
+
try:
|
|
707
|
+
from superlocalmemory.encoding.entity_reflexion import EntityReflexion
|
|
708
|
+
|
|
709
|
+
reflexion = EntityReflexion(
|
|
710
|
+
self._llm,
|
|
711
|
+
max_facts=getattr(self._config, "reflexion_max_facts", 8),
|
|
712
|
+
)
|
|
713
|
+
return reflexion.refine(source_text, facts)
|
|
714
|
+
except Exception as exc:
|
|
715
|
+
logger.debug("Entity reflexion skipped: %s", exc)
|
|
716
|
+
return facts
|
|
717
|
+
|
|
693
718
|
def _parse_llm_response(
|
|
694
719
|
self,
|
|
695
720
|
raw: str,
|
|
@@ -96,6 +96,18 @@ class TemporalValidator:
|
|
|
96
96
|
severity = contradiction["severity"]
|
|
97
97
|
reason = contradiction["description"]
|
|
98
98
|
|
|
99
|
+
# Temporal-anchor guard: if the new fact and the old fact carry
|
|
100
|
+
# distinct explicit event anchors, they describe the same subject at
|
|
101
|
+
# different times (a valid timeline — "lived in Delhi (2020)" vs
|
|
102
|
+
# "lives in Mumbai (2024)"), NOT a contradiction. Do not supersede;
|
|
103
|
+
# both stay recallable so historical queries still find them.
|
|
104
|
+
if self._is_historical_progression(new_fact, old_fact_id):
|
|
105
|
+
logger.debug(
|
|
106
|
+
"Temporal: %s vs %s have distinct event anchors — historical "
|
|
107
|
+
"progression, not superseding", new_fact.fact_id, old_fact_id,
|
|
108
|
+
)
|
|
109
|
+
continue
|
|
110
|
+
|
|
99
111
|
# Step 1: Invalidate the old fact (bi-temporal)
|
|
100
112
|
self.invalidate_fact(
|
|
101
113
|
fact_id=old_fact_id,
|
|
@@ -119,6 +131,54 @@ class TemporalValidator:
|
|
|
119
131
|
)
|
|
120
132
|
return actions
|
|
121
133
|
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _event_anchor(
|
|
136
|
+
referenced_date: str | None,
|
|
137
|
+
observation_date: str | None,
|
|
138
|
+
interval_start: str | None,
|
|
139
|
+
) -> str | None:
|
|
140
|
+
"""The most-specific explicit event time, or None when undated."""
|
|
141
|
+
return referenced_date or observation_date or interval_start or None
|
|
142
|
+
|
|
143
|
+
def _is_historical_progression(
|
|
144
|
+
self, new_fact: AtomicFact, old_fact_id: str,
|
|
145
|
+
) -> bool:
|
|
146
|
+
"""True when new_fact and the old fact have distinct explicit event
|
|
147
|
+
anchors (same subject, different times) — a valid timeline rather than a
|
|
148
|
+
contradiction. Conservative: False unless BOTH anchors are present and
|
|
149
|
+
resolve to different calendar days, so undated facts supersede as before.
|
|
150
|
+
"""
|
|
151
|
+
new_anchor = self._event_anchor(
|
|
152
|
+
getattr(new_fact, "referenced_date", None),
|
|
153
|
+
getattr(new_fact, "observation_date", None),
|
|
154
|
+
getattr(new_fact, "interval_start", None),
|
|
155
|
+
)
|
|
156
|
+
if not new_anchor:
|
|
157
|
+
return False
|
|
158
|
+
try:
|
|
159
|
+
rows = self._db.execute(
|
|
160
|
+
"SELECT referenced_date, observation_date, interval_start "
|
|
161
|
+
"FROM atomic_facts WHERE fact_id = ?",
|
|
162
|
+
(old_fact_id,),
|
|
163
|
+
)
|
|
164
|
+
except Exception:
|
|
165
|
+
return False
|
|
166
|
+
if not rows:
|
|
167
|
+
return False
|
|
168
|
+
d = dict(rows[0])
|
|
169
|
+
old_anchor = self._event_anchor(
|
|
170
|
+
d.get("referenced_date"), d.get("observation_date"),
|
|
171
|
+
d.get("interval_start"),
|
|
172
|
+
)
|
|
173
|
+
if not old_anchor:
|
|
174
|
+
return False
|
|
175
|
+
from superlocalmemory.retrieval.time_window import parse_timestamp
|
|
176
|
+
nd = parse_timestamp(new_anchor)
|
|
177
|
+
od = parse_timestamp(old_anchor)
|
|
178
|
+
if nd is None or od is None:
|
|
179
|
+
return False
|
|
180
|
+
return nd.date() != od.date()
|
|
181
|
+
|
|
122
182
|
def detect_contradiction(
|
|
123
183
|
self,
|
|
124
184
|
new_fact: AtomicFact,
|
|
@@ -206,7 +266,10 @@ class TemporalValidator:
|
|
|
206
266
|
Both params are REQUIRED in the call site. Do NOT rename to is_valid().
|
|
207
267
|
"""
|
|
208
268
|
try:
|
|
209
|
-
|
|
269
|
+
# profile_id or None: an empty-string default must NOT become a
|
|
270
|
+
# `profile_id = ''` filter (which never matches → an expired fact
|
|
271
|
+
# would be reported valid). None falls back to the unscoped lookup.
|
|
272
|
+
tv = self._db.get_temporal_validity(fact_id, profile_id or None)
|
|
210
273
|
if tv is None:
|
|
211
274
|
return True # No temporal record = assumed valid
|
|
212
275
|
return (
|
|
@@ -32,6 +32,7 @@ logger = logging.getLogger(__name__)
|
|
|
32
32
|
_SCHEMA_DDL = """
|
|
33
33
|
CREATE TABLE IF NOT EXISTS skill_evolution_log (
|
|
34
34
|
id TEXT PRIMARY KEY,
|
|
35
|
+
profile_id TEXT NOT NULL DEFAULT 'default',
|
|
35
36
|
skill_name TEXT NOT NULL,
|
|
36
37
|
parent_skill_id TEXT,
|
|
37
38
|
evolution_type TEXT NOT NULL,
|
|
@@ -49,14 +50,16 @@ CREATE TABLE IF NOT EXISTS skill_evolution_log (
|
|
|
49
50
|
completed_at TEXT
|
|
50
51
|
);
|
|
51
52
|
|
|
52
|
-
CREATE INDEX IF NOT EXISTS idx_evo_skill ON skill_evolution_log(skill_name);
|
|
53
|
-
CREATE INDEX IF NOT EXISTS idx_evo_status ON skill_evolution_log(status);
|
|
54
|
-
CREATE INDEX IF NOT EXISTS idx_evo_created ON skill_evolution_log(created_at);
|
|
53
|
+
CREATE INDEX IF NOT EXISTS idx_evo_skill ON skill_evolution_log(profile_id, skill_name);
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_evo_status ON skill_evolution_log(profile_id, status);
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_evo_created ON skill_evolution_log(profile_id, created_at);
|
|
55
56
|
|
|
56
57
|
CREATE TABLE IF NOT EXISTS evolution_cycle_state (
|
|
57
|
-
|
|
58
|
+
profile_id TEXT NOT NULL DEFAULT 'default',
|
|
59
|
+
key TEXT NOT NULL,
|
|
58
60
|
value INTEGER DEFAULT 0,
|
|
59
|
-
updated_at TEXT
|
|
61
|
+
updated_at TEXT,
|
|
62
|
+
PRIMARY KEY (profile_id, key)
|
|
60
63
|
);
|
|
61
64
|
"""
|
|
62
65
|
|
|
@@ -77,6 +80,11 @@ class EvolutionStore:
|
|
|
77
80
|
def _ensure_schema(self) -> None:
|
|
78
81
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
79
82
|
try:
|
|
83
|
+
# Migrate an existing pre-isolation DB BEFORE running the schema DDL:
|
|
84
|
+
# the DDL creates indexes on profile_id, which would fail on a legacy
|
|
85
|
+
# table that still lacks the column. On a fresh DB the migration is a
|
|
86
|
+
# no-op (tables absent) and the DDL creates everything correctly.
|
|
87
|
+
self._migrate_profile_isolation(conn)
|
|
80
88
|
conn.executescript(_SCHEMA_DDL)
|
|
81
89
|
conn.commit()
|
|
82
90
|
except sqlite3.OperationalError as exc:
|
|
@@ -84,56 +92,113 @@ class EvolutionStore:
|
|
|
84
92
|
finally:
|
|
85
93
|
conn.close()
|
|
86
94
|
|
|
87
|
-
def
|
|
88
|
-
"""
|
|
95
|
+
def _migrate_profile_isolation(self, conn: sqlite3.Connection) -> None:
|
|
96
|
+
"""Self-migrate pre-profile-isolation DBs to add per-profile scoping.
|
|
97
|
+
|
|
98
|
+
These tables are store-owned (created lazily here, not by the schema
|
|
99
|
+
migration runner which fires before this store exists), so the store
|
|
100
|
+
owns their upgrade too. Idempotent: only alters when the column/PK is
|
|
101
|
+
missing. Existing rows backfill to 'default' — the historical profile.
|
|
102
|
+
"""
|
|
103
|
+
# skill_evolution_log: additive column (safe, preserves rows).
|
|
104
|
+
cols = {r[1] for r in conn.execute(
|
|
105
|
+
"PRAGMA table_info(skill_evolution_log)").fetchall()}
|
|
106
|
+
if cols and "profile_id" not in cols:
|
|
107
|
+
conn.execute(
|
|
108
|
+
"ALTER TABLE skill_evolution_log "
|
|
109
|
+
"ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'default'"
|
|
110
|
+
)
|
|
111
|
+
conn.execute(
|
|
112
|
+
"CREATE INDEX IF NOT EXISTS idx_evo_skill "
|
|
113
|
+
"ON skill_evolution_log(profile_id, skill_name)"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# evolution_cycle_state: PK changes from (key) to (profile_id, key).
|
|
117
|
+
# SQLite cannot alter a PK in place → rebuild + copy, backfilling
|
|
118
|
+
# existing counters to the 'default' profile.
|
|
119
|
+
cyc_cols = {r[1] for r in conn.execute(
|
|
120
|
+
"PRAGMA table_info(evolution_cycle_state)").fetchall()}
|
|
121
|
+
if cyc_cols and "profile_id" not in cyc_cols:
|
|
122
|
+
conn.execute(
|
|
123
|
+
"ALTER TABLE evolution_cycle_state RENAME TO _evo_cycle_old"
|
|
124
|
+
)
|
|
125
|
+
conn.execute(
|
|
126
|
+
"CREATE TABLE evolution_cycle_state ("
|
|
127
|
+
" profile_id TEXT NOT NULL DEFAULT 'default',"
|
|
128
|
+
" key TEXT NOT NULL,"
|
|
129
|
+
" value INTEGER DEFAULT 0,"
|
|
130
|
+
" updated_at TEXT,"
|
|
131
|
+
" PRIMARY KEY (profile_id, key))"
|
|
132
|
+
)
|
|
133
|
+
conn.execute(
|
|
134
|
+
"INSERT INTO evolution_cycle_state "
|
|
135
|
+
"(profile_id, key, value, updated_at) "
|
|
136
|
+
"SELECT 'default', key, value, updated_at FROM _evo_cycle_old"
|
|
137
|
+
)
|
|
138
|
+
conn.execute("DROP TABLE _evo_cycle_old")
|
|
139
|
+
|
|
140
|
+
def reset_cycle(self, profile_id: str) -> None:
|
|
141
|
+
"""Reset per-cycle counters for one profile. Called at cycle start.
|
|
142
|
+
|
|
143
|
+
The evolve budget is per-profile: one profile exhausting its budget
|
|
144
|
+
must never block another profile from evolving.
|
|
145
|
+
"""
|
|
89
146
|
now = datetime.now(timezone.utc).isoformat()
|
|
90
147
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
91
148
|
try:
|
|
92
149
|
conn.execute(
|
|
93
|
-
"INSERT OR REPLACE INTO evolution_cycle_state
|
|
94
|
-
"
|
|
95
|
-
(
|
|
150
|
+
"INSERT OR REPLACE INTO evolution_cycle_state "
|
|
151
|
+
"(profile_id, key, value, updated_at) "
|
|
152
|
+
"VALUES (?, 'cycle_count', 0, ?)",
|
|
153
|
+
(profile_id, now),
|
|
96
154
|
)
|
|
97
155
|
conn.commit()
|
|
98
156
|
finally:
|
|
99
157
|
conn.close()
|
|
100
158
|
|
|
101
|
-
def can_evolve(self) -> bool:
|
|
102
|
-
"""Check if budget allows another evolution this cycle."""
|
|
159
|
+
def can_evolve(self, profile_id: str) -> bool:
|
|
160
|
+
"""Check if this profile's budget allows another evolution this cycle."""
|
|
103
161
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
104
162
|
try:
|
|
105
163
|
row = conn.execute(
|
|
106
|
-
"SELECT value FROM evolution_cycle_state
|
|
164
|
+
"SELECT value FROM evolution_cycle_state "
|
|
165
|
+
"WHERE profile_id = ? AND key = 'cycle_count'",
|
|
166
|
+
(profile_id,),
|
|
107
167
|
).fetchone()
|
|
108
168
|
count = row[0] if row else 0
|
|
109
169
|
return count < MAX_EVOLUTIONS_PER_CYCLE
|
|
110
170
|
finally:
|
|
111
171
|
conn.close()
|
|
112
172
|
|
|
113
|
-
def record_evolution_attempt(self) -> None:
|
|
114
|
-
"""Increment cycle counter in DB."""
|
|
173
|
+
def record_evolution_attempt(self, profile_id: str) -> None:
|
|
174
|
+
"""Increment this profile's cycle counter in DB."""
|
|
115
175
|
now = datetime.now(timezone.utc).isoformat()
|
|
116
176
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
117
177
|
try:
|
|
118
178
|
row = conn.execute(
|
|
119
|
-
"SELECT value FROM evolution_cycle_state
|
|
179
|
+
"SELECT value FROM evolution_cycle_state "
|
|
180
|
+
"WHERE profile_id = ? AND key = 'cycle_count'",
|
|
181
|
+
(profile_id,),
|
|
120
182
|
).fetchone()
|
|
121
183
|
current = row[0] if row else 0
|
|
122
184
|
conn.execute(
|
|
123
|
-
"INSERT OR REPLACE INTO evolution_cycle_state
|
|
124
|
-
"
|
|
125
|
-
(
|
|
185
|
+
"INSERT OR REPLACE INTO evolution_cycle_state "
|
|
186
|
+
"(profile_id, key, value, updated_at) "
|
|
187
|
+
"VALUES (?, 'cycle_count', ?, ?)",
|
|
188
|
+
(profile_id, current + 1, now),
|
|
126
189
|
)
|
|
127
190
|
conn.commit()
|
|
128
191
|
finally:
|
|
129
192
|
conn.close()
|
|
130
193
|
|
|
131
|
-
def _get_cycle_count(self) -> int:
|
|
132
|
-
"""Read current cycle count from DB."""
|
|
194
|
+
def _get_cycle_count(self, profile_id: str) -> int:
|
|
195
|
+
"""Read this profile's current cycle count from DB."""
|
|
133
196
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
134
197
|
try:
|
|
135
198
|
row = conn.execute(
|
|
136
|
-
"SELECT value FROM evolution_cycle_state
|
|
199
|
+
"SELECT value FROM evolution_cycle_state "
|
|
200
|
+
"WHERE profile_id = ? AND key = 'cycle_count'",
|
|
201
|
+
(profile_id,),
|
|
137
202
|
).fetchone()
|
|
138
203
|
return row[0] if row else 0
|
|
139
204
|
finally:
|
|
@@ -162,18 +227,19 @@ class EvolutionStore:
|
|
|
162
227
|
# CRUD
|
|
163
228
|
# ------------------------------------------------------------------
|
|
164
229
|
|
|
165
|
-
def save_record(self, record: EvolutionRecord) -> None:
|
|
230
|
+
def save_record(self, record: EvolutionRecord, profile_id: str) -> None:
|
|
166
231
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
167
232
|
try:
|
|
168
233
|
conn.execute(
|
|
169
234
|
"INSERT OR REPLACE INTO skill_evolution_log "
|
|
170
|
-
"(id, skill_name, parent_skill_id, evolution_type,
|
|
171
|
-
" generation, status, mutation_summary, evidence, "
|
|
235
|
+
"(id, profile_id, skill_name, parent_skill_id, evolution_type, "
|
|
236
|
+
" trigger_type, generation, status, mutation_summary, evidence, "
|
|
172
237
|
" original_content, evolved_content, content_diff, "
|
|
173
238
|
" blind_verified, rejection_reason, created_at, completed_at) "
|
|
174
|
-
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
239
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
175
240
|
(
|
|
176
241
|
record.id,
|
|
242
|
+
profile_id,
|
|
177
243
|
record.skill_name,
|
|
178
244
|
record.parent_skill_id,
|
|
179
245
|
record.evolution_type.value,
|
|
@@ -195,13 +261,14 @@ class EvolutionStore:
|
|
|
195
261
|
finally:
|
|
196
262
|
conn.close()
|
|
197
263
|
|
|
198
|
-
def get_record(self, record_id: str) -> Optional[EvolutionRecord]:
|
|
264
|
+
def get_record(self, record_id: str, profile_id: str) -> Optional[EvolutionRecord]:
|
|
199
265
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
200
266
|
conn.row_factory = sqlite3.Row
|
|
201
267
|
try:
|
|
202
268
|
row = conn.execute(
|
|
203
|
-
"SELECT * FROM skill_evolution_log
|
|
204
|
-
|
|
269
|
+
"SELECT * FROM skill_evolution_log "
|
|
270
|
+
"WHERE id = ? AND profile_id = ?",
|
|
271
|
+
(record_id, profile_id),
|
|
205
272
|
).fetchone()
|
|
206
273
|
if not row:
|
|
207
274
|
return None
|
|
@@ -209,68 +276,78 @@ class EvolutionStore:
|
|
|
209
276
|
finally:
|
|
210
277
|
conn.close()
|
|
211
278
|
|
|
212
|
-
def get_skill_history(
|
|
279
|
+
def get_skill_history(
|
|
280
|
+
self, skill_name: str, profile_id: str, limit: int = 20,
|
|
281
|
+
) -> list[EvolutionRecord]:
|
|
213
282
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
214
283
|
conn.row_factory = sqlite3.Row
|
|
215
284
|
try:
|
|
216
285
|
rows = conn.execute(
|
|
217
286
|
"SELECT * FROM skill_evolution_log "
|
|
218
|
-
"WHERE skill_name = ?
|
|
219
|
-
|
|
287
|
+
"WHERE skill_name = ? AND profile_id = ? "
|
|
288
|
+
"ORDER BY created_at DESC LIMIT ?",
|
|
289
|
+
(skill_name, profile_id, limit),
|
|
220
290
|
).fetchall()
|
|
221
291
|
return [self._row_to_record(dict(r)) for r in rows]
|
|
222
292
|
finally:
|
|
223
293
|
conn.close()
|
|
224
294
|
|
|
225
|
-
def get_recent(self, limit: int = 10) -> list[EvolutionRecord]:
|
|
295
|
+
def get_recent(self, profile_id: str, limit: int = 10) -> list[EvolutionRecord]:
|
|
226
296
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
227
297
|
conn.row_factory = sqlite3.Row
|
|
228
298
|
try:
|
|
229
299
|
rows = conn.execute(
|
|
230
300
|
"SELECT * FROM skill_evolution_log "
|
|
231
|
-
"ORDER BY created_at DESC LIMIT ?",
|
|
232
|
-
(limit
|
|
301
|
+
"WHERE profile_id = ? ORDER BY created_at DESC LIMIT ?",
|
|
302
|
+
(profile_id, limit),
|
|
233
303
|
).fetchall()
|
|
234
304
|
return [self._row_to_record(dict(r)) for r in rows]
|
|
235
305
|
finally:
|
|
236
306
|
conn.close()
|
|
237
307
|
|
|
238
|
-
def count_attempts(self, skill_name: str) -> int:
|
|
308
|
+
def count_attempts(self, skill_name: str, profile_id: str) -> int:
|
|
239
309
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
240
310
|
try:
|
|
241
311
|
row = conn.execute(
|
|
242
312
|
"SELECT COUNT(*) FROM skill_evolution_log "
|
|
243
|
-
"WHERE skill_name = ? AND
|
|
244
|
-
(
|
|
313
|
+
"WHERE skill_name = ? AND profile_id = ? "
|
|
314
|
+
"AND status NOT IN ('promoted')",
|
|
315
|
+
(skill_name, profile_id),
|
|
245
316
|
).fetchone()
|
|
246
317
|
return row[0] if row else 0
|
|
247
318
|
finally:
|
|
248
319
|
conn.close()
|
|
249
320
|
|
|
250
|
-
def has_exceeded_attempts(self, skill_name: str) -> bool:
|
|
251
|
-
return self.count_attempts(skill_name) >= MAX_ATTEMPTS_PER_SKILL
|
|
321
|
+
def has_exceeded_attempts(self, skill_name: str, profile_id: str) -> bool:
|
|
322
|
+
return self.count_attempts(skill_name, profile_id) >= MAX_ATTEMPTS_PER_SKILL
|
|
252
323
|
|
|
253
|
-
def get_stats(self) -> dict:
|
|
324
|
+
def get_stats(self, profile_id: str) -> dict:
|
|
254
325
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
255
326
|
try:
|
|
256
327
|
total = conn.execute(
|
|
257
|
-
"SELECT COUNT(*) FROM skill_evolution_log",
|
|
328
|
+
"SELECT COUNT(*) FROM skill_evolution_log WHERE profile_id = ?",
|
|
329
|
+
(profile_id,),
|
|
258
330
|
).fetchone()[0]
|
|
259
331
|
by_status = {}
|
|
260
332
|
for row in conn.execute(
|
|
261
|
-
"SELECT status, COUNT(*) FROM skill_evolution_log
|
|
333
|
+
"SELECT status, COUNT(*) FROM skill_evolution_log "
|
|
334
|
+
"WHERE profile_id = ? GROUP BY status",
|
|
335
|
+
(profile_id,),
|
|
262
336
|
).fetchall():
|
|
263
337
|
by_status[row[0]] = row[1]
|
|
264
338
|
by_type = {}
|
|
265
339
|
for row in conn.execute(
|
|
266
|
-
"SELECT evolution_type, COUNT(*) FROM skill_evolution_log
|
|
340
|
+
"SELECT evolution_type, COUNT(*) FROM skill_evolution_log "
|
|
341
|
+
"WHERE profile_id = ? GROUP BY evolution_type",
|
|
342
|
+
(profile_id,),
|
|
267
343
|
).fetchall():
|
|
268
344
|
by_type[row[0]] = row[1]
|
|
269
345
|
return {
|
|
270
346
|
"total": total,
|
|
271
347
|
"by_status": by_status,
|
|
272
348
|
"by_type": by_type,
|
|
273
|
-
"cycle_budget_remaining":
|
|
349
|
+
"cycle_budget_remaining":
|
|
350
|
+
MAX_EVOLUTIONS_PER_CYCLE - self._get_cycle_count(profile_id),
|
|
274
351
|
}
|
|
275
352
|
finally:
|
|
276
353
|
conn.close()
|
|
@@ -162,7 +162,18 @@ ALLOWED_LLM_MODELS: frozenset[str] = frozenset({
|
|
|
162
162
|
|
|
163
163
|
FORBIDDEN_MODEL_SUBSTRINGS: tuple[str, ...] = ("opus", "gpt-4-turbo")
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
# Hard ceiling on ``max_tokens`` for any single evolution LLM call
|
|
166
|
+
# (LLD-11). This is a CEILING, not a per-call default: each caller passes
|
|
167
|
+
# its own budget (confirm=100, blind-verify=500, mutation=4000). The value
|
|
168
|
+
# MUST stay >= the largest legitimate caller request — otherwise that call
|
|
169
|
+
# trips the guard in ``_dispatch_llm`` and silently fails-closed. The
|
|
170
|
+
# mutation step (``SkillEvolver._generate_mutation``) generates a full
|
|
171
|
+
# SKILL.md and requests 4000; a stale 500 here made mutation return "" on
|
|
172
|
+
# every cycle, killing evolution invisibly. 8000 accommodates 4000 with
|
|
173
|
+
# retry/large-skill headroom. Do NOT lower below 4096 without first
|
|
174
|
+
# shrinking that caller's request. Per-call spend is still bounded by
|
|
175
|
+
# EvolutionBudget (10 LLM calls/cycle x 3 cycles/day).
|
|
176
|
+
MAX_TOKENS_CAP: int = 8000
|
|
166
177
|
|
|
167
178
|
|
|
168
179
|
# ---------------------------------------------------------------------------
|