leakscope 0.2.0rc1__py3-none-any.whl

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.
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: leakscope
3
+ Version: 0.2.0rc1
4
+ Summary: LeakScope Python collector and CLI; native executables and web assets are installed separately
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/polsala/LeakScope
7
+ Project-URL: Documentation, https://github.com/polsala/LeakScope/tree/main/docs
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: fastapi==0.143.1
12
+ Requires-Dist: uvicorn==0.34.3
13
+ Requires-Dist: pydantic==2.11.7
14
+ Requires-Dist: sqlalchemy==2.0.41
15
+ Requires-Dist: alembic==1.16.2
16
+ Requires-Dist: zstandard==0.23.0
17
+ Requires-Dist: protobuf==6.33.5
18
+ Requires-Dist: PyYAML==6.0.2
19
+ Requires-Dist: httpx==0.28.1
20
+ Requires-Dist: starlette==1.8.0
21
+ Dynamic: license-file
22
+
23
+ # LeakScope Python component
24
+
25
+ This distribution installs the Python collector, FastAPI backend, CLI and Alembic
26
+ migrations. It requires Python 3.11 or newer. It does **not** bundle the Rust sensor,
27
+ Rust analyzer, web assets or CPython 2.7 compatibility extension, and performs no
28
+ executable downloads during installation.
29
+
30
+ `leakscope --version`, offline capture inspection and database backup work through
31
+ this package. Recording and native allocation analysis require separately installed
32
+ matching `leakscope-sensor` and `leakscope-analyzer` executables on PATH (or the
33
+ explicit LEAKSCOPE_SENSOR_BIN / LEAKSCOPE_ANALYZER_BIN settings). Serving the UI
34
+ requires LEAKSCOPE_WEB_DIST pointing to matching built assets. For the complete
35
+ application, use the verified offline standalone tarball or Debian distribution
36
+ from https://github.com/polsala/LeakScope/releases.
37
+
38
+ Engineering previews do not claim production readiness. Native tracing is opt-in;
39
+ metrics remain available without BPF. Python 2.7 is used only by the separate
40
+ compatibility agent and optional laboratory interpreter.
@@ -0,0 +1,32 @@
1
+ leakscope-0.2.0rc1.dist-info/licenses/LICENSE,sha256=crLYarT66hziCSc788sNGeGffBWd2JhNLFI3kmHgJF8,1062
2
+ leakscope_api/__init__.py,sha256=W0B2d4Yb04BxsclJNHF-NtDsaubMhwcwrrYeRkH7dZg,72
3
+ leakscope_api/agent.py,sha256=e-r9a8xWiBnL6z9XF4O6H0gMWLBf9Fbfr-fL_-PeXHk,21648
4
+ leakscope_api/analysis.py,sha256=VfYsOJjDSIy2etB3opqPt0CKQ48eYRwmNo_Od6Roagc,3743
5
+ leakscope_api/app.py,sha256=GFqfWvhj76q5TsypyrJPeHqh7QiySGNFwfJ9I2Ko3Hs,67566
6
+ leakscope_api/capture.py,sha256=d5Onla3Mm5mCzk_QySSxa5z6-3Aw7gNUB2EaVifLMcc,6312
7
+ leakscope_api/cli.py,sha256=l99_fEPJRHLN-gEBijvA2ZSS25d0qa9WJPbQnOf-voE,22636
8
+ leakscope_api/config.py,sha256=n1dSSjPSkx6G6YujtnZ2-ai69FwtFO8NSa-GwNIRxA8,3598
9
+ leakscope_api/context_capture.py,sha256=meKvR2n-dvYx03qiZJYifx-b6--BoNi8W_5aaX1KVuw,2238
10
+ leakscope_api/contracts.py,sha256=qAmn7x6cG4vIsPepkUwqwFHMEOJxkrTLeC8glKhrarI,6421
11
+ leakscope_api/db.py,sha256=SJPIxqHNUXTXlbkeLUF6DqX46mvM7NdWhtEO5Oh_9FE,2438
12
+ leakscope_api/deployment.py,sha256=Z1WvZGGG3JCq4pr41qyswZa1tnIDKfEnUOYxU1kKQlQ,1534
13
+ leakscope_api/healthcheck.py,sha256=1UGD4vLEEwYqiicsYZm3SxoJs1EAGgFMQ5gpFMqu3UQ,3009
14
+ leakscope_api/investigation.py,sha256=XE6ez5_bpOy6pK6ujJZq7BewutzoqN3B9rOFk2AsB3Q,20790
15
+ leakscope_api/limits.py,sha256=DzyG6Sb1dr0E02qNlm76aKL3EDUbLjfxml8TWiuA9yY,1514
16
+ leakscope_api/models.py,sha256=ecboN_OdY-RA-wFLpS4P_JIp4qTubRnztUnXZQBq_Xg,8404
17
+ leakscope_api/monitoring.py,sha256=Da_-0JrFw5HeA1YoI9t1steSHeBpOVDR7C0UL-Mp4s0,25421
18
+ leakscope_api/native.py,sha256=zwAzItRjO3ytUqnyQEY_SZ5FOkLxJW1dgzsLcFVGNJ0,5203
19
+ leakscope_api/readiness.py,sha256=uYkrtwOiygYae0bLVMp0iqTMUq-DPlKzZ8RRv1E4DsQ,8385
20
+ leakscope_api/reports.py,sha256=VOgB6lroG7i6qzQUcPvaaLR1CjTnNoDx1gYnspxR3N0,6186
21
+ leakscope_api/sensor.py,sha256=TsNLlGECLtBoAyZlW8YYf3wHlxwYYFADrEJU33gViQc,2357
22
+ leakscope_api/service.py,sha256=8rrLqXrRjSnl0q_SI4TJJybGxN65A8uQUjZH5M9dHPM,29722
23
+ leakscope_api/storage.py,sha256=6HVCVoYKAj9U8tH3FDJQQ6v9j17bNVhJNmAVpAiJx0U,4639
24
+ leakscope_api/migrations/env.py,sha256=JulslKQ1Wm5YIMqrhaGHU_rMoUrtuPEFXQJWsFqP_Gw,466
25
+ leakscope_api/migrations/versions/0001_initial.py,sha256=cMX2Hu9eH7BvOP1HNAWDBlahjTvIjKuhpBhy1dj5ot8,347
26
+ leakscope_api/migrations/versions/0002_agent_evidence.py,sha256=7ZFobSRz3HlDuFpPzwSyfciny1g5E94MB-EZXrPEHJk,447
27
+ leakscope_api/migrations/versions/0003_monitoring.py,sha256=3sBKEVVUN9N4ryFZxZsu7NFlHB34TbYFWEWoj1S6EGQ,678
28
+ leakscope-0.2.0rc1.dist-info/METADATA,sha256=t_SR0MgRtlEqT9uH25kG7bslkgafJzAG3RPoRClqQpQ,1832
29
+ leakscope-0.2.0rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
+ leakscope-0.2.0rc1.dist-info/entry_points.txt,sha256=0HOaZN1Iyyw_YoW7uepQx852ft60dk5Ey31f2YoTQmE,53
31
+ leakscope-0.2.0rc1.dist-info/top_level.txt,sha256=QEQSR0sZ5p2cA5GEVkdXbbI4prCISwD5qU4rWcj2m4k,14
32
+ leakscope-0.2.0rc1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ leakscope = leakscope_api.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PSala
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ leakscope_api
@@ -0,0 +1,3 @@
1
+ """LeakScope collector and standalone CLI."""
2
+
3
+ __version__ = "0.2.0rc1"
leakscope_api/agent.py ADDED
@@ -0,0 +1,477 @@
1
+ """Receive opt-in cooperative safe-point diagnostics, authenticated by Linux peer credentials."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import os
6
+ import socket
7
+ import struct
8
+ import time
9
+ import uuid
10
+ from collections import deque
11
+ from typing import Literal
12
+ from pydantic import Field
13
+ from sqlalchemy import select, func
14
+ from .config import Strict
15
+ from .contracts import MarkerCreate
16
+ from . import models as m
17
+
18
+
19
+ class Packet(Strict):
20
+ version: Literal[1]
21
+ pid: int = Field(ge=1, le=2**31 - 1)
22
+ start_ticks: int = Field(ge=0)
23
+ sequence: int = Field(ge=1, le=2**63 - 1)
24
+ thread: int = Field(ge=0)
25
+ timestamp: float = Field(ge=0, allow_inf_nan=False)
26
+ monotonic_ns: int | None = Field(None, ge=0, le=2**63 - 1)
27
+ dropped: int = Field(0, ge=0)
28
+ kind: Literal[
29
+ "hello",
30
+ "marker",
31
+ "gc_snapshot",
32
+ "operation",
33
+ "python_codes",
34
+ "python_profile",
35
+ "lab_stack",
36
+ "lab_stacks",
37
+ "lab_events",
38
+ "lab_end",
39
+ ]
40
+ payload: dict
41
+
42
+
43
+ class TypeCount(Strict):
44
+ count: int = Field(ge=0, le=50000)
45
+ shallow_bytes_estimate: int = Field(ge=0, le=2**63 - 1)
46
+
47
+
48
+ class Snapshot(Strict):
49
+ name: str = Field(min_length=1, max_length=80)
50
+ visibility: Literal["gc_generation_zero_only"]
51
+ visited: int = Field(ge=0, le=50000)
52
+ duration_ms: float = Field(ge=0, le=1000, allow_inf_nan=False)
53
+ partial: bool
54
+ types: dict[str, TypeCount] = Field(max_length=128)
55
+
56
+
57
+ class Operation(Strict):
58
+ id: str = Field(pattern=r"^[a-f0-9-]{1,100}$")
59
+ parent: str | None = Field(None, pattern=r"^[a-f0-9-]{1,100}$")
60
+ operation: str = Field(pattern=r"^[A-Za-z_][A-Za-z0-9_.: -]{0,79}$")
61
+ module: str | None = Field(None, pattern=r"^[A-Za-z_][A-Za-z0-9_.: -]{0,79}$")
62
+ model: str | None = Field(None, pattern=r"^[A-Za-z_][A-Za-z0-9_.: -]{0,79}$")
63
+ method: str | None = Field(None, pattern=r"^[A-Za-z_][A-Za-z0-9_.: -]{0,79}$")
64
+ depth: int = Field(ge=0, le=31)
65
+ thread: int = Field(ge=1, le=2**31 - 1)
66
+ start_ns: int = Field(ge=0, le=2**63 - 1)
67
+ end_ns: int | None = Field(None, ge=0, le=2**63 - 1)
68
+ status: Literal["running", "completed", "error", "cancelled"]
69
+ evidence: Literal["exact_operation_boundary"]
70
+
71
+
72
+ class PythonCode(Strict):
73
+ id: int = Field(ge=1, le=512)
74
+ file: str = Field(max_length=256)
75
+ function: str = Field(max_length=96)
76
+ first_line: int = Field(ge=0)
77
+
78
+
79
+ class PythonCodes(Strict):
80
+ epoch: int = Field(ge=0, le=2**63 - 1)
81
+ codes: list[PythonCode] = Field(max_length=16)
82
+
83
+
84
+ class PythonBoundary(Strict):
85
+ activation: int = Field(0, ge=0, le=2**63 - 1)
86
+ sequence: int = Field(ge=1, le=2**63 - 1)
87
+ monotonic_ns: int = Field(ge=0, le=2**63 - 1)
88
+ thread: int = Field(ge=1, le=2**31 - 1)
89
+ event: Literal[0, 3, 4, 5, 6, 7, 8]
90
+ frames: list[tuple[int, int]] = Field(max_length=16)
91
+
92
+
93
+ class PythonProfile(Strict):
94
+ expired: int = Field(0, ge=0)
95
+ epoch: int = Field(ge=0, le=2**63 - 1)
96
+ lost: int = Field(ge=0, le=2**63 - 1)
97
+ evidence: Literal["function_boundary"]
98
+ events: list[PythonBoundary] = Field(max_length=64)
99
+
100
+
101
+ class LabFrame(Strict):
102
+ file: str = Field(max_length=192)
103
+ module: str = Field(max_length=192)
104
+ function: str = Field(max_length=64)
105
+ line: int = Field(ge=0)
106
+ first_line: int = Field(ge=0)
107
+ evidence: Literal["verified_lab_allocation_frame"]
108
+
109
+
110
+ class LabStack(Strict):
111
+ epoch: int = Field(0, ge=0, le=2**63 - 1)
112
+ id: int = Field(ge=1, le=512)
113
+ flags: int = Field(ge=0, le=7)
114
+ frames: list[LabFrame] = Field(max_length=8)
115
+
116
+
117
+ class LabStacks(Strict):
118
+ epoch: int = Field(ge=1, le=2**63 - 1)
119
+ stacks: list[LabStack] = Field(max_length=8)
120
+
121
+
122
+ class LabEvents(Strict):
123
+ unknown_frees: int = Field(0, ge=0, le=2**63 - 1)
124
+ stop_reason: Literal[0, 1, 2] = 0
125
+ epoch: int = Field(ge=1, le=2**63 - 1)
126
+ events: list[tuple[int, int, int, int, int, int, int, int, int, int, int, int]] = Field(
127
+ max_length=64
128
+ )
129
+ lost: int = Field(ge=0, le=2**63 - 1)
130
+
131
+
132
+ class LabEnd(Strict):
133
+ epoch: int = Field(ge=1, le=2**63 - 1)
134
+ last_sequence: int = Field(ge=0, le=2**63 - 1)
135
+ delivered: bool
136
+
137
+
138
+ def same_clock_namespace(pid):
139
+ try:
140
+ own = os.stat("/proc/self/ns/time")
141
+ target = os.stat(f"/proc/{pid}/ns/time")
142
+ return (own.st_dev, own.st_ino) == (target.st_dev, target.st_ino)
143
+ except FileNotFoundError:
144
+ return not os.path.exists("/proc/self/ns/time")
145
+ except PermissionError:
146
+ return False
147
+
148
+
149
+ def validate_evidence(kind, payload):
150
+ if kind == "operation":
151
+ return Operation.model_validate(payload).model_dump()
152
+ if kind == "python_codes":
153
+ return PythonCodes.model_validate(payload).model_dump()
154
+ if kind == "python_profile":
155
+ return PythonProfile.model_validate(payload).model_dump()
156
+ if kind == "lab_stack":
157
+ return LabStack.model_validate(payload).model_dump()
158
+ if kind == "lab_stacks":
159
+ return LabStacks.model_validate(payload).model_dump()
160
+ if kind == "lab_end":
161
+ return LabEnd.model_validate(payload).model_dump()
162
+ if kind == "lab_events":
163
+ result = LabEvents.model_validate(payload).model_dump()
164
+ for row in result["events"]:
165
+ if (
166
+ any(v < 0 or v > 2**64 - 1 for v in row)
167
+ or row[6] not in {1, 2, 3}
168
+ or row[8] > 512
169
+ or row[10] > 7
170
+ ):
171
+ raise ValueError("Invalid Lab native record")
172
+ return result
173
+ raise ValueError("Unsupported cooperative evidence kind")
174
+
175
+
176
+ async def receive_agents(collector, path):
177
+ if path.exists():
178
+ path.unlink()
179
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
180
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_PASSCRED, 1)
181
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 256 * 1024)
182
+ sock.bind(str(path))
183
+ path.chmod(0o600)
184
+ sock.settimeout(0.25)
185
+ rates: dict[int, deque] = {}
186
+ inbox: asyncio.Queue = asyncio.Queue(maxsize=128) # <=4 MiB of datagrams
187
+
188
+ async def read_packets():
189
+ while True:
190
+ try:
191
+ packet = await asyncio.to_thread(sock.recvmsg, 32768, socket.CMSG_SPACE(12))
192
+ try:
193
+ inbox.put_nowait(packet)
194
+ except asyncio.QueueFull:
195
+ collector.agent_dropped += 1
196
+ except socket.timeout:
197
+ continue
198
+
199
+ collector.agent_inbox = inbox
200
+ reader = asyncio.create_task(read_packets())
201
+ try:
202
+ while True:
203
+ raw, ancillary, flags, _ = await inbox.get()
204
+ try:
205
+ peer = next(
206
+ struct.unpack("3i", value)
207
+ for level, kind, value in ancillary
208
+ if level == socket.SOL_SOCKET and kind == socket.SCM_CREDENTIALS
209
+ )
210
+ packet = Packet.model_validate_json(raw)
211
+ if flags & socket.MSG_TRUNC or peer[0] != packet.pid or peer[1] != os.getuid():
212
+ continue
213
+ now = time.monotonic()
214
+ if packet.pid not in rates:
215
+ if len(rates) >= 128:
216
+ rates.pop(next(iter(rates)))
217
+ rates[packet.pid] = deque()
218
+ rate = rates[packet.pid]
219
+ while rate and rate[0] < now - 1:
220
+ rate.popleft()
221
+ if len(rate) >= 100:
222
+ collector.agent_dropped += 1
223
+ continue
224
+ rate.append(now)
225
+ process = await collector.call("sample", pid=packet.pid)
226
+ if process["identity"]["start_ticks"] != packet.start_ticks:
227
+ continue
228
+ key = process["key"]
229
+ clock_compatible = same_clock_namespace(packet.pid)
230
+ if key not in collector.agents and len(collector.agents) >= 128:
231
+ collector.agents.pop(next(iter(collector.agents)))
232
+ old = collector.agents.get(key)
233
+ if old and packet.sequence <= old["sequence"]:
234
+ continue
235
+ collector.agents[key] = {
236
+ "operations_completed": (old or {}).get("operations_completed", 0),
237
+ "context_incomplete": (old or {}).get("context_incomplete", False)
238
+ or packet.dropped > 0
239
+ or bool(old and packet.sequence != old["sequence"] + 1),
240
+ "last_seen": time.time(),
241
+ "sequence": packet.sequence,
242
+ "visibility": "gc_generation_zero_only",
243
+ "operation_context": True,
244
+ "native_clock_correlation": clock_compatible,
245
+ "lab": packet.payload.get("lab_object_blocks", False)
246
+ if packet.kind == "hello"
247
+ else (old or {}).get("lab", False),
248
+ "python_function_boundaries": packet.kind
249
+ in {"python_codes", "python_profile", "lab_stack", "lab_events"}
250
+ or (old or {}).get("python_function_boundaries", False),
251
+ }
252
+ if packet.kind == "operation":
253
+ operation = Operation.model_validate(packet.payload)
254
+ if operation.end_ns is not None:
255
+ collector.agents[key]["operations_completed"] += 1
256
+ with collector.db.begin() as db:
257
+ sessions = list(
258
+ db.scalars(
259
+ select(m.Session).where(
260
+ m.Session.process_key == key,
261
+ m.Session.state.in_(["recording", "stopping"]),
262
+ )
263
+ )
264
+ )
265
+ for s in sessions:
266
+ if (
267
+ s.quality.get("coverage") == "native_observed"
268
+ and not clock_compatible
269
+ and packet.kind in {"operation", "python_codes", "python_profile"}
270
+ ):
271
+ s.quality = {
272
+ **s.quality,
273
+ "context_incomplete": True,
274
+ "context_failure": "Different or inaccessible Linux time namespaces; native/Python temporal correlation disabled",
275
+ }
276
+ continue
277
+ # Independent caps on opt-in diagnostics. A faulty agent cannot fill storage.
278
+ if packet.kind in {
279
+ "operation",
280
+ "python_codes",
281
+ "python_profile",
282
+ "lab_stack",
283
+ "lab_stacks",
284
+ "lab_events",
285
+ "lab_end",
286
+ }:
287
+ if packet.kind.startswith("lab_") and s.config["mode"] != "lab":
288
+ continue
289
+ payload = validate_evidence(packet.kind, packet.payload)
290
+ if packet.kind == "operation" and (
291
+ payload["thread"] != packet.thread
292
+ or (
293
+ payload["end_ns"] is not None
294
+ and payload["end_ns"] < payload["start_ns"]
295
+ )
296
+ ):
297
+ continue
298
+ count, size = db.execute(
299
+ select(
300
+ func.count(), func.coalesce(func.sum(m.AgentRecord.size), 0)
301
+ ).where(m.AgentRecord.session_id == s.id)
302
+ ).one()
303
+ quota = min(
304
+ 16 * 1024 * 1024, s.config["max_capture_mb"] * 1024 * 1024 // 4
305
+ )
306
+ if (
307
+ count >= 2048
308
+ or size + len(raw) > quota
309
+ or collector.disk_usage() + len(raw)
310
+ > collector.config.max_storage_mb * 1024**2
311
+ ):
312
+ collector.agent_dropped += 1
313
+ s.quality = {
314
+ **s.quality,
315
+ "context_incomplete": True,
316
+ "context_limit": "cooperative evidence quota",
317
+ }
318
+ continue
319
+ if packet.kind in {"lab_stacks", "lab_events", "lab_end"}:
320
+ epoch = payload["epoch"]
321
+ epochs = s.quality.get("lab_epochs", [])
322
+ if epoch not in epochs:
323
+ epochs = epochs + [epoch]
324
+ s.quality = {
325
+ **s.quality,
326
+ "lab_epochs": epochs[:16],
327
+ "lab_last_sequence": 0,
328
+ "lab_complete": False,
329
+ "incomplete": s.quality.get("incomplete", False)
330
+ or len(epochs) > 1,
331
+ }
332
+ if len(epochs) > 1:
333
+ s.quality = {
334
+ **s.quality,
335
+ "lab_boundary": "Multiple Lab activations; unobserved gaps suppress exact end survivors",
336
+ }
337
+ gap = bool(old and packet.sequence != old["sequence"] + 1)
338
+ s.quality = {
339
+ **s.quality,
340
+ "python_context": True,
341
+ "context_incomplete": s.quality.get("context_incomplete", False)
342
+ or gap
343
+ or packet.dropped > 0
344
+ or (packet.kind == "python_profile" and payload["lost"] > 0),
345
+ "agent_dropped": packet.dropped,
346
+ "python_profile_expired": max(
347
+ s.quality.get("python_profile_expired", 0),
348
+ payload.get("expired", 0)
349
+ if packet.kind == "python_profile"
350
+ else 0,
351
+ ),
352
+ "lab_complete": s.quality.get("lab_complete", False)
353
+ or (
354
+ packet.kind == "lab_end"
355
+ and payload["delivered"]
356
+ and packet.dropped == 0
357
+ and payload["last_sequence"]
358
+ == s.quality.get("lab_last_sequence", 0)
359
+ ),
360
+ "lab_last_sequence": max(
361
+ s.quality.get("lab_last_sequence", 0),
362
+ max((row[0] for row in payload["events"]), default=0)
363
+ if packet.kind == "lab_events"
364
+ else 0,
365
+ ),
366
+ "lab_stop_reason": payload.get("stop_reason", 0)
367
+ if packet.kind == "lab_events"
368
+ else s.quality.get("lab_stop_reason", 0),
369
+ "lab_unknown_frees": max(
370
+ s.quality.get("lab_unknown_frees", 0),
371
+ payload.get("unknown_frees", 0),
372
+ ),
373
+ "python_profile_lost": max(
374
+ s.quality.get("python_profile_lost", 0),
375
+ payload.get("lost", 0)
376
+ if packet.kind == "python_profile"
377
+ else 0,
378
+ ),
379
+ "lost_events": max(
380
+ s.quality.get("lost_events", 0),
381
+ payload.get("lost", 0) if packet.kind == "lab_events" else 0,
382
+ ),
383
+ "lab_unattributed_events": s.quality.get(
384
+ "lab_unattributed_events", 0
385
+ )
386
+ + sum(bool(row[10] & 5) for row in payload.get("events", []))
387
+ if packet.kind == "lab_events"
388
+ else s.quality.get("lab_unattributed_events", 0),
389
+ "lab_events": s.quality.get("lab_events", 0)
390
+ + (len(payload["events"]) if packet.kind == "lab_events" else 0),
391
+ "incomplete": s.quality.get("incomplete", False)
392
+ or (packet.kind == "lab_events" and payload["lost"] > 0),
393
+ }
394
+ db.add(
395
+ m.AgentRecord(
396
+ session_id=s.id,
397
+ agent_sequence=packet.sequence,
398
+ timestamp=packet.timestamp,
399
+ size=len(raw),
400
+ data={
401
+ "kind": packet.kind,
402
+ "process_key": key,
403
+ "thread": packet.thread,
404
+ "monotonic_ns": packet.monotonic_ns,
405
+ "timestamp": packet.timestamp,
406
+ "payload": payload,
407
+ },
408
+ )
409
+ )
410
+ elif packet.kind == "gc_snapshot":
411
+ snapshot = Snapshot.model_validate(packet.payload)
412
+ if any(
413
+ len(name) > 96 or not name.isprintable() for name in snapshot.types
414
+ ):
415
+ continue
416
+ count = db.scalar(
417
+ select(func.count())
418
+ .select_from(m.GCTypeSnapshot)
419
+ .where(m.GCTypeSnapshot.session_id == s.id)
420
+ )
421
+ if count >= 100:
422
+ collector.agent_dropped += 1
423
+ continue
424
+ db.add(
425
+ m.GCTypeSnapshot(
426
+ id=uuid.uuid4().hex,
427
+ session_id=s.id,
428
+ timestamp=packet.timestamp,
429
+ snapshot=snapshot.model_dump(),
430
+ )
431
+ )
432
+ elif packet.kind == "marker":
433
+ allowed = {
434
+ "operation",
435
+ "module",
436
+ "model",
437
+ "method",
438
+ "phase",
439
+ "worker_type",
440
+ }
441
+ metadata = packet.payload.get("metadata", {})
442
+ if not isinstance(metadata, dict) or set(metadata) - allowed:
443
+ continue
444
+ marker = MarkerCreate(
445
+ name=packet.payload["name"], operation=metadata.get("operation")
446
+ )
447
+ count = db.scalar(
448
+ select(func.count())
449
+ .select_from(m.Marker)
450
+ .where(m.Marker.session_id == s.id)
451
+ )
452
+ if count >= 1000:
453
+ collector.agent_dropped += 1
454
+ continue
455
+ db.add(
456
+ m.Marker(
457
+ id=uuid.uuid4().hex,
458
+ session_id=s.id,
459
+ timestamp=packet.timestamp,
460
+ name=marker.name,
461
+ metadata_json={
462
+ "operation": marker.operation,
463
+ "source": "cooperative_agent",
464
+ "thread": packet.thread,
465
+ },
466
+ )
467
+ )
468
+ except (ValueError, KeyError, StopIteration, RuntimeError, OSError):
469
+ collector.agent_dropped += 1
470
+ finally:
471
+ inbox.task_done()
472
+ finally:
473
+ reader.cancel()
474
+ with contextlib.suppress(asyncio.CancelledError):
475
+ await reader
476
+ sock.close()
477
+ path.unlink(missing_ok=True)