superlocalmemory 3.8.0 → 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/CHANGELOG.md +60 -0
- package/README.md +32 -120
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +1 -2
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +2 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +134 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +2 -7
- package/src/superlocalmemory/cli/main.py +10 -4
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
- package/src/superlocalmemory/core/embedding_worker.py +4 -5
- package/src/superlocalmemory/core/embeddings.py +119 -45
- package/src/superlocalmemory/core/engine.py +24 -21
- package/src/superlocalmemory/core/engine_ingestion.py +332 -45
- package/src/superlocalmemory/core/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- package/src/superlocalmemory/core/recall_pipeline.py +5 -3
- package/src/superlocalmemory/core/store_pipeline.py +153 -20
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/portable_kit.py +34 -2
- 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/source_quality.py +523 -1
- package/src/superlocalmemory/loops/ledger.py +25 -5
- package/src/superlocalmemory/mcp/server.py +11 -30
- package/src/superlocalmemory/mcp/tools_core.py +4 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/retrieval/engine.py +53 -21
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/routes/backup.py +293 -70
- package/src/superlocalmemory/server/routes/behavioral.py +336 -59
- package/src/superlocalmemory/server/routes/brain.py +57 -16
- package/src/superlocalmemory/server/routes/config_api.py +84 -82
- package/src/superlocalmemory/server/routes/entity.py +100 -23
- package/src/superlocalmemory/server/routes/evolution.py +103 -100
- package/src/superlocalmemory/server/routes/learning.py +276 -105
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/mesh.py +121 -32
- package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
- package/src/superlocalmemory/server/routes/stats.py +93 -155
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +64 -20
- package/src/superlocalmemory/server/unified_daemon.py +467 -40
- package/src/superlocalmemory/storage/migration_runner.py +79 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -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/schema.py +49 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/ui/index.html +3 -6
- package/src/superlocalmemory/ui/js/core.js +52 -9
- package/src/superlocalmemory/ui/js/dashboard.js +169 -82
- package/src/superlocalmemory/ui/js/od-backup.js +156 -65
- package/src/superlocalmemory/ui/js/od-brain.js +88 -51
- package/src/superlocalmemory/ui/js/od-entities.js +22 -22
- package/src/superlocalmemory/ui/js/od-graph.js +11 -4
- package/src/superlocalmemory/ui/js/od-memories.js +47 -5
- package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
- package/src/superlocalmemory/ui/js/od-settings.js +113 -59
- package/src/superlocalmemory/ui/js/od-shell.js +249 -33
- package/src/superlocalmemory/ui/js/od-skills.js +44 -17
- package/src/superlocalmemory/ui/js/settings.js +15 -1
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-governance-advisor.md +0 -80
- package/plugin-src/agents/slm-loop-runner.md +0 -71
- package/plugin-src/agents/slm-memory-advisor.md +0 -49
- package/plugin-src/agents/slm-optimize-advisor.md +0 -44
- package/plugin-src/commands/slm-loop.md +0 -31
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -102
- package/plugin-src/manifest.json +0 -30
- 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 -62
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -25
- package/plugin-src/skills/slm-governance/SKILL.md +0 -248
- package/plugin-src/skills/slm-loop/SKILL.md +0 -99
- package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
- package/plugin-src/skills/slm-profile/SKILL.md +0 -148
- package/plugin-src/skills/slm-scope/SKILL.md +0 -176
|
@@ -84,8 +84,9 @@ def _record_recall_hits(
|
|
|
84
84
|
enqueue_shown_flip,
|
|
85
85
|
)
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
pid = profile_id
|
|
88
|
+
if not pid:
|
|
89
|
+
pid = get_engine().profile_id
|
|
89
90
|
slm_dir = canonical_data_root()
|
|
90
91
|
|
|
91
92
|
shown_ids = [r.get("fact_id", "") for r in results[:10]
|
|
@@ -121,7 +122,7 @@ def _record_recall_hits(
|
|
|
121
122
|
def register_core_tools(server, get_engine: Callable) -> None:
|
|
122
123
|
"""Register the 13 core MCP tools on *server*."""
|
|
123
124
|
|
|
124
|
-
@server.tool(
|
|
125
|
+
@server.tool()
|
|
125
126
|
async def remember(
|
|
126
127
|
content: str, tags: str = "", project: str = "",
|
|
127
128
|
importance: int = 5, session_id: str = "",
|
|
@@ -143,7 +143,7 @@ def register_learning_tools(server, get_engine: Callable) -> None:
|
|
|
143
143
|
logger.debug("get_assertions failed: %s", exc)
|
|
144
144
|
return {"assertions": [], "count": 0, "error": str(exc)}
|
|
145
145
|
|
|
146
|
-
@server.tool(
|
|
146
|
+
@server.tool()
|
|
147
147
|
async def reinforce_assertion(assertion_id: str) -> dict:
|
|
148
148
|
"""Reinforce a behavioral assertion (increase confidence).
|
|
149
149
|
|
|
@@ -171,7 +171,7 @@ def register_learning_tools(server, get_engine: Callable) -> None:
|
|
|
171
171
|
except Exception as exc:
|
|
172
172
|
return {"success": False, "error": str(exc)}
|
|
173
173
|
|
|
174
|
-
@server.tool(
|
|
174
|
+
@server.tool()
|
|
175
175
|
async def contradict_assertion(assertion_id: str) -> dict:
|
|
176
176
|
"""Contradict a behavioral assertion (decrease confidence).
|
|
177
177
|
|
|
@@ -679,32 +679,47 @@ class RetrievalEngine:
|
|
|
679
679
|
fused: list,
|
|
680
680
|
ch_results: dict[str, list[tuple[str, float]]],
|
|
681
681
|
effective_limit: int,
|
|
682
|
-
min_per_channel: int = 2,
|
|
683
682
|
) -> list:
|
|
684
|
-
"""
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
683
|
+
"""Keep strong lexical and structure evidence visible in the result cap.
|
|
684
|
+
|
|
685
|
+
A semantic channel with a larger weight can fill a small result limit
|
|
686
|
+
even when BM25 has an exact, high-signal hit. That broke the
|
|
687
|
+
``queryable now`` ingestion contract: a freshly inserted FTS row could
|
|
688
|
+
exist durably but remain invisible to immediate recall. Reserve one
|
|
689
|
+
capped slot for a strong BM25 hit and two for a structure channel when
|
|
690
|
+
such candidates exist, without returning more than ``effective_limit``.
|
|
690
691
|
"""
|
|
691
|
-
|
|
692
|
+
channel_minimums = (
|
|
693
|
+
("bm25", 1, 0.0),
|
|
694
|
+
("entity_graph", 2, 0.0),
|
|
695
|
+
)
|
|
692
696
|
top_ids = {fr.fact_id for fr in top}
|
|
693
697
|
|
|
694
698
|
promoted = []
|
|
695
|
-
for ch_name in
|
|
699
|
+
for ch_name, minimum, score_floor in channel_minimums:
|
|
696
700
|
ch_items = ch_results.get(ch_name, [])
|
|
697
701
|
if not ch_items:
|
|
698
702
|
continue
|
|
699
703
|
|
|
700
|
-
|
|
701
|
-
|
|
704
|
+
eligible_ids = {
|
|
705
|
+
fid
|
|
706
|
+
for fid, score in ch_items
|
|
707
|
+
if (
|
|
708
|
+
float(score) > score_floor
|
|
709
|
+
if ch_name == "bm25"
|
|
710
|
+
else float(score) >= score_floor
|
|
711
|
+
)
|
|
712
|
+
}
|
|
713
|
+
if not eligible_ids:
|
|
714
|
+
continue
|
|
715
|
+
|
|
716
|
+
present = sum(1 for fid in eligible_ids if fid in top_ids)
|
|
717
|
+
if present >= minimum:
|
|
702
718
|
continue
|
|
703
719
|
|
|
704
|
-
needed =
|
|
705
|
-
ch_fids = {fid for fid, _ in ch_items}
|
|
720
|
+
needed = minimum - present
|
|
706
721
|
for fr in fused:
|
|
707
|
-
if fr.fact_id in
|
|
722
|
+
if fr.fact_id in eligible_ids and fr.fact_id not in top_ids:
|
|
708
723
|
promoted.append(fr)
|
|
709
724
|
top_ids.add(fr.fact_id)
|
|
710
725
|
needed -= 1
|
|
@@ -714,10 +729,15 @@ class RetrievalEngine:
|
|
|
714
729
|
if not promoted:
|
|
715
730
|
return top
|
|
716
731
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
732
|
+
selected = promoted[:effective_limit]
|
|
733
|
+
result = list(top[:effective_limit])
|
|
734
|
+
free_slots = max(0, effective_limit - len(result))
|
|
735
|
+
result.extend(selected[:free_slots])
|
|
736
|
+
remaining = selected[free_slots:]
|
|
737
|
+
if remaining:
|
|
738
|
+
keep = max(0, effective_limit - len(remaining))
|
|
739
|
+
result = result[:keep] + remaining
|
|
740
|
+
return result[:effective_limit]
|
|
721
741
|
|
|
722
742
|
# -- Channel execution --------------------------------------------------
|
|
723
743
|
|
|
@@ -853,14 +873,26 @@ class RetrievalEngine:
|
|
|
853
873
|
q_emb, profile_id, self._config.bm25_top_k,
|
|
854
874
|
)
|
|
855
875
|
|
|
856
|
-
#
|
|
876
|
+
# Each local channel gets a strict latency budget. A slow graph walk
|
|
877
|
+
# must not make an interactive recall wait 30 seconds; completed
|
|
878
|
+
# channels still participate in fusion and the timeout is observable.
|
|
879
|
+
channel_timeout_seconds = 1.0
|
|
880
|
+
# One shared deadline keeps parallel dispatch genuinely bounded. A
|
|
881
|
+
# per-future timeout here would serialise the wait and turn five slow
|
|
882
|
+
# channels into five seconds of UI latency.
|
|
883
|
+
done, pending = concurrent.futures.wait(
|
|
884
|
+
futures.values(), timeout=channel_timeout_seconds,
|
|
885
|
+
)
|
|
857
886
|
for name, fut in futures.items():
|
|
887
|
+
if fut in pending:
|
|
888
|
+
logger.warning("Channel %s exceeded %.1fs latency budget", name, channel_timeout_seconds)
|
|
889
|
+
continue
|
|
858
890
|
try:
|
|
859
|
-
ch_name, result = fut.result(
|
|
891
|
+
ch_name, result = fut.result()
|
|
860
892
|
if result:
|
|
861
893
|
out[ch_name] = result
|
|
862
894
|
except Exception as exc:
|
|
863
|
-
logger.warning("Channel %s
|
|
895
|
+
logger.warning("Channel %s failed: %s", name, exc)
|
|
864
896
|
|
|
865
897
|
# Apply registered post-retrieval filters (forgetting filter, etc.)
|
|
866
898
|
if hasattr(self, '_registry') and self._registry._filters:
|
|
@@ -25,9 +25,8 @@ import sys
|
|
|
25
25
|
import threading
|
|
26
26
|
import time
|
|
27
27
|
import weakref
|
|
28
|
-
from typing import Any
|
|
29
|
-
|
|
30
28
|
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
31
30
|
|
|
32
31
|
from superlocalmemory.infra.data_root import state_path
|
|
33
32
|
from superlocalmemory.storage.models import AtomicFact
|
|
@@ -57,9 +56,9 @@ _live_rerankers: set[weakref.ref] = set()
|
|
|
57
56
|
|
|
58
57
|
logger = logging.getLogger(__name__)
|
|
59
58
|
|
|
60
|
-
_IDLE_TIMEOUT_SECONDS =
|
|
59
|
+
_IDLE_TIMEOUT_SECONDS = 1800 # V3.8.1: keep interactive sessions warm.
|
|
61
60
|
# V3.3.12: Configurable via SLM_RERANKER_IDLE_TIMEOUT env var.
|
|
62
|
-
#
|
|
61
|
+
# Low-RAM installations can retain aggressive recycling through the override.
|
|
63
62
|
# Set ``SLM_RERANKER_IDLE_TIMEOUT=120`` + ``slm restart`` to revert.
|
|
64
63
|
_IDLE_TIMEOUT_SECONDS = int(os.environ.get("SLM_RERANKER_IDLE_TIMEOUT", _IDLE_TIMEOUT_SECONDS))
|
|
65
64
|
_SUBPROCESS_RESPONSE_TIMEOUT = 15 # v3.4.52: 15s (was 180s). Long timeout blocked the
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Process-safe, durable read/modify/write access to ``config.json``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
import threading
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable, Iterator
|
|
12
|
+
|
|
13
|
+
_PROCESS_LOCK = threading.RLock()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@contextmanager
|
|
17
|
+
def _file_lock(path: Path) -> Iterator[None]:
|
|
18
|
+
"""Serialize config access across daemon, CLI, and worker processes."""
|
|
19
|
+
lock_path = path.with_name(f".{path.name}.lock")
|
|
20
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
21
|
+
with _PROCESS_LOCK, lock_path.open("a+b") as handle:
|
|
22
|
+
if os.name == "nt":
|
|
23
|
+
import msvcrt
|
|
24
|
+
|
|
25
|
+
handle.seek(0)
|
|
26
|
+
handle.write(b"\0")
|
|
27
|
+
handle.flush()
|
|
28
|
+
handle.seek(0)
|
|
29
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
|
30
|
+
else:
|
|
31
|
+
import fcntl
|
|
32
|
+
|
|
33
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
|
34
|
+
try:
|
|
35
|
+
yield
|
|
36
|
+
finally:
|
|
37
|
+
if os.name == "nt":
|
|
38
|
+
handle.seek(0)
|
|
39
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
|
40
|
+
else:
|
|
41
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read_unlocked(path: Path) -> dict:
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return {}
|
|
47
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
48
|
+
if not isinstance(value, dict):
|
|
49
|
+
raise ValueError("config root must be a JSON object")
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def read_config(path: Path) -> dict:
|
|
54
|
+
"""Return one coherent config snapshot under the interprocess lock."""
|
|
55
|
+
with _file_lock(path):
|
|
56
|
+
return _read_unlocked(path)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def update_config(path: Path, update: Callable[[dict], None]) -> dict:
|
|
60
|
+
"""Atomically update and durably replace a config JSON object."""
|
|
61
|
+
with _file_lock(path):
|
|
62
|
+
data = _read_unlocked(path)
|
|
63
|
+
update(data)
|
|
64
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
65
|
+
descriptor, temp_name = tempfile.mkstemp(
|
|
66
|
+
prefix=f".{path.name}.",
|
|
67
|
+
suffix=".tmp",
|
|
68
|
+
dir=path.parent,
|
|
69
|
+
)
|
|
70
|
+
temp_path = Path(temp_name)
|
|
71
|
+
try:
|
|
72
|
+
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
|
73
|
+
json.dump(data, stream, indent=2)
|
|
74
|
+
stream.write("\n")
|
|
75
|
+
stream.flush()
|
|
76
|
+
os.fsync(stream.fileno())
|
|
77
|
+
os.chmod(temp_path, 0o600)
|
|
78
|
+
os.replace(temp_path, path)
|
|
79
|
+
if hasattr(os, "O_DIRECTORY"):
|
|
80
|
+
directory_fd = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
|
|
81
|
+
try:
|
|
82
|
+
os.fsync(directory_fd)
|
|
83
|
+
finally:
|
|
84
|
+
os.close(directory_fd)
|
|
85
|
+
finally:
|
|
86
|
+
temp_path.unlink(missing_ok=True)
|
|
87
|
+
return data
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
__all__ = ("read_config", "update_config")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Exact browser-origin validation for the local dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from urllib.parse import urlsplit
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_LOOPBACK_HOSTS = frozenset({"127.0.0.1", "::1", "localhost"})
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def origin_is_loopback(origin: str) -> bool:
|
|
12
|
+
"""Return whether an Origin is absent or an exact HTTP(S) loopback URL."""
|
|
13
|
+
if not origin:
|
|
14
|
+
return True
|
|
15
|
+
try:
|
|
16
|
+
parsed = urlsplit(origin)
|
|
17
|
+
# Accessing port validates malformed/non-numeric port values.
|
|
18
|
+
_ = parsed.port
|
|
19
|
+
except (TypeError, ValueError):
|
|
20
|
+
return False
|
|
21
|
+
return (
|
|
22
|
+
parsed.scheme in {"http", "https"}
|
|
23
|
+
and parsed.hostname is not None
|
|
24
|
+
and parsed.hostname.lower() in _LOOPBACK_HOSTS
|
|
25
|
+
and parsed.username is None
|
|
26
|
+
and parsed.password is None
|
|
27
|
+
and parsed.path in {"", "/"}
|
|
28
|
+
and not parsed.query
|
|
29
|
+
and not parsed.fragment
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def origin_is_daemon(origin: str, *, port: int) -> bool:
|
|
34
|
+
"""Return whether ``origin`` is one of this daemon's loopback aliases.
|
|
35
|
+
|
|
36
|
+
A loopback host alone is not a browser trust boundary: another local web
|
|
37
|
+
server can run on a different port. Credentialless dashboard writes are
|
|
38
|
+
therefore limited to the port owned by this daemon. Authenticated local
|
|
39
|
+
integrations are handled separately by the write-identity boundary.
|
|
40
|
+
"""
|
|
41
|
+
if not origin_is_loopback(origin):
|
|
42
|
+
return False
|
|
43
|
+
try:
|
|
44
|
+
parsed = urlsplit(origin)
|
|
45
|
+
return parsed.port == port
|
|
46
|
+
except (TypeError, ValueError):
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
__all__ = ("origin_is_daemon", "origin_is_loopback")
|