superlocalmemory 3.4.21 → 3.4.23
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/CHANGELOG.md +30 -1
- package/package.json +1 -1
- package/pyproject.toml +2 -2
- package/scripts/build_entry.py +1 -1
- package/scripts/release_manifest.py +2 -2
- package/skills/slm-build-graph/SKILL.md +1 -1
- package/skills/slm-list-recent/SKILL.md +1 -1
- package/skills/slm-recall/SKILL.md +3 -3
- package/skills/slm-remember/SKILL.md +1 -1
- package/skills/slm-status/SKILL.md +1 -1
- package/skills/slm-switch-profile/SKILL.md +1 -1
- package/src/superlocalmemory/__init__.py +3 -0
- package/src/superlocalmemory/cli/commands.py +40 -5
- package/src/superlocalmemory/cli/context_commands.py +1 -1
- package/src/superlocalmemory/cli/db_migrate.py +1 -1
- package/src/superlocalmemory/cli/escape_hatch.py +1 -1
- package/src/superlocalmemory/cli/main.py +3 -3
- package/src/superlocalmemory/core/context_cache.py +2 -2
- package/src/superlocalmemory/core/ram_lock.py +1 -1
- package/src/superlocalmemory/core/recall_pipeline.py +5 -5
- package/src/superlocalmemory/core/security_primitives.py +2 -2
- package/src/superlocalmemory/core/shadow_router.py +2 -2
- package/src/superlocalmemory/core/slm_disabled.py +1 -1
- package/src/superlocalmemory/core/slmignore.py +1 -1
- package/src/superlocalmemory/core/topic_signature.py +3 -3
- package/src/superlocalmemory/evolution/budget.py +1 -1
- package/src/superlocalmemory/evolution/llm_dispatch.py +2 -2
- package/src/superlocalmemory/hooks/_outcome_common.py +1 -1
- package/src/superlocalmemory/hooks/adapter_base.py +1 -1
- package/src/superlocalmemory/hooks/antigravity_adapter.py +1 -1
- package/src/superlocalmemory/hooks/context_payload.py +2 -2
- package/src/superlocalmemory/hooks/copilot_adapter.py +1 -1
- package/src/superlocalmemory/hooks/cross_platform_connector.py +1 -1
- package/src/superlocalmemory/hooks/cursor_adapter.py +1 -1
- package/src/superlocalmemory/hooks/hook_handlers.py +1 -1
- package/src/superlocalmemory/hooks/ide_connector.py +2 -2
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +1 -1
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +1 -1
- package/src/superlocalmemory/hooks/prewarm_auth.py +1 -1
- package/src/superlocalmemory/hooks/session_registry.py +1 -1
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +1 -1
- package/src/superlocalmemory/hooks/sync_loop.py +2 -2
- package/src/superlocalmemory/hooks/user_prompt_hook.py +1 -1
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +1 -1
- package/src/superlocalmemory/learning/arm_catalog.py +1 -1
- package/src/superlocalmemory/learning/bandit.py +1 -1
- package/src/superlocalmemory/learning/bandit_cache.py +1 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +4 -4
- package/src/superlocalmemory/learning/consolidation_worker.py +1 -1
- package/src/superlocalmemory/learning/database.py +5 -5
- package/src/superlocalmemory/learning/dedup_hnsw.py +1 -1
- package/src/superlocalmemory/learning/ensemble.py +2 -2
- package/src/superlocalmemory/learning/fact_outcome_joins.py +1 -1
- package/src/superlocalmemory/learning/forgetting_scheduler.py +2 -2
- package/src/superlocalmemory/learning/hnsw_dedup.py +2 -2
- package/src/superlocalmemory/learning/labeler.py +3 -3
- package/src/superlocalmemory/learning/legacy_migration.py +1 -1
- package/src/superlocalmemory/learning/memory_merge.py +1 -1
- package/src/superlocalmemory/learning/model_cache.py +1 -1
- package/src/superlocalmemory/learning/model_rollback.py +2 -2
- package/src/superlocalmemory/learning/outcome_queue.py +2 -2
- package/src/superlocalmemory/learning/pattern_miner.py +1 -1
- package/src/superlocalmemory/learning/pattern_miner_constants.py +1 -1
- package/src/superlocalmemory/learning/ranker.py +3 -3
- package/src/superlocalmemory/learning/ranker_common.py +1 -1
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +3 -3
- package/src/superlocalmemory/learning/ranker_retrain_online.py +4 -4
- package/src/superlocalmemory/learning/reward.py +1 -1
- package/src/superlocalmemory/learning/reward_archive.py +2 -2
- package/src/superlocalmemory/learning/reward_boost.py +2 -2
- package/src/superlocalmemory/learning/reward_proxy.py +4 -4
- package/src/superlocalmemory/learning/shadow_test.py +1 -1
- package/src/superlocalmemory/learning/signal_worker.py +2 -2
- package/src/superlocalmemory/learning/signals.py +2 -2
- package/src/superlocalmemory/learning/trigram_index.py +1 -1
- package/src/superlocalmemory/mcp/tools_context.py +1 -1
- package/src/superlocalmemory/mcp/tools_core.py +2 -2
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +3 -3
- package/src/superlocalmemory/server/bandit_loops.py +2 -2
- package/src/superlocalmemory/server/middleware/__init__.py +2 -2
- package/src/superlocalmemory/server/middleware/security_headers.py +3 -3
- package/src/superlocalmemory/server/routes/brain.py +10 -10
- package/src/superlocalmemory/server/routes/prewarm.py +1 -1
- package/src/superlocalmemory/server/security_middleware.py +21 -3
- package/src/superlocalmemory/server/unified_daemon.py +111 -9
- package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +423 -0
- package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +348 -0
- package/src/superlocalmemory/skills/slm-recall/SKILL.md +343 -0
- package/src/superlocalmemory/skills/slm-remember/SKILL.md +194 -0
- package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +224 -0
- package/src/superlocalmemory/skills/slm-status/SKILL.md +363 -0
- package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/storage/migration_runner.py +4 -4
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +1 -1
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +2 -2
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +1 -1
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +1 -1
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +1 -1
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +1 -1
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +1 -1
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +1 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +2 -2
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +1 -1
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +1 -1
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +2 -2
- package/src/superlocalmemory/storage/migrations/__init__.py +3 -3
- package/src/superlocalmemory/ui/index.html +4 -0
- package/src/superlocalmemory/ui/js/core.js +96 -1
- package/src/superlocalmemory.egg-info/PKG-INFO +655 -0
- package/src/superlocalmemory.egg-info/SOURCES.txt +426 -0
- package/src/superlocalmemory.egg-info/dependency_links.txt +1 -0
- package/src/superlocalmemory.egg-info/entry_points.txt +2 -0
- package/src/superlocalmemory.egg-info/requires.txt +58 -0
- package/src/superlocalmemory.egg-info/top_level.txt +1 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory v3.4.
|
|
3
|
+
# Part of SuperLocalMemory v3.4.22 — LLD-04 §4.1 + §3
|
|
4
4
|
|
|
5
5
|
"""``/api/v3/brain`` — unified Brain endpoint (LLD-04 v2).
|
|
6
6
|
|
|
7
|
-
Merges the pre-3.4.
|
|
7
|
+
Merges the pre-3.4.22 Patterns / Learning / Behavioral dashboard tabs
|
|
8
8
|
into one auth-gated, honestly-labelled JSON payload. Every metric has
|
|
9
9
|
an ``is_real`` (numeric counters) or ``is_real_ml`` (ML-derived) flag
|
|
10
10
|
and points at the real table it was computed from. No metric is
|
|
@@ -24,7 +24,7 @@ Design notes (LLD-04 §7 hard rules):
|
|
|
24
24
|
* **U2** — every metric section carries ``is_real`` or ``is_real_ml``.
|
|
25
25
|
* **U3** — ``learning.phase`` never returns ``3`` without an active
|
|
26
26
|
model row AND passing SHA check. Both conditions computed here.
|
|
27
|
-
* **U4** — response shape must not contain the three pre-3.4.
|
|
27
|
+
* **U4** — response shape must not contain the three pre-3.4.22
|
|
28
28
|
fabricated metrics (24h hit-rate, avg age on hit, skill-evolution
|
|
29
29
|
row counts). A static test greps this file to verify; therefore we
|
|
30
30
|
never spell those names anywhere in this module.
|
|
@@ -64,7 +64,7 @@ router = APIRouter(prefix="/api/v3", tags=["brain"])
|
|
|
64
64
|
# LLD-03 v2 stratum space = 4 query types × 3 entity bins × 4 time buckets.
|
|
65
65
|
_STRATA_TOTAL: int = 48
|
|
66
66
|
|
|
67
|
-
_VERSION: str = "3.4.
|
|
67
|
+
_VERSION: str = "3.4.23"
|
|
68
68
|
|
|
69
69
|
# Banned metric names (LLD-04 U4). Kept as a tuple for grep visibility;
|
|
70
70
|
# the source-level test asserts we don't accidentally reintroduce them.
|
|
@@ -246,7 +246,7 @@ def _compute_learning_status(profile_id: str,
|
|
|
246
246
|
"""Section ``learning`` — LLD-02 §4.10 phase truth + counters."""
|
|
247
247
|
signals_total = _safe_count(lrn_db, "learning_signals", profile_id)
|
|
248
248
|
features_total = _safe_count(lrn_db, "learning_features", profile_id)
|
|
249
|
-
# Raw count of historic pre-v3.4.
|
|
249
|
+
# Raw count of historic pre-v3.4.22 feedback rows (the source table)
|
|
250
250
|
legacy_feedback_rows = _safe_count(lrn_db, "learning_feedback", profile_id)
|
|
251
251
|
# Count of rows actually copied forward into learning_signals by
|
|
252
252
|
# legacy_migration.migrate_legacy_feedback (signal_type='legacy_feedback').
|
|
@@ -289,7 +289,7 @@ def _compute_learning_status(profile_id: str,
|
|
|
289
289
|
"signals_last_hour": signals_last_hour,
|
|
290
290
|
"features_total": features_total,
|
|
291
291
|
"feature_count_expected": FEATURE_DIM,
|
|
292
|
-
# Honest split (v3.4.
|
|
292
|
+
# Honest split (v3.4.22+): raw historic table count vs rows
|
|
293
293
|
# actually copied forward via the migrate-legacy flow.
|
|
294
294
|
"legacy_feedback_rows": legacy_feedback_rows,
|
|
295
295
|
"legacy_migrated_count": legacy_migrated,
|
|
@@ -356,7 +356,7 @@ def _top_query_types(
|
|
|
356
356
|
``query_type_od`` (features.py FEATURE_NAMES). We aggregate by the
|
|
357
357
|
active one-hot slot. Missing table or zero rows → empty list.
|
|
358
358
|
"""
|
|
359
|
-
# E.1 (v3.4.
|
|
359
|
+
# E.1 (v3.4.22 perf): push the query-type classification into SQL via
|
|
360
360
|
# json_extract so we don't drag 10k rows' worth of JSON through Python.
|
|
361
361
|
# SQLite's json1 extension treats json_extract as an ordinary scalar
|
|
362
362
|
# function, so the whole aggregation becomes a single COUNT(*)
|
|
@@ -601,7 +601,7 @@ def _compute_cross_platform() -> dict:
|
|
|
601
601
|
reports ``active: false`` with ``reason: error:<ExcName>`` rather
|
|
602
602
|
than crashing the whole Brain endpoint (LLD-04 §2 — "honest, never
|
|
603
603
|
fake"). An unimportable adapter means the install is missing Wave 2C
|
|
604
|
-
components, which is legitimate for an older 3.4.20 → 3.4.
|
|
604
|
+
components, which is legitimate for an older 3.4.20 → 3.4.22 upgrade
|
|
605
605
|
mid-migration.
|
|
606
606
|
"""
|
|
607
607
|
out: dict = {}
|
|
@@ -871,7 +871,7 @@ def _action_outcomes_count(lrn_db: LearningDatabase,
|
|
|
871
871
|
profile_id: str) -> int:
|
|
872
872
|
"""Row count in ``action_outcomes`` for ``profile_id``.
|
|
873
873
|
|
|
874
|
-
``action_outcomes`` ships in v3.4.
|
|
874
|
+
``action_outcomes`` ships in v3.4.22 (M006). While absent, returns 0.
|
|
875
875
|
"""
|
|
876
876
|
try:
|
|
877
877
|
conn = sqlite3.connect(lrn_db.path, timeout=5.0)
|
|
@@ -963,7 +963,7 @@ async def get_brain(profile_id: str = "default") -> dict:
|
|
|
963
963
|
"outcomes_preview": {
|
|
964
964
|
"action_outcomes_rows":
|
|
965
965
|
0 if isinstance(outcomes_rows, Exception) else outcomes_rows,
|
|
966
|
-
"ships_in": "3.4.
|
|
966
|
+
"ships_in": "3.4.22",
|
|
967
967
|
},
|
|
968
968
|
# S9-defer H-22: live tile data for the Reward / Shadow /
|
|
969
969
|
# Evolution-Cost dashboard tiles. Each block is a honest-empty
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
2
|
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
-
# Part of SuperLocalMemory v3.4.
|
|
3
|
+
# Part of SuperLocalMemory v3.4.22 — LLD-01 §4.4 / §4.5
|
|
4
4
|
|
|
5
5
|
"""POST /internal/prewarm — populates the context cache for a session.
|
|
6
6
|
|
|
@@ -33,7 +33,7 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
|
33
33
|
# Enable browser XSS filter (legacy, but doesn't hurt)
|
|
34
34
|
response.headers["X-XSS-Protection"] = "1; mode=block"
|
|
35
35
|
|
|
36
|
-
# Content Security Policy (v3.4.
|
|
36
|
+
# Content Security Policy (v3.4.22 — vendored assets, no CDN hosts).
|
|
37
37
|
# All Bootstrap/D3/Sigma/graphology/Inter assets ship locally under
|
|
38
38
|
# /static/vendor/, so we drop every CDN host from the allow-list.
|
|
39
39
|
# 'unsafe-inline' stays on script-src/style-src for the legacy inline
|
|
@@ -56,9 +56,27 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
|
56
56
|
# Control referrer information leakage
|
|
57
57
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
58
58
|
|
|
59
|
-
#
|
|
60
|
-
|
|
59
|
+
# v3.4.23: Cache-Control strategy
|
|
60
|
+
# ---------------------------------------------------------------
|
|
61
|
+
# Three classes of paths, three policies:
|
|
62
|
+
#
|
|
63
|
+
# /api/* -> no-store (sensitive data, never cache)
|
|
64
|
+
# index.html -> no-cache, must-revalidate (always revalidate)
|
|
65
|
+
# /static/* -> no-cache, must-revalidate (always revalidate
|
|
66
|
+
# with ETag; fast reloads but never stale-after-
|
|
67
|
+
# upgrade)
|
|
68
|
+
#
|
|
69
|
+
# Before v3.4.23 only /api/* had cache headers. Browsers then cached
|
|
70
|
+
# JS/CSS/HTML aggressively via default heuristics, and after a daemon
|
|
71
|
+
# upgrade the dashboard showed an infinite spinner because old cached
|
|
72
|
+
# JS was calling endpoints with stale response shapes. "no-cache"
|
|
73
|
+
# (not "no-store") still allows 304s on unchanged files, so reload
|
|
74
|
+
# cost stays low.
|
|
75
|
+
path = request.url.path
|
|
76
|
+
if path.startswith("/api/"):
|
|
61
77
|
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate"
|
|
62
78
|
response.headers["Pragma"] = "no-cache"
|
|
79
|
+
elif path == "/" or path.endswith(".html") or path.startswith("/static/"):
|
|
80
|
+
response.headers["Cache-Control"] = "no-cache, must-revalidate"
|
|
63
81
|
|
|
64
82
|
return response
|
|
@@ -369,7 +369,7 @@ async def lifespan(application: FastAPI):
|
|
|
369
369
|
)
|
|
370
370
|
|
|
371
371
|
# S9-DASH-02: start the outcome-queue worker so recall →
|
|
372
|
-
# pending_outcomes is actually produced. Before v3.4.
|
|
372
|
+
# pending_outcomes is actually produced. Before v3.4.22 this
|
|
373
373
|
# producer had zero callers and the closed-loop pipeline was
|
|
374
374
|
# dark. Worker drains at 250 ms cadence; one SQLite INSERT per
|
|
375
375
|
# event via EngagementRewardModel.record_recall.
|
|
@@ -457,7 +457,7 @@ async def lifespan(application: FastAPI):
|
|
|
457
457
|
if enable_legacy:
|
|
458
458
|
asyncio.create_task(_start_legacy_redirect(_DEFAULT_PORT, _LEGACY_PORT))
|
|
459
459
|
|
|
460
|
-
# V3.4.
|
|
460
|
+
# V3.4.22 LLD-02: signal-worker background drainer (S8-SK-01 fix).
|
|
461
461
|
# Without this, ``signals.enqueue`` fills a bounded queue and drops
|
|
462
462
|
# silently after ~250 recalls — learning_signals never populates,
|
|
463
463
|
# Phase 3 never activates, the whole Living Brain stays cold.
|
|
@@ -473,7 +473,7 @@ async def lifespan(application: FastAPI):
|
|
|
473
473
|
logger.warning("signal_worker failed to start: %s", exc)
|
|
474
474
|
application.state.signal_worker_started = False
|
|
475
475
|
|
|
476
|
-
# V3.4.
|
|
476
|
+
# V3.4.22 LLD-05: cross-platform adapter sync loop
|
|
477
477
|
if os.environ.get("SLM_CROSS_PLATFORM_SYNC_DISABLED", "").lower() not in ("1", "true"):
|
|
478
478
|
try:
|
|
479
479
|
from superlocalmemory.cli.context_commands import build_default_adapters
|
|
@@ -482,7 +482,7 @@ async def lifespan(application: FastAPI):
|
|
|
482
482
|
except Exception as exc: # pragma: no cover — defensive
|
|
483
483
|
logger.warning("cross-platform sync loop failed to start: %s", exc)
|
|
484
484
|
|
|
485
|
-
# V3.4.
|
|
485
|
+
# V3.4.22 LLD-03: bandit reward proxy settler + retention sweep loops
|
|
486
486
|
if os.environ.get("SLM_BANDIT_DISABLED", "0") != "1":
|
|
487
487
|
try:
|
|
488
488
|
from superlocalmemory.server.bandit_loops import (
|
|
@@ -495,9 +495,20 @@ async def lifespan(application: FastAPI):
|
|
|
495
495
|
global _start_time
|
|
496
496
|
_start_time = time.monotonic()
|
|
497
497
|
_last_activity = time.monotonic()
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
498
|
+
# v3.4.23: pre-format the ready message. Previous code passed a ternary as
|
|
499
|
+
# the log format string with a fixed 2-arg tuple; when idle_timeout<=0 the
|
|
500
|
+
# chosen branch had only one %d, triggering a TypeError on every startup.
|
|
501
|
+
# Python's logging module then wrote the full stack to stderr. Because the
|
|
502
|
+
# call runs inside FastAPI's stacked merged_lifespan, each dump was ~30 KB
|
|
503
|
+
# and the error log grew to tens of MB within a day.
|
|
504
|
+
if idle_timeout <= 0:
|
|
505
|
+
_ready_msg = f"Unified daemon ready on port {_DEFAULT_PORT} (24/7 mode)"
|
|
506
|
+
else:
|
|
507
|
+
_ready_msg = (
|
|
508
|
+
f"Unified daemon ready on port {_DEFAULT_PORT} "
|
|
509
|
+
f"(idle timeout: {idle_timeout}s)"
|
|
510
|
+
)
|
|
511
|
+
logger.info(_ready_msg)
|
|
501
512
|
|
|
502
513
|
yield
|
|
503
514
|
|
|
@@ -850,7 +861,18 @@ def _register_dashboard_routes(application: FastAPI) -> None:
|
|
|
850
861
|
_data_io_mod.ws_manager = ws_manager
|
|
851
862
|
|
|
852
863
|
# Root page
|
|
853
|
-
from fastapi.responses import HTMLResponse
|
|
864
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
865
|
+
|
|
866
|
+
# v3.4.23: /api/version — dashboard polls this to detect daemon upgrades
|
|
867
|
+
# and auto-reload stale tabs (see ui/js/core.js::checkVersionFingerprint).
|
|
868
|
+
try:
|
|
869
|
+
from superlocalmemory import __version__ as _SLM_VERSION
|
|
870
|
+
except Exception: # pragma: no cover — defensive
|
|
871
|
+
_SLM_VERSION = "unknown"
|
|
872
|
+
|
|
873
|
+
@application.get("/api/version")
|
|
874
|
+
async def api_version():
|
|
875
|
+
return JSONResponse({"version": _SLM_VERSION})
|
|
854
876
|
|
|
855
877
|
@application.get("/", response_class=HTMLResponse)
|
|
856
878
|
async def root():
|
|
@@ -863,7 +885,11 @@ def _register_dashboard_routes(application: FastAPI) -> None:
|
|
|
863
885
|
"<p><a href='/docs'>API Documentation</a></p>"
|
|
864
886
|
"</body></html>"
|
|
865
887
|
)
|
|
866
|
-
|
|
888
|
+
# v3.4.23: substitute version placeholder so the dashboard can detect
|
|
889
|
+
# upgrades and auto-reload. Read fresh each request (daemon uptime is
|
|
890
|
+
# days, but we want zero caching surprises during development).
|
|
891
|
+
html = index_path.read_text()
|
|
892
|
+
return html.replace("__SLM_VERSION__", _SLM_VERSION)
|
|
867
893
|
|
|
868
894
|
# Startup event for event listener
|
|
869
895
|
@application.on_event("startup")
|
|
@@ -1066,6 +1092,13 @@ def start_server(port: int = _DEFAULT_PORT) -> None:
|
|
|
1066
1092
|
global _start_time
|
|
1067
1093
|
import uvicorn
|
|
1068
1094
|
|
|
1095
|
+
# v3.4.23: rotate oversized logs before anything else so both the CLI
|
|
1096
|
+
# path (`slm serve`) and the LaunchAgent path (__main__) are covered.
|
|
1097
|
+
try:
|
|
1098
|
+
rotate_oversized_logs()
|
|
1099
|
+
except Exception:
|
|
1100
|
+
pass # never block startup on log housekeeping
|
|
1101
|
+
|
|
1069
1102
|
_PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
1070
1103
|
_PID_FILE.write_text(str(os.getpid()))
|
|
1071
1104
|
_PORT_FILE.write_text(str(port))
|
|
@@ -1094,11 +1127,80 @@ def start_server(port: int = _DEFAULT_PORT) -> None:
|
|
|
1094
1127
|
_PORT_FILE.unlink(missing_ok=True)
|
|
1095
1128
|
|
|
1096
1129
|
|
|
1130
|
+
# ---------------------------------------------------------------------------
|
|
1131
|
+
# v3.4.23 — Startup log rotation
|
|
1132
|
+
# ---------------------------------------------------------------------------
|
|
1133
|
+
# The LaunchAgent plist redirects stdout/stderr to daemon.log and
|
|
1134
|
+
# daemon-error.log. Those files are managed by launchd, not Python, so
|
|
1135
|
+
# Python's RotatingFileHandler cannot prune them. If any bug ever writes
|
|
1136
|
+
# large amounts of data to stderr (the v3.4.22 logger-format bug produced
|
|
1137
|
+
# ~30 KB per startup and the file grew to 69 MB), end users end up with a
|
|
1138
|
+
# disk-eating log they never knew existed.
|
|
1139
|
+
#
|
|
1140
|
+
# rotate_oversized_logs() is a belt-and-suspenders guard: every time the
|
|
1141
|
+
# daemon starts, if either log exceeds MAX_LOG_BYTES we rename the current
|
|
1142
|
+
# file to ".1" (keeping one rotated copy) and truncate the original so
|
|
1143
|
+
# launchd's open file descriptor keeps working. This is cheap, stateless,
|
|
1144
|
+
# and independent of whatever caused the overflow.
|
|
1145
|
+
# ---------------------------------------------------------------------------
|
|
1146
|
+
|
|
1147
|
+
_MAX_LOG_BYTES = 10 * 1024 * 1024 # 10 MB
|
|
1148
|
+
|
|
1149
|
+
|
|
1150
|
+
def rotate_oversized_logs(log_dir: Optional[Path] = None,
|
|
1151
|
+
max_bytes: int = _MAX_LOG_BYTES) -> None:
|
|
1152
|
+
"""Rotate daemon.log and daemon-error.log at startup if oversized.
|
|
1153
|
+
|
|
1154
|
+
Keeps one rotated copy (.1). Safe under concurrent start attempts:
|
|
1155
|
+
rename is atomic on POSIX, and truncation is idempotent.
|
|
1156
|
+
"""
|
|
1157
|
+
log_dir = log_dir or (Path.home() / ".superlocalmemory" / "logs")
|
|
1158
|
+
try:
|
|
1159
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
1160
|
+
except Exception:
|
|
1161
|
+
return
|
|
1162
|
+
for name in ("daemon.log", "daemon-error.log", "daemon.json.log"):
|
|
1163
|
+
path = log_dir / name
|
|
1164
|
+
try:
|
|
1165
|
+
if not path.exists() or path.stat().st_size <= max_bytes:
|
|
1166
|
+
continue
|
|
1167
|
+
rotated = log_dir / f"{name}.1"
|
|
1168
|
+
try:
|
|
1169
|
+
if rotated.exists():
|
|
1170
|
+
rotated.unlink()
|
|
1171
|
+
except Exception:
|
|
1172
|
+
pass
|
|
1173
|
+
try:
|
|
1174
|
+
path.rename(rotated)
|
|
1175
|
+
except Exception:
|
|
1176
|
+
# If rename fails (e.g., file is the open stderr fd under
|
|
1177
|
+
# launchd), fall back to truncation so we at least reclaim
|
|
1178
|
+
# disk without breaking the redirect.
|
|
1179
|
+
try:
|
|
1180
|
+
with open(path, "w"):
|
|
1181
|
+
pass
|
|
1182
|
+
except Exception:
|
|
1183
|
+
pass
|
|
1184
|
+
continue
|
|
1185
|
+
# Re-create the original path as empty so launchd's redirect
|
|
1186
|
+
# keeps appending to a fresh file.
|
|
1187
|
+
try:
|
|
1188
|
+
path.touch()
|
|
1189
|
+
except Exception:
|
|
1190
|
+
pass
|
|
1191
|
+
except Exception:
|
|
1192
|
+
# Log rotation must never prevent daemon startup.
|
|
1193
|
+
continue
|
|
1194
|
+
|
|
1195
|
+
|
|
1097
1196
|
# ---------------------------------------------------------------------------
|
|
1098
1197
|
# CLI entry point
|
|
1099
1198
|
# ---------------------------------------------------------------------------
|
|
1100
1199
|
|
|
1101
1200
|
if __name__ == "__main__":
|
|
1201
|
+
# Rotate first, then configure logging, so the first log line lands in a
|
|
1202
|
+
# freshly-sized file.
|
|
1203
|
+
rotate_oversized_logs()
|
|
1102
1204
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
|
|
1103
1205
|
port = _DEFAULT_PORT
|
|
1104
1206
|
for arg in sys.argv:
|