superlocalmemory 3.7.7 → 3.8.0
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 +85 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- 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/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- 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-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- 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-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -2
- 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 +96 -12
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- 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/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 +5 -43
- 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 +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- 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/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- 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 +2 -0
- 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 +35 -11
- 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/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/auth_middleware.py +28 -0
- 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/reward_proxy.py +42 -9
- 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 +243 -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 +21 -49
- 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 +50 -5
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- 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 +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- 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 +21 -4
- 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 +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +72 -16
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- 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 +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +221 -49
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- 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 +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +194 -81
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +30 -5
- package/src/superlocalmemory/server/unified_daemon.py +431 -75
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -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/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v32.py +0 -9
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- 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 +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- 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 +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -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 +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -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 +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -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 +2 -2
- 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/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -102,13 +102,20 @@ class EventBus:
|
|
|
102
102
|
logger.info("EventBus initialized: db=%s", self.db_path)
|
|
103
103
|
|
|
104
104
|
def _init_schema(self) -> None:
|
|
105
|
-
"""Create the memory_events table if it does not exist.
|
|
105
|
+
"""Create the memory_events table if it does not exist.
|
|
106
|
+
|
|
107
|
+
Self-migrates a pre-isolation DB (memory_events without profile_id) so a
|
|
108
|
+
dashboard viewing profile A never sees profile B's events. This table is
|
|
109
|
+
store-owned (created here, not by the migration runner), so the store
|
|
110
|
+
owns its upgrade. Existing rows backfill to the 'default' profile.
|
|
111
|
+
"""
|
|
106
112
|
conn = sqlite3.connect(str(self.db_path))
|
|
107
113
|
try:
|
|
108
114
|
cur = conn.cursor()
|
|
109
115
|
cur.execute("""
|
|
110
116
|
CREATE TABLE IF NOT EXISTS memory_events (
|
|
111
117
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
118
|
+
profile_id TEXT NOT NULL DEFAULT 'default',
|
|
112
119
|
event_type TEXT NOT NULL,
|
|
113
120
|
memory_id INTEGER,
|
|
114
121
|
source_agent TEXT DEFAULT 'user',
|
|
@@ -119,13 +126,42 @@ class EventBus:
|
|
|
119
126
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
120
127
|
)
|
|
121
128
|
""")
|
|
129
|
+
existing = {r[1] for r in cur.execute(
|
|
130
|
+
"PRAGMA table_info(memory_events)").fetchall()}
|
|
131
|
+
if "profile_id" not in existing:
|
|
132
|
+
cur.execute(
|
|
133
|
+
"ALTER TABLE memory_events "
|
|
134
|
+
"ADD COLUMN profile_id TEXT NOT NULL DEFAULT 'default'"
|
|
135
|
+
)
|
|
122
136
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_events_type ON memory_events(event_type)")
|
|
123
137
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_events_created ON memory_events(created_at)")
|
|
124
138
|
cur.execute("CREATE INDEX IF NOT EXISTS idx_events_tier ON memory_events(tier)")
|
|
139
|
+
cur.execute("CREATE INDEX IF NOT EXISTS idx_events_profile ON memory_events(profile_id, id)")
|
|
125
140
|
conn.commit()
|
|
126
141
|
finally:
|
|
127
142
|
conn.close()
|
|
128
143
|
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _resolve_profile(profile_id: Optional[str]) -> str:
|
|
146
|
+
"""Resolve the active profile for an event when not passed explicitly.
|
|
147
|
+
|
|
148
|
+
Uses the request-runtime helper (ContextVar for HTTP, else the
|
|
149
|
+
profiles.json active_profile cache that every switch keeps in sync) so
|
|
150
|
+
MCP-in-daemon and CLI emits are attributed correctly too. Lazy import
|
|
151
|
+
keeps the infra layer free of a hard server dependency; any failure
|
|
152
|
+
falls back to 'default'.
|
|
153
|
+
"""
|
|
154
|
+
if profile_id:
|
|
155
|
+
return profile_id
|
|
156
|
+
try:
|
|
157
|
+
from superlocalmemory.server.routes.helpers import get_active_profile
|
|
158
|
+
resolved = get_active_profile()
|
|
159
|
+
if resolved:
|
|
160
|
+
return resolved
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
return "default"
|
|
164
|
+
|
|
129
165
|
def emit(
|
|
130
166
|
self,
|
|
131
167
|
event_type: str,
|
|
@@ -134,8 +170,14 @@ class EventBus:
|
|
|
134
170
|
source_agent: str = "user",
|
|
135
171
|
source_protocol: str = "internal",
|
|
136
172
|
importance: int = 5,
|
|
173
|
+
profile_id: Optional[str] = None,
|
|
137
174
|
) -> Optional[int]:
|
|
138
|
-
"""Emit an event to all subscribers and persist to database.
|
|
175
|
+
"""Emit an event to all subscribers and persist to database.
|
|
176
|
+
|
|
177
|
+
``profile_id`` scopes the event to a memory profile. When omitted it is
|
|
178
|
+
resolved from the active profile so a dashboard viewing one profile
|
|
179
|
+
never sees another profile's real-time or historical events.
|
|
180
|
+
"""
|
|
139
181
|
if event_type not in VALID_EVENT_TYPES:
|
|
140
182
|
raise ValueError(
|
|
141
183
|
f"Invalid event type: {event_type}. "
|
|
@@ -143,6 +185,7 @@ class EventBus:
|
|
|
143
185
|
)
|
|
144
186
|
|
|
145
187
|
importance = max(1, min(10, importance))
|
|
188
|
+
profile_id = self._resolve_profile(profile_id)
|
|
146
189
|
|
|
147
190
|
now = datetime.now(timezone.utc).isoformat()
|
|
148
191
|
with self._counter_lock:
|
|
@@ -151,6 +194,7 @@ class EventBus:
|
|
|
151
194
|
|
|
152
195
|
event: Dict[str, Any] = {
|
|
153
196
|
"seq": seq,
|
|
197
|
+
"profile_id": profile_id,
|
|
154
198
|
"event_type": event_type,
|
|
155
199
|
"memory_id": memory_id,
|
|
156
200
|
"source_agent": source_agent,
|
|
@@ -177,18 +221,24 @@ class EventBus:
|
|
|
177
221
|
event_type, event_id, memory_id,
|
|
178
222
|
)
|
|
179
223
|
|
|
180
|
-
# Auto-prune heuristic
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
224
|
+
# Auto-prune heuristic. Decide + reset the counter atomically under the
|
|
225
|
+
# lock so concurrent emit() calls cannot both cross the threshold and
|
|
226
|
+
# double-run the prune; run the prune itself OUTSIDE the lock.
|
|
227
|
+
should_prune = False
|
|
228
|
+
with self._counter_lock:
|
|
229
|
+
self._write_count += 1
|
|
230
|
+
if (
|
|
231
|
+
self._write_count >= 100
|
|
232
|
+
or (datetime.now() - self._last_prune).total_seconds() > 86400
|
|
233
|
+
):
|
|
234
|
+
should_prune = True
|
|
235
|
+
self._write_count = 0
|
|
236
|
+
self._last_prune = datetime.now()
|
|
237
|
+
if should_prune:
|
|
186
238
|
try:
|
|
187
239
|
self.prune_events()
|
|
188
240
|
except Exception:
|
|
189
241
|
pass
|
|
190
|
-
self._write_count = 0
|
|
191
|
-
self._last_prune = datetime.now()
|
|
192
242
|
|
|
193
243
|
return event_id
|
|
194
244
|
|
|
@@ -202,10 +252,12 @@ class EventBus:
|
|
|
202
252
|
try:
|
|
203
253
|
cur = conn.cursor()
|
|
204
254
|
cur.execute(
|
|
205
|
-
"INSERT INTO memory_events (event_type, memory_id,
|
|
206
|
-
" source_protocol, payload, importance, tier,
|
|
207
|
-
"
|
|
208
|
-
(
|
|
255
|
+
"INSERT INTO memory_events (profile_id, event_type, memory_id,"
|
|
256
|
+
" source_agent, source_protocol, payload, importance, tier,"
|
|
257
|
+
" created_at)"
|
|
258
|
+
" VALUES (?, ?, ?, ?, ?, ?, ?, 'hot', ?)",
|
|
259
|
+
(event.get("profile_id", "default"), event["event_type"],
|
|
260
|
+
event.get("memory_id"),
|
|
209
261
|
event["source_agent"], event["source_protocol"],
|
|
210
262
|
json.dumps(event["payload"]), event["importance"],
|
|
211
263
|
event["timestamp"]),
|
|
@@ -253,9 +305,16 @@ class EventBus:
|
|
|
253
305
|
since_id: Optional[int] = None,
|
|
254
306
|
limit: int = 50,
|
|
255
307
|
event_type: Optional[str] = None,
|
|
308
|
+
profile_id: Optional[str] = None,
|
|
256
309
|
) -> List[dict]:
|
|
257
|
-
"""Get recent events from the database.
|
|
310
|
+
"""Get recent events from the database, scoped to a profile.
|
|
311
|
+
|
|
312
|
+
``profile_id`` defaults to the active profile so callers never leak
|
|
313
|
+
another profile's events. Pass ``profile_id="*"`` to bypass scoping
|
|
314
|
+
(internal maintenance/pruning only — never a client-facing path).
|
|
315
|
+
"""
|
|
258
316
|
limit = min(limit, 200)
|
|
317
|
+
scope = profile_id if profile_id == "*" else self._resolve_profile(profile_id)
|
|
259
318
|
|
|
260
319
|
try:
|
|
261
320
|
conn = sqlite3.connect(str(self.db_path))
|
|
@@ -263,10 +322,15 @@ class EventBus:
|
|
|
263
322
|
cur = conn.cursor()
|
|
264
323
|
|
|
265
324
|
query = ("SELECT id, event_type, memory_id, source_agent,"
|
|
266
|
-
" source_protocol, payload, importance, tier, created_at"
|
|
325
|
+
" source_protocol, payload, importance, tier, created_at,"
|
|
326
|
+
" profile_id"
|
|
267
327
|
" FROM memory_events WHERE 1=1")
|
|
268
328
|
params: List[Any] = []
|
|
269
329
|
|
|
330
|
+
if scope != "*":
|
|
331
|
+
query += " AND profile_id = ?"
|
|
332
|
+
params.append(scope)
|
|
333
|
+
|
|
270
334
|
if since_id is not None:
|
|
271
335
|
query += " AND id > ?"
|
|
272
336
|
params.append(since_id)
|
|
@@ -293,7 +357,7 @@ class EventBus:
|
|
|
293
357
|
"id": row[0], "event_type": row[1], "memory_id": row[2],
|
|
294
358
|
"source_agent": row[3], "source_protocol": row[4],
|
|
295
359
|
"payload": parsed, "importance": row[6],
|
|
296
|
-
"tier": row[7], "timestamp": row[8],
|
|
360
|
+
"tier": row[7], "timestamp": row[8], "profile_id": row[9],
|
|
297
361
|
})
|
|
298
362
|
return events
|
|
299
363
|
|
|
@@ -306,19 +370,32 @@ class EventBus:
|
|
|
306
370
|
with self._buffer_lock:
|
|
307
371
|
return [e for e in self._buffer if e.get("seq", 0) > since_seq]
|
|
308
372
|
|
|
309
|
-
def get_event_stats(self) -> dict:
|
|
310
|
-
"""Get event system statistics.
|
|
373
|
+
def get_event_stats(self, profile_id: Optional[str] = None) -> dict:
|
|
374
|
+
"""Get event system statistics, scoped to a profile.
|
|
375
|
+
|
|
376
|
+
``profile_id`` defaults to the active profile so dashboard event
|
|
377
|
+
counts never blend across profiles.
|
|
378
|
+
"""
|
|
379
|
+
scope = self._resolve_profile(profile_id)
|
|
311
380
|
try:
|
|
312
381
|
conn = sqlite3.connect(str(self.db_path))
|
|
313
382
|
try:
|
|
314
383
|
cur = conn.cursor()
|
|
315
384
|
|
|
316
|
-
total = cur.execute(
|
|
317
|
-
|
|
385
|
+
total = cur.execute(
|
|
386
|
+
"SELECT COUNT(*) FROM memory_events WHERE profile_id = ?",
|
|
387
|
+
(scope,)).fetchone()[0]
|
|
388
|
+
cur.execute(
|
|
389
|
+
"SELECT event_type, COUNT(*) FROM memory_events "
|
|
390
|
+
"WHERE profile_id = ? GROUP BY event_type", (scope,))
|
|
318
391
|
by_type = dict(cur.fetchall())
|
|
319
|
-
cur.execute(
|
|
392
|
+
cur.execute(
|
|
393
|
+
"SELECT tier, COUNT(*) FROM memory_events "
|
|
394
|
+
"WHERE profile_id = ? GROUP BY tier", (scope,))
|
|
320
395
|
by_tier = dict(cur.fetchall())
|
|
321
|
-
cur.execute(
|
|
396
|
+
cur.execute(
|
|
397
|
+
"SELECT COUNT(*) FROM memory_events WHERE profile_id = ? "
|
|
398
|
+
"AND created_at >= datetime('now', '-24 hours')", (scope,))
|
|
322
399
|
last_24h = cur.fetchone()[0]
|
|
323
400
|
finally:
|
|
324
401
|
conn.close()
|
|
@@ -343,7 +420,13 @@ class EventBus:
|
|
|
343
420
|
warm_hours: int = DEFAULT_WARM_HOURS,
|
|
344
421
|
cold_hours: int = DEFAULT_COLD_HOURS,
|
|
345
422
|
) -> dict:
|
|
346
|
-
"""Apply tiered retention policy to persisted events.
|
|
423
|
+
"""Apply tiered retention policy to persisted events.
|
|
424
|
+
|
|
425
|
+
INTENTIONALLY GLOBAL (all profiles): this is age/tier-based housekeeping
|
|
426
|
+
of the shared event log, not a per-tenant read or retention-policy
|
|
427
|
+
surface. Tenant isolation of event CONTENT is enforced on read via the
|
|
428
|
+
profile_id filter; this sweep only demotes/expires old rows by age.
|
|
429
|
+
"""
|
|
347
430
|
try:
|
|
348
431
|
conn = sqlite3.connect(str(self.db_path))
|
|
349
432
|
try:
|
|
@@ -118,6 +118,22 @@ class RateLimiter:
|
|
|
118
118
|
del self._requests[k]
|
|
119
119
|
return len(stale)
|
|
120
120
|
|
|
121
|
+
def configure(
|
|
122
|
+
self,
|
|
123
|
+
max_requests: int | None = None,
|
|
124
|
+
window_seconds: int | None = None,
|
|
125
|
+
) -> None:
|
|
126
|
+
"""Reconfigure limits at runtime (thread-safe).
|
|
127
|
+
|
|
128
|
+
Recorded request timestamps are preserved; only the ceilings change,
|
|
129
|
+
so a raised limit takes effect immediately for the current window.
|
|
130
|
+
"""
|
|
131
|
+
with self._lock:
|
|
132
|
+
if max_requests is not None:
|
|
133
|
+
self.max_requests = max(1, int(max_requests))
|
|
134
|
+
if window_seconds is not None:
|
|
135
|
+
self.window = max(1, int(window_seconds))
|
|
136
|
+
|
|
121
137
|
def get_stats(self) -> dict:
|
|
122
138
|
"""Return a snapshot of limiter state."""
|
|
123
139
|
with self._lock:
|
|
@@ -133,3 +149,80 @@ class RateLimiter:
|
|
|
133
149
|
# ---------------------------------------------------------------------------
|
|
134
150
|
write_limiter = RateLimiter(max_requests=WRITE_LIMIT, window_seconds=WINDOW_SECONDS)
|
|
135
151
|
read_limiter = RateLimiter(max_requests=READ_LIMIT, window_seconds=WINDOW_SECONDS)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# Runtime-configurable limits (task #47) — dashboard-editable thresholds.
|
|
156
|
+
#
|
|
157
|
+
# The enforcement middleware builds its own limiter instances; it registers
|
|
158
|
+
# them here by role so a single set_limits() call reconfigures every live
|
|
159
|
+
# limiter at once (no restart). Loopback limiters derive from write/read,
|
|
160
|
+
# matching the startup derivation in unified_daemon.
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
_MANAGED: List[Tuple[str, "RateLimiter"]] = []
|
|
164
|
+
_MANAGED_LOCK = threading.Lock()
|
|
165
|
+
_CURRENT: Dict[str, int] = {
|
|
166
|
+
"write": WRITE_LIMIT, "read": READ_LIMIT, "window": WINDOW_SECONDS,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _loopback_write(write: int) -> int:
|
|
171
|
+
return max(300, int(write) * 10)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _loopback_read(read: int) -> int:
|
|
175
|
+
return max(2000, int(read) * 20)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def register_managed(role: str, limiter: "RateLimiter") -> None:
|
|
179
|
+
"""Register an enforcement limiter so set_limits() can reconfigure it.
|
|
180
|
+
|
|
181
|
+
role is one of: 'write', 'read', 'lb_write', 'lb_read'.
|
|
182
|
+
"""
|
|
183
|
+
with _MANAGED_LOCK:
|
|
184
|
+
_MANAGED.append((role, limiter))
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def reset_managed() -> None:
|
|
188
|
+
"""Drop all registered limiters (app teardown / test isolation)."""
|
|
189
|
+
with _MANAGED_LOCK:
|
|
190
|
+
_MANAGED.clear()
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def get_limits() -> Dict[str, int]:
|
|
194
|
+
"""Return the current effective write/read/window limits."""
|
|
195
|
+
with _MANAGED_LOCK:
|
|
196
|
+
return dict(_CURRENT)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def set_limits(
|
|
200
|
+
write: int | None = None,
|
|
201
|
+
read: int | None = None,
|
|
202
|
+
window: int | None = None,
|
|
203
|
+
) -> Dict[str, int]:
|
|
204
|
+
"""Update limits and reconfigure every registered limiter live.
|
|
205
|
+
|
|
206
|
+
Partial updates are supported (pass only what changes). Loopback
|
|
207
|
+
limiters re-derive from the new write/read. Returns the new effective
|
|
208
|
+
limits. Values are floored at 1.
|
|
209
|
+
"""
|
|
210
|
+
with _MANAGED_LOCK:
|
|
211
|
+
if write is not None:
|
|
212
|
+
_CURRENT["write"] = max(1, int(write))
|
|
213
|
+
if read is not None:
|
|
214
|
+
_CURRENT["read"] = max(1, int(read))
|
|
215
|
+
if window is not None:
|
|
216
|
+
_CURRENT["window"] = max(1, int(window))
|
|
217
|
+
w, r, win = _CURRENT["write"], _CURRENT["read"], _CURRENT["window"]
|
|
218
|
+
lb_w, lb_r = _loopback_write(w), _loopback_read(r)
|
|
219
|
+
for role, limiter in _MANAGED:
|
|
220
|
+
if role == "write":
|
|
221
|
+
limiter.configure(max_requests=w, window_seconds=win)
|
|
222
|
+
elif role == "read":
|
|
223
|
+
limiter.configure(max_requests=r, window_seconds=win)
|
|
224
|
+
elif role == "lb_write":
|
|
225
|
+
limiter.configure(max_requests=lb_w, window_seconds=win)
|
|
226
|
+
elif role == "lb_read":
|
|
227
|
+
limiter.configure(max_requests=lb_r, window_seconds=win)
|
|
228
|
+
return dict(_CURRENT)
|
|
@@ -175,7 +175,10 @@ def stop_adapter(name: str) -> dict:
|
|
|
175
175
|
proc.terminate()
|
|
176
176
|
proc.wait(timeout=10)
|
|
177
177
|
except ImportError:
|
|
178
|
-
os.kill(pid, 15)
|
|
178
|
+
# No psutil: best-effort termination. On Windows os.kill(pid, 15) maps to
|
|
179
|
+
# TerminateProcess (a hard kill, not a graceful SIGTERM) — psutil's
|
|
180
|
+
# terminate() does the same there, so behavior is equivalent either way.
|
|
181
|
+
os.kill(pid, 15)
|
|
179
182
|
except Exception:
|
|
180
183
|
pass
|
|
181
184
|
|
|
@@ -42,7 +42,7 @@ def store_credential(service: str, key: str, value: str) -> bool:
|
|
|
42
42
|
except Exception:
|
|
43
43
|
pass
|
|
44
44
|
|
|
45
|
-
# Fallback:
|
|
45
|
+
# Fallback: restricted-permission file (0600) when no OS keychain is present
|
|
46
46
|
try:
|
|
47
47
|
cred_dir = _credential_dir()
|
|
48
48
|
cred_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -65,6 +65,7 @@ TECH_CATEGORIES: dict[str, list[str]] = {
|
|
|
65
65
|
_SCHEMA = """
|
|
66
66
|
CREATE TABLE IF NOT EXISTS transferable_patterns (
|
|
67
67
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
68
|
+
profile_id TEXT NOT NULL DEFAULT 'default',
|
|
68
69
|
pattern_type TEXT NOT NULL DEFAULT 'preference',
|
|
69
70
|
key TEXT NOT NULL,
|
|
70
71
|
value TEXT NOT NULL,
|
|
@@ -75,7 +76,7 @@ CREATE TABLE IF NOT EXISTS transferable_patterns (
|
|
|
75
76
|
contradictions TEXT DEFAULT '[]',
|
|
76
77
|
first_seen TEXT,
|
|
77
78
|
last_seen TEXT,
|
|
78
|
-
UNIQUE(pattern_type, key)
|
|
79
|
+
UNIQUE(profile_id, pattern_type, key)
|
|
79
80
|
);
|
|
80
81
|
|
|
81
82
|
CREATE TABLE IF NOT EXISTS memories (
|
|
@@ -115,25 +116,29 @@ class CrossProjectAggregator:
|
|
|
115
116
|
# Step 2: Merge with temporal decay
|
|
116
117
|
merged = self._merge_with_decay(profile_patterns)
|
|
117
118
|
|
|
118
|
-
# Step 3: Detect contradictions
|
|
119
|
+
# Step 3: Detect contradictions (scoped to the target profile)
|
|
119
120
|
for key, pattern_data in merged.items():
|
|
120
121
|
pattern_data["contradictions"] = self._detect_contradictions(
|
|
121
|
-
key, pattern_data
|
|
122
|
+
key, pattern_data, target_profile
|
|
122
123
|
)
|
|
123
124
|
|
|
124
|
-
# Step 4: Store and return
|
|
125
|
-
self._store_patterns(merged)
|
|
125
|
+
# Step 4: Store and return (scoped to the target profile)
|
|
126
|
+
self._store_patterns(merged, target_profile)
|
|
126
127
|
return [{"key": k, **v} for k, v in merged.items()]
|
|
127
128
|
|
|
128
|
-
def get_preferences(
|
|
129
|
-
|
|
129
|
+
def get_preferences(
|
|
130
|
+
self,
|
|
131
|
+
profile_id: str = "default",
|
|
132
|
+
min_confidence: float = 0.6,
|
|
133
|
+
) -> dict[str, dict]:
|
|
134
|
+
"""Retrieve stored transferable preferences for *profile_id* above *min_confidence*."""
|
|
130
135
|
conn = sqlite3.connect(str(self._db_path))
|
|
131
136
|
conn.row_factory = sqlite3.Row
|
|
132
137
|
try:
|
|
133
138
|
cur = conn.execute(
|
|
134
139
|
"SELECT * FROM transferable_patterns "
|
|
135
|
-
"WHERE confidence >= ? ORDER BY confidence DESC",
|
|
136
|
-
(min_confidence
|
|
140
|
+
"WHERE profile_id = ? AND confidence >= ? ORDER BY confidence DESC",
|
|
141
|
+
(profile_id, min_confidence),
|
|
137
142
|
)
|
|
138
143
|
result: dict[str, dict] = {}
|
|
139
144
|
for row in cur.fetchall():
|
|
@@ -277,18 +282,18 @@ class CrossProjectAggregator:
|
|
|
277
282
|
}
|
|
278
283
|
|
|
279
284
|
def _detect_contradictions(
|
|
280
|
-
self, pattern_key: str, pattern_data: dict
|
|
285
|
+
self, pattern_key: str, pattern_data: dict, profile_id: str = "default"
|
|
281
286
|
) -> list[str]:
|
|
282
287
|
contradictions: list[str] = []
|
|
283
288
|
|
|
284
|
-
# Check stored value vs new value
|
|
289
|
+
# Check stored value vs new value, scoped to this profile.
|
|
285
290
|
conn = sqlite3.connect(str(self._db_path))
|
|
286
291
|
conn.row_factory = sqlite3.Row
|
|
287
292
|
try:
|
|
288
293
|
cur = conn.execute(
|
|
289
294
|
"SELECT value, last_seen FROM transferable_patterns "
|
|
290
|
-
"WHERE key = ? AND pattern_type = 'preference'",
|
|
291
|
-
(pattern_key
|
|
295
|
+
"WHERE profile_id = ? AND key = ? AND pattern_type = 'preference'",
|
|
296
|
+
(profile_id, pattern_key),
|
|
292
297
|
)
|
|
293
298
|
row = cur.fetchone()
|
|
294
299
|
if row:
|
|
@@ -316,18 +321,21 @@ class CrossProjectAggregator:
|
|
|
316
321
|
finally:
|
|
317
322
|
conn.close()
|
|
318
323
|
|
|
319
|
-
def _store_patterns(
|
|
324
|
+
def _store_patterns(
|
|
325
|
+
self, merged: dict[str, dict], profile_id: str = "default"
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Persist merged patterns for *profile_id*. Each profile has isolated rows."""
|
|
320
328
|
conn = sqlite3.connect(str(self._db_path))
|
|
321
329
|
now = datetime.now(UTC).isoformat()
|
|
322
330
|
try:
|
|
323
331
|
for key, data in merged.items():
|
|
324
332
|
conn.execute(
|
|
325
333
|
"""INSERT INTO transferable_patterns
|
|
326
|
-
(pattern_type, key, value, confidence,
|
|
327
|
-
profiles_seen, decay_factor,
|
|
328
|
-
first_seen, last_seen)
|
|
329
|
-
VALUES ('preference', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
330
|
-
ON CONFLICT(pattern_type, key) DO UPDATE SET
|
|
334
|
+
(profile_id, pattern_type, key, value, confidence,
|
|
335
|
+
evidence_count, profiles_seen, decay_factor,
|
|
336
|
+
contradictions, first_seen, last_seen)
|
|
337
|
+
VALUES (?, 'preference', ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
338
|
+
ON CONFLICT(profile_id, pattern_type, key) DO UPDATE SET
|
|
331
339
|
value = excluded.value,
|
|
332
340
|
confidence = excluded.confidence,
|
|
333
341
|
evidence_count = excluded.evidence_count,
|
|
@@ -337,6 +345,7 @@ class CrossProjectAggregator:
|
|
|
337
345
|
last_seen = excluded.last_seen
|
|
338
346
|
""",
|
|
339
347
|
(
|
|
348
|
+
profile_id,
|
|
340
349
|
key,
|
|
341
350
|
data["value"],
|
|
342
351
|
data["confidence"],
|
|
@@ -114,8 +114,14 @@ def _tool_event_hit(
|
|
|
114
114
|
memory_conn: sqlite3.Connection,
|
|
115
115
|
played_at: datetime,
|
|
116
116
|
fact_ids: list[str],
|
|
117
|
+
profile_id: str | None = None,
|
|
117
118
|
) -> bool:
|
|
118
|
-
"""True iff any tool_events row references any fact_id within +30 s.
|
|
119
|
+
"""True iff any tool_events row references any fact_id within +30 s.
|
|
120
|
+
|
|
121
|
+
Tenant-scoped: without a profile predicate, one tenant's reward signal
|
|
122
|
+
would be reinforced by another tenant's tool events firing in the same
|
|
123
|
+
30 s window — cross-contaminated behavioral learning.
|
|
124
|
+
"""
|
|
119
125
|
if not fact_ids:
|
|
120
126
|
return False
|
|
121
127
|
start = played_at.isoformat(timespec="seconds")
|
|
@@ -139,12 +145,24 @@ def _tool_event_hit(
|
|
|
139
145
|
# in Python — typically 0-few rows in a 30 s window, far cheaper than
|
|
140
146
|
# N LIKE passes against a growing table. Still O(rows_in_window * len(fact_ids))
|
|
141
147
|
# worst case but the constant factor is tiny (a substring check).
|
|
148
|
+
# Add the profile predicate only when tool_events actually has the column
|
|
149
|
+
# (schema varies by install).
|
|
150
|
+
scope_sql, scope_params = "", ()
|
|
151
|
+
if profile_id is not None:
|
|
152
|
+
try:
|
|
153
|
+
cols = {r[1] for r in memory_conn.execute(
|
|
154
|
+
"PRAGMA table_info(tool_events)").fetchall()}
|
|
155
|
+
if "profile_id" in cols:
|
|
156
|
+
scope_sql = " AND profile_id = ?"
|
|
157
|
+
scope_params = (profile_id,)
|
|
158
|
+
except sqlite3.Error:
|
|
159
|
+
pass
|
|
142
160
|
try:
|
|
143
161
|
candidate_rows = memory_conn.execute(
|
|
144
162
|
"SELECT payload_json FROM tool_events "
|
|
145
163
|
"WHERE occurred_at BETWEEN ? AND ? "
|
|
146
|
-
" AND payload_json IS NOT NULL",
|
|
147
|
-
(start, end),
|
|
164
|
+
" AND payload_json IS NOT NULL" + scope_sql,
|
|
165
|
+
(start, end, *scope_params),
|
|
148
166
|
).fetchall()
|
|
149
167
|
except sqlite3.Error:
|
|
150
168
|
return False
|
|
@@ -165,6 +183,7 @@ def _requery_detected(
|
|
|
165
183
|
memory_conn: sqlite3.Connection,
|
|
166
184
|
played_at: datetime,
|
|
167
185
|
query_id: str,
|
|
186
|
+
profile_id: str | None = None,
|
|
168
187
|
) -> bool:
|
|
169
188
|
"""True iff a follow-up query within 30 s has matching NFC topic sig.
|
|
170
189
|
|
|
@@ -193,14 +212,28 @@ def _requery_detected(
|
|
|
193
212
|
if tbl is None:
|
|
194
213
|
return False
|
|
195
214
|
|
|
215
|
+
# Tenant scope (guarded — tool_events schema varies): without it, another
|
|
216
|
+
# profile's recall events in the same window count as this profile's
|
|
217
|
+
# requeries, corrupting reward attribution.
|
|
218
|
+
scope_sql, scope_params = "", ()
|
|
219
|
+
if profile_id is not None:
|
|
220
|
+
try:
|
|
221
|
+
cols = {r[1] for r in memory_conn.execute(
|
|
222
|
+
"PRAGMA table_info(tool_events)").fetchall()}
|
|
223
|
+
if "profile_id" in cols:
|
|
224
|
+
scope_sql = " AND profile_id = ?"
|
|
225
|
+
scope_params = (profile_id,)
|
|
226
|
+
except sqlite3.Error:
|
|
227
|
+
pass
|
|
228
|
+
|
|
196
229
|
# Read *query* text from tool_events payload for within-window events;
|
|
197
230
|
# compute topic sig on each, compare against the original query's sig.
|
|
198
231
|
try:
|
|
199
232
|
rows = memory_conn.execute(
|
|
200
233
|
"SELECT payload_json FROM tool_events "
|
|
201
234
|
"WHERE occurred_at > ? AND occurred_at <= ? "
|
|
202
|
-
" AND tool_name = 'recall' LIMIT 20",
|
|
203
|
-
(start, end),
|
|
235
|
+
" AND tool_name = 'recall'" + scope_sql + " LIMIT 20",
|
|
236
|
+
(start, end, *scope_params),
|
|
204
237
|
).fetchall()
|
|
205
238
|
except sqlite3.Error:
|
|
206
239
|
return False
|
|
@@ -212,9 +245,9 @@ def _requery_detected(
|
|
|
212
245
|
try:
|
|
213
246
|
seed_row = memory_conn.execute(
|
|
214
247
|
"SELECT payload_json FROM tool_events "
|
|
215
|
-
"WHERE occurred_at <= ? AND tool_name = 'recall' "
|
|
248
|
+
"WHERE occurred_at <= ? AND tool_name = 'recall'" + scope_sql + " "
|
|
216
249
|
"ORDER BY occurred_at DESC LIMIT 1",
|
|
217
|
-
(played_at.isoformat(timespec="seconds"),),
|
|
250
|
+
(played_at.isoformat(timespec="seconds"), *scope_params),
|
|
218
251
|
).fetchone()
|
|
219
252
|
except sqlite3.Error:
|
|
220
253
|
seed_row = None
|
|
@@ -288,12 +321,12 @@ def settle_stale_plays(
|
|
|
288
321
|
reward: float | None = None
|
|
289
322
|
kind = "default"
|
|
290
323
|
if memory_conn is not None and _tool_event_hit(
|
|
291
|
-
memory_conn, played, top3,
|
|
324
|
+
memory_conn, played, top3, profile_id=str(profile_id),
|
|
292
325
|
):
|
|
293
326
|
reward = 1.0
|
|
294
327
|
kind = "proxy_position"
|
|
295
328
|
elif memory_conn is not None and _requery_detected(
|
|
296
|
-
memory_conn, played, row["query_id"],
|
|
329
|
+
memory_conn, played, row["query_id"], profile_id=str(profile_id),
|
|
297
330
|
):
|
|
298
331
|
reward = 0.0
|
|
299
332
|
kind = "proxy_requery"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""SuperLocalMemory bounded loops.
|
|
2
|
+
|
|
3
|
+
Agent loops that stop when an independent gate passes — not when the agent
|
|
4
|
+
says it is done — with the run's history persisted in SLM's durable memory.
|
|
5
|
+
|
|
6
|
+
Public API::
|
|
7
|
+
|
|
8
|
+
from superlocalmemory.loops import (
|
|
9
|
+
Bounds, Rung, Status, Verdict, LapResult, Outcome,
|
|
10
|
+
run_bounded_loop, InMemoryLedger, SLMMemoryLedger, open_engine_store,
|
|
11
|
+
)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from superlocalmemory.loops.engine import run_bounded_loop
|
|
17
|
+
from superlocalmemory.loops.ledger import (
|
|
18
|
+
InMemoryLedger,
|
|
19
|
+
LedgerEntry,
|
|
20
|
+
LedgerStore,
|
|
21
|
+
SLMMemoryLedger,
|
|
22
|
+
engine_backed_ledger,
|
|
23
|
+
open_engine_store,
|
|
24
|
+
)
|
|
25
|
+
from superlocalmemory.loops.models import (
|
|
26
|
+
Bounds,
|
|
27
|
+
LapResult,
|
|
28
|
+
Outcome,
|
|
29
|
+
Rung,
|
|
30
|
+
Status,
|
|
31
|
+
Verdict,
|
|
32
|
+
)
|
|
33
|
+
from superlocalmemory.loops.rules import (
|
|
34
|
+
no_progress,
|
|
35
|
+
rung_requires_approval,
|
|
36
|
+
stop_condition_met,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Bounds",
|
|
41
|
+
"Rung",
|
|
42
|
+
"Status",
|
|
43
|
+
"Verdict",
|
|
44
|
+
"LapResult",
|
|
45
|
+
"Outcome",
|
|
46
|
+
"run_bounded_loop",
|
|
47
|
+
"LedgerEntry",
|
|
48
|
+
"LedgerStore",
|
|
49
|
+
"InMemoryLedger",
|
|
50
|
+
"SLMMemoryLedger",
|
|
51
|
+
"engine_backed_ledger",
|
|
52
|
+
"open_engine_store",
|
|
53
|
+
"no_progress",
|
|
54
|
+
"rung_requires_approval",
|
|
55
|
+
"stop_condition_met",
|
|
56
|
+
]
|