telys 0.1.0b1__tar.gz
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.
- telys-0.1.0b1/.gitignore +64 -0
- telys-0.1.0b1/PKG-INFO +80 -0
- telys-0.1.0b1/README.md +49 -0
- telys-0.1.0b1/pyproject.toml +54 -0
- telys-0.1.0b1/telys/__init__.py +55 -0
- telys-0.1.0b1/telys/_keys/README.md +36 -0
- telys-0.1.0b1/telys/_supervisor.py +61 -0
- telys-0.1.0b1/telys/_wire.py +38 -0
- telys-0.1.0b1/telys/cli.py +285 -0
- telys-0.1.0b1/telys/client.py +173 -0
- telys-0.1.0b1/telys/embedding.py +231 -0
- telys-0.1.0b1/telys/engine.py +323 -0
- telys-0.1.0b1/telys/filters.py +17 -0
- telys-0.1.0b1/telys/installer.py +356 -0
- telys-0.1.0b1/telys/login.py +225 -0
- telys-0.1.0b1/telys/mcp.py +195 -0
- telys-0.1.0b1/telys/mcp_install.py +211 -0
- telys-0.1.0b1/telys/paths.py +141 -0
- telys-0.1.0b1/telys/runtime.py +145 -0
- telys-0.1.0b1/telys/server.py +427 -0
- telys-0.1.0b1/telys/tuning.py +98 -0
- telys-0.1.0b1/telys/verify.py +226 -0
telys-0.1.0b1/.gitignore
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Mojo build output
|
|
2
|
+
mojo_build/
|
|
3
|
+
*.o
|
|
4
|
+
*.so
|
|
5
|
+
*.dylib
|
|
6
|
+
*.a
|
|
7
|
+
|
|
8
|
+
# Pixi / conda env
|
|
9
|
+
.pixi/
|
|
10
|
+
mojo_env/.pixi/
|
|
11
|
+
|
|
12
|
+
# Python
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
.venv/
|
|
16
|
+
.venv-bench/
|
|
17
|
+
bench/.venv-bench/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.ruff_cache/
|
|
21
|
+
|
|
22
|
+
# Secrets — NEVER commit (local Algenta key, env, certs, signing PRIVATE keys)
|
|
23
|
+
*.key
|
|
24
|
+
.algenta_key
|
|
25
|
+
**/.algenta_key
|
|
26
|
+
.env
|
|
27
|
+
.env.*
|
|
28
|
+
*.pem
|
|
29
|
+
dist-keys/ # signing PRIVATE keys (issuer custody) — NEVER committed
|
|
30
|
+
# Exception: the embedded Telys PUBLIC verification keys are public by design and MUST ship in the SDK wheel.
|
|
31
|
+
!packages/telys-sdk/telys/_keys/*_pub.pem
|
|
32
|
+
|
|
33
|
+
# Python build/wheel artifacts (incl. internal full-engine wheel — NEVER commit a bundled-kernel wheel)
|
|
34
|
+
dist/
|
|
35
|
+
dist-public/
|
|
36
|
+
dist-internal/
|
|
37
|
+
build/
|
|
38
|
+
*.whl
|
|
39
|
+
*.tar.gz
|
|
40
|
+
|
|
41
|
+
# Node / TS SDK
|
|
42
|
+
node_modules/
|
|
43
|
+
packages/memengine-ts/dist/
|
|
44
|
+
|
|
45
|
+
# Benchmark artifacts (raw results are committed selectively; large datasets are not)
|
|
46
|
+
bench/datasets/*
|
|
47
|
+
!bench/datasets/.gitkeep
|
|
48
|
+
bench/results/raw/
|
|
49
|
+
*.parquet
|
|
50
|
+
*.faiss
|
|
51
|
+
*.npy
|
|
52
|
+
*.ame
|
|
53
|
+
*.vidx
|
|
54
|
+
*.sidx
|
|
55
|
+
*.tidx
|
|
56
|
+
|
|
57
|
+
# OS / editor
|
|
58
|
+
.DS_Store
|
|
59
|
+
*.swp
|
|
60
|
+
.idea/
|
|
61
|
+
.vscode/
|
|
62
|
+
|
|
63
|
+
# local agent/session artifacts
|
|
64
|
+
.claude/
|
telys-0.1.0b1/PKG-INFO
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: telys
|
|
3
|
+
Version: 0.1.0b1
|
|
4
|
+
Summary: Telys — public, thin SDK for embedded on-device memory & retrieval (in-process, zero cloud roundtrips)
|
|
5
|
+
Project-URL: Homepage, https://telys.ai
|
|
6
|
+
Project-URL: Documentation, https://telys.ai
|
|
7
|
+
Project-URL: Repository, https://github.com/thyn-ai/telys
|
|
8
|
+
Project-URL: Issues, https://github.com/thyn-ai/telys/issues
|
|
9
|
+
Project-URL: Company, https://thyn.ai
|
|
10
|
+
Author-email: Thyn <eng@thyn.ai>
|
|
11
|
+
Maintainer-email: Thyn <eng@thyn.ai>
|
|
12
|
+
License: Apache-2.0
|
|
13
|
+
Keywords: embeddings,memory,on-device,rag,retrieval,sdk,telys,vector-search
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: cryptography>=42
|
|
27
|
+
Requires-Dist: numpy>=1.24
|
|
28
|
+
Provides-Extra: runtime
|
|
29
|
+
Requires-Dist: telys-runtime; extra == 'runtime'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# telys
|
|
33
|
+
|
|
34
|
+
**Public, thin SDK** for **Telys** — embedded, on-device memory & retrieval. In-process, **zero cloud
|
|
35
|
+
roundtrips** at query time.
|
|
36
|
+
|
|
37
|
+
This package contains only the developer-facing surface: the `Telys`/`Collection` facades, query/filter
|
|
38
|
+
types, the `EmbeddingProvider` interface, the `Tuner`/`TuningPlan` interfaces, a runtime loader, and the
|
|
39
|
+
`telys` CLI. **It contains no engine implementation** — the engine is a separate, closed, signed, on-device
|
|
40
|
+
runtime fetched by `telys runtime install` (see [DECISIONS D-30](../../docs/DECISIONS.md)).
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
**As a CLI** (recommended — isolated env, on your PATH, not pinned to one Python):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pipx install telys
|
|
48
|
+
# …or the one-liner (installs via pipx):
|
|
49
|
+
curl -fsSL https://telys.ai/install.sh | sh
|
|
50
|
+
|
|
51
|
+
telys login # sign in → free device license + signed runtime; fully offline thereafter
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**As a library** (to `import telys` in your own project):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m venv .venv && . .venv/bin/activate
|
|
58
|
+
pip install telys
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
> Plain `pip install --user telys` works too, but pip may warn that its user-scripts dir isn't on your
|
|
62
|
+
> PATH (a macOS `--user` quirk) — `pipx` avoids that entirely. Runtime platforms: **macOS arm64, Linux
|
|
63
|
+
> x86_64/arm64** (Windows: run under **WSL2**).
|
|
64
|
+
```python
|
|
65
|
+
from telys import Telys
|
|
66
|
+
db = Telys("./memory")
|
|
67
|
+
col = db.create_collection("docs", dim=768, partition_by="tenant_id")
|
|
68
|
+
col.add(vectors, ids=ids, metadata=metadata) # bring your own vectors (embedding-agnostic)
|
|
69
|
+
hits = col.search(qvec, where={"tenant_id": "acme"}, top_k=10, explain=True)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The **runtime is required for execution**; the **embedder is optional** — `col.add(vectors, …)` and
|
|
73
|
+
`col.add_texts(…)` both need the runtime, but only `*_texts` needs an embedder (bring your own via
|
|
74
|
+
`telys.embedding.CallableEmbedder`, or use the on-device bigram embedder).
|
|
75
|
+
|
|
76
|
+
For local development, install the runtime as a package instead of via the CLI:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install "telys[runtime]" # or: pip install telys-runtime
|
|
80
|
+
```
|
telys-0.1.0b1/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# telys
|
|
2
|
+
|
|
3
|
+
**Public, thin SDK** for **Telys** — embedded, on-device memory & retrieval. In-process, **zero cloud
|
|
4
|
+
roundtrips** at query time.
|
|
5
|
+
|
|
6
|
+
This package contains only the developer-facing surface: the `Telys`/`Collection` facades, query/filter
|
|
7
|
+
types, the `EmbeddingProvider` interface, the `Tuner`/`TuningPlan` interfaces, a runtime loader, and the
|
|
8
|
+
`telys` CLI. **It contains no engine implementation** — the engine is a separate, closed, signed, on-device
|
|
9
|
+
runtime fetched by `telys runtime install` (see [DECISIONS D-30](../../docs/DECISIONS.md)).
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
**As a CLI** (recommended — isolated env, on your PATH, not pinned to one Python):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pipx install telys
|
|
17
|
+
# …or the one-liner (installs via pipx):
|
|
18
|
+
curl -fsSL https://telys.ai/install.sh | sh
|
|
19
|
+
|
|
20
|
+
telys login # sign in → free device license + signed runtime; fully offline thereafter
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**As a library** (to `import telys` in your own project):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python -m venv .venv && . .venv/bin/activate
|
|
27
|
+
pip install telys
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> Plain `pip install --user telys` works too, but pip may warn that its user-scripts dir isn't on your
|
|
31
|
+
> PATH (a macOS `--user` quirk) — `pipx` avoids that entirely. Runtime platforms: **macOS arm64, Linux
|
|
32
|
+
> x86_64/arm64** (Windows: run under **WSL2**).
|
|
33
|
+
```python
|
|
34
|
+
from telys import Telys
|
|
35
|
+
db = Telys("./memory")
|
|
36
|
+
col = db.create_collection("docs", dim=768, partition_by="tenant_id")
|
|
37
|
+
col.add(vectors, ids=ids, metadata=metadata) # bring your own vectors (embedding-agnostic)
|
|
38
|
+
hits = col.search(qvec, where={"tenant_id": "acme"}, top_k=10, explain=True)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The **runtime is required for execution**; the **embedder is optional** — `col.add(vectors, …)` and
|
|
42
|
+
`col.add_texts(…)` both need the runtime, but only `*_texts` needs an embedder (bring your own via
|
|
43
|
+
`telys.embedding.CallableEmbedder`, or use the on-device bigram embedder).
|
|
44
|
+
|
|
45
|
+
For local development, install the runtime as a package instead of via the CLI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install "telys[runtime]" # or: pip install telys-runtime
|
|
49
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "telys"
|
|
3
|
+
version = "0.1.0b1"
|
|
4
|
+
description = "Telys — public, thin SDK for embedded on-device memory & retrieval (in-process, zero cloud roundtrips)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "Apache-2.0" }
|
|
8
|
+
# Telys is a product of Thyn (thyn.ai), the parent company.
|
|
9
|
+
authors = [{ name = "Thyn", email = "eng@thyn.ai" }]
|
|
10
|
+
maintainers = [{ name = "Thyn", email = "eng@thyn.ai" }]
|
|
11
|
+
keywords = ["memory", "retrieval", "vector-search", "embeddings", "rag", "on-device", "sdk", "telys"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: Apache Software License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
]
|
|
25
|
+
# Thin SDK: facades, types, provider/tuner interfaces, runtime loader, CLI. The engine is a separate, closed,
|
|
26
|
+
# on-device runtime fetched by `telys runtime install` (D-30) — NOT a dependency of this package.
|
|
27
|
+
dependencies = [
|
|
28
|
+
"numpy>=1.24", # the facade marshals float32 arrays to/from the runtime
|
|
29
|
+
"cryptography>=42", # offline verification of the signed runtime manifest + RS256 license (D-31 #3/#5)
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
# Local development: pull the runtime as an editable/local package instead of `telys runtime install`.
|
|
34
|
+
runtime = ["telys-runtime"]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
telys = "telys.cli:main"
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://telys.ai"
|
|
41
|
+
Documentation = "https://telys.ai"
|
|
42
|
+
Repository = "https://github.com/thyn-ai/telys"
|
|
43
|
+
Issues = "https://github.com/thyn-ai/telys/issues"
|
|
44
|
+
Company = "https://thyn.ai"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["hatchling"]
|
|
48
|
+
build-backend = "hatchling.build"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.wheel]
|
|
51
|
+
packages = ["telys"] # PUBLIC SDK only — no engine modules (enforced by scripts/check_public_wheel.py)
|
|
52
|
+
# Force-include the embedded PUBLIC verification keys: they match the global `*.pem` gitignore (so the wheel
|
|
53
|
+
# would otherwise drop them), but they are public trust anchors that MUST ship. ONLY *_pub.pem — never private.
|
|
54
|
+
artifacts = ["telys/_keys/*_pub.pem"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Telys — embedded, on-device memory & retrieval SDK (public, thin).
|
|
2
|
+
|
|
3
|
+
from telys import Telys, Eq, scope_key
|
|
4
|
+
eng = Telys(path, embedding_providers={...})
|
|
5
|
+
eng.create_collection(name, dim, partition_by, embedder=, dtype=, filter_columns=)
|
|
6
|
+
eng.open_collection(name, embedder=) · eng.collections()
|
|
7
|
+
col.add_texts / add / upsert_texts / upsert
|
|
8
|
+
col.search_text / search (where=, top_k=, explain=, target_recall=, with_metadata=)
|
|
9
|
+
col.ids(where=) # live external ids in a scope (or off-key column)
|
|
10
|
+
col.delete / update_texts / compact / build_ivf / save / stats / snapshot
|
|
11
|
+
|
|
12
|
+
This is the public SDK: facades, types, and provider/tuner interfaces only — no engine implementation. The
|
|
13
|
+
engine is a separate, closed, on-device runtime loaded on first use (`telys runtime install`; see D-30).
|
|
14
|
+
`telys turns filtered vector search into a contiguous memory operation.`
|
|
15
|
+
"""
|
|
16
|
+
def _activate_installed_runtime() -> None:
|
|
17
|
+
"""Zero-config runtime: after a VERIFIED `telys runtime install`, add the installed runtime's Python package
|
|
18
|
+
dir (pysite, from the signed bundle's slim telys-runtime-native wheel) to sys.path — so the native engine and
|
|
19
|
+
kernel-backed embedders import with NO env vars. Only ever prepends a telys-managed, verified path; a silent
|
|
20
|
+
no-op when nothing is installed (dev/editable installs are unaffected)."""
|
|
21
|
+
try:
|
|
22
|
+
import json
|
|
23
|
+
import os
|
|
24
|
+
import sys
|
|
25
|
+
|
|
26
|
+
import telys.paths as _paths
|
|
27
|
+
|
|
28
|
+
record = os.path.join(_paths.install_dir(), _paths.INSTALL_RECORD_NAME)
|
|
29
|
+
with open(record, encoding="utf-8") as fh:
|
|
30
|
+
if not json.load(fh).get("verified"):
|
|
31
|
+
return
|
|
32
|
+
site = _paths.installed_pysite()
|
|
33
|
+
if site and site not in sys.path:
|
|
34
|
+
sys.path.insert(0, site)
|
|
35
|
+
except Exception: # noqa: BLE001 — never let runtime activation break `import telys`
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
_activate_installed_runtime()
|
|
40
|
+
|
|
41
|
+
from telys.engine import AME, Telys, FORMAT_VERSION, __version__, scope_key # noqa: F401,E402
|
|
42
|
+
from telys.filters import Eq # noqa: F401,E402
|
|
43
|
+
from telys.tuning import Tuner, TuningPlan # noqa: F401,E402 (HeuristicTuner is lazy — see __getattr__)
|
|
44
|
+
|
|
45
|
+
__all__ = ["Telys", "AME", "Eq", "scope_key", "Tuner", "HeuristicTuner", "TuningPlan",
|
|
46
|
+
"__version__", "FORMAT_VERSION"]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def __getattr__(name):
|
|
50
|
+
# HeuristicTuner is a closed-runtime implementation; expose it lazily so `import telys` never loads the
|
|
51
|
+
# engine, while `from telys import HeuristicTuner` keeps working when the runtime is installed.
|
|
52
|
+
if name == "HeuristicTuner":
|
|
53
|
+
from telys.tuning import HeuristicTuner
|
|
54
|
+
return HeuristicTuner
|
|
55
|
+
raise AttributeError(f"module 'telys' has no attribute {name!r}")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Embedded Telys verification keys (PUBLIC only)
|
|
2
|
+
|
|
3
|
+
These are the **public** halves of the Telys signing keys, embedded as the default offline trust anchors
|
|
4
|
+
(D-31 #3/#5). `telys.verify` loads them when no `TELYS_RELEASE_PUBKEY[_FILE]` / `TELYS_LICENSE_PUBKEY[_FILE]`
|
|
5
|
+
override is set, so `telys runtime install --file …` verifies with **zero configuration**.
|
|
6
|
+
|
|
7
|
+
| File | Verifies | Signed by (private key, NOT in repo) |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `telys_release_pub.pem` | the runtime **manifest** (artifact SHA-256s) | **Telys release key** (Telys-owned, local) |
|
|
10
|
+
| `telys_license_pub.pem` | the offline **RS256 license** token (ver:2 entitlement) | the **SHARED platform license key** (decision-engine `license-signer` Worker — the SOLE license key for Telys/Algenta/Codna) |
|
|
11
|
+
|
|
12
|
+
**Two different authorities (post platform-convergence):**
|
|
13
|
+
- **RELEASE** key is Telys-owned and signs runtime manifests (`scripts/telys_issuer.py bundle`). Production-real here.
|
|
14
|
+
- **LICENSE** key is NOT Telys-owned — Telys *rides the shared signer*. Real licenses are issued by the
|
|
15
|
+
decision-engine control plane (`api.codna.ai` → the Cloudflare `license-signer` Worker), which alone holds
|
|
16
|
+
the license private key across all products. `telys_license_pub.pem` must therefore be the **decision-engine
|
|
17
|
+
license public key** (export from its JWKS `GET /.well-known/jwks.json` or the operator). `telys.verify`
|
|
18
|
+
checks the engine's ver:2 token (iss `https://license.algenta.ai`, aud `telys-runtime`) against it, offline.
|
|
19
|
+
|
|
20
|
+
**Override:** `TELYS_LICENSE_PUBKEY[_FILE]`, `TELYS_RELEASE_PUBKEY[_FILE]`, and `TELYS_LICENSE_ISSUER` /
|
|
21
|
+
`TELYS_LICENSE_AUDIENCE` env vars override the embedded defaults (rotation / self-hosted / air-gapped).
|
|
22
|
+
|
|
23
|
+
> ⚠️ **`telys_license_pub.pem` is a BOOTSTRAP platform keypair** (generated locally for dev/staging continuity
|
|
24
|
+
> so the offline-license path is exercisable end-to-end before the control plane is live). Its **private half is
|
|
25
|
+
> at `dist-keys/platform_license_priv.pem`** (gitignored, issuer custody — NEVER committed). To make real
|
|
26
|
+
> licenses verify, do ONE of:
|
|
27
|
+
> 1. **Adopt this bootstrap pair (dev/staging):** load `dist-keys/platform_license_priv.pem` into the
|
|
28
|
+
> decision-engine `license-signer` (its `license_signing_private_key` / Cloudflare Worker secret) so the
|
|
29
|
+
> signer mints with the private half of the key embedded here.
|
|
30
|
+
> 2. **Production:** generate a **KMS-born** RS256 key in the control plane, then replace `telys_license_pub.pem`
|
|
31
|
+
> with that signer's public half (export from its JWKS `GET /.well-known/jwks.json`) and delete the bootstrap
|
|
32
|
+
> private from `dist-keys/`.
|
|
33
|
+
>
|
|
34
|
+
> Either way, **one signer authority** holds the license private key across Telys/Algenta/Codna — never mint
|
|
35
|
+
> Telys licenses with a separate Telys-local key (the parallel-signer mistake the convergence removed). The
|
|
36
|
+
> RELEASE key remains Telys-owned and production-real.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Supervisor for the Telys self-host server — run it as a child process and relaunch on abnormal exit.
|
|
2
|
+
|
|
3
|
+
The engine runs in-process, so an (already-rare, after the FFI guards in P0/P1) uncatchable native crash takes
|
|
4
|
+
the daemon down. The supervisor restarts it from the last on-disk snapshot (collections persist and are lazily
|
|
5
|
+
reopened), bounding downtime; periodic save (`telys serve --save-interval`) bounds the data-loss window. A
|
|
6
|
+
clean exit (code 0, e.g. a SIGTERM-drained shutdown) is NOT restarted. A crash loop is capped so the
|
|
7
|
+
supervisor gives up instead of spinning forever.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import signal
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
|
|
18
|
+
_log = logging.getLogger("telys.supervisor")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def supervise(child_argv: list[str], *, env: dict | None = None,
|
|
22
|
+
max_restarts: int = 20, window_s: float = 60.0, backoff_s: float = 1.0) -> int:
|
|
23
|
+
"""Run `python -m telys.cli <child_argv>` as a child, relaunching on abnormal exit.
|
|
24
|
+
|
|
25
|
+
Returns the child's last exit code (0 on a clean drain). Restarts are capped at ``max_restarts`` within
|
|
26
|
+
``window_s`` to avoid a tight crash loop. SIGTERM/SIGINT are forwarded to the child for a graceful drain.
|
|
27
|
+
"""
|
|
28
|
+
cmd = [sys.executable, "-m", "telys.cli", *child_argv]
|
|
29
|
+
child_env = dict(os.environ if env is None else env)
|
|
30
|
+
proc: subprocess.Popen | None = None
|
|
31
|
+
restarts: list[float] = []
|
|
32
|
+
|
|
33
|
+
def _forward(signum, _frame):
|
|
34
|
+
if proc is not None and proc.poll() is None:
|
|
35
|
+
proc.send_signal(signum)
|
|
36
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
37
|
+
try:
|
|
38
|
+
signal.signal(sig, _forward)
|
|
39
|
+
except (ValueError, OSError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
while True:
|
|
43
|
+
_log.info("supervisor: starting child: telys %s", " ".join(child_argv))
|
|
44
|
+
proc = subprocess.Popen(cmd, env=child_env)
|
|
45
|
+
try:
|
|
46
|
+
rc = proc.wait()
|
|
47
|
+
except KeyboardInterrupt:
|
|
48
|
+
proc.send_signal(signal.SIGINT)
|
|
49
|
+
return proc.wait()
|
|
50
|
+
if rc == 0:
|
|
51
|
+
_log.info("supervisor: child exited cleanly (0) — done")
|
|
52
|
+
return 0
|
|
53
|
+
now = time.monotonic()
|
|
54
|
+
restarts = [t for t in restarts if now - t < window_s]
|
|
55
|
+
restarts.append(now)
|
|
56
|
+
if len(restarts) > max_restarts:
|
|
57
|
+
_log.error("supervisor: child crash-looping (%d restarts in %.0fs) — giving up (rc=%d)",
|
|
58
|
+
len(restarts), window_s, rc)
|
|
59
|
+
return rc
|
|
60
|
+
_log.warning("supervisor: child died (rc=%d) — restarting from last snapshot in %.1fs", rc, backoff_s)
|
|
61
|
+
time.sleep(backoff_s)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Tiny length-prefixed JSON wire protocol shared by the Telys self-host server + client.
|
|
2
|
+
|
|
3
|
+
Frame = 4-byte big-endian unsigned length, then that many bytes of UTF-8 JSON. A request is
|
|
4
|
+
``{"op": str, "collection": str|null, "args": {...}}``; a reply is ``{"ok": true, "result": ...}`` or
|
|
5
|
+
``{"ok": false, "error": str}``. Vectors travel as plain JSON number lists (MVP; a binary frame is a later
|
|
6
|
+
optimization). The 4-byte length is bounded to fail closed on a hostile/oversized frame (anti-OOM).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import socket
|
|
12
|
+
import struct
|
|
13
|
+
|
|
14
|
+
MAX_FRAME = 256 * 1024 * 1024 # 256 MiB hard ceiling per message (anti-OOM on a bad/hostile length prefix)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def send_msg(sock: socket.socket, obj) -> None:
|
|
18
|
+
data = json.dumps(obj).encode("utf-8")
|
|
19
|
+
if len(data) > MAX_FRAME:
|
|
20
|
+
raise ValueError(f"frame too large: {len(data)} > {MAX_FRAME}")
|
|
21
|
+
sock.sendall(struct.pack(">I", len(data)) + data)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _recv_exact(sock: socket.socket, n: int) -> bytes:
|
|
25
|
+
buf = bytearray()
|
|
26
|
+
while len(buf) < n:
|
|
27
|
+
chunk = sock.recv(n - len(buf))
|
|
28
|
+
if not chunk:
|
|
29
|
+
raise ConnectionError("peer closed the connection mid-frame")
|
|
30
|
+
buf += chunk
|
|
31
|
+
return bytes(buf)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def recv_msg(sock: socket.socket):
|
|
35
|
+
(n,) = struct.unpack(">I", _recv_exact(sock, 4))
|
|
36
|
+
if n > MAX_FRAME:
|
|
37
|
+
raise ValueError(f"declared frame too large: {n} > {MAX_FRAME}")
|
|
38
|
+
return json.loads(_recv_exact(sock, n).decode("utf-8"))
|