cc-session-control 0.4.0__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.
- cc_session_control/__init__.py +3 -0
- cc_session_control/__main__.py +5 -0
- cc_session_control/actions/__init__.py +0 -0
- cc_session_control/actions/agent_ops.py +201 -0
- cc_session_control/actions/session_ops.py +150 -0
- cc_session_control/app.py +264 -0
- cc_session_control/cli.py +288 -0
- cc_session_control/clipboard.py +44 -0
- cc_session_control/config.py +132 -0
- cc_session_control/data/__init__.py +0 -0
- cc_session_control/data/agents.py +12 -0
- cc_session_control/data/cleanup.py +402 -0
- cc_session_control/data/environments.py +444 -0
- cc_session_control/data/liveness.py +140 -0
- cc_session_control/data/proc.py +214 -0
- cc_session_control/data/rc.py +411 -0
- cc_session_control/data/registry.py +155 -0
- cc_session_control/data/sessions.py +188 -0
- cc_session_control/data/snapshot.py +115 -0
- cc_session_control/models.py +170 -0
- cc_session_control/views/__init__.py +0 -0
- cc_session_control/views/_session_row.py +145 -0
- cc_session_control/views/agents.py +293 -0
- cc_session_control/views/rc.py +374 -0
- cc_session_control/views/sessions.py +595 -0
- cc_session_control-0.4.0.dist-info/METADATA +115 -0
- cc_session_control-0.4.0.dist-info/RECORD +31 -0
- cc_session_control-0.4.0.dist-info/WHEEL +5 -0
- cc_session_control-0.4.0.dist-info/entry_points.txt +2 -0
- cc_session_control-0.4.0.dist-info/licenses/LICENSE +21 -0
- cc_session_control-0.4.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""Cleanup strategies for Claude Code's on-disk state (D6/R7).
|
|
2
|
+
|
|
3
|
+
Two strategies, both preview-first (a `list_*`/`*_stats` read split from the
|
|
4
|
+
matching `remove_*` write, so the view can preview then confirm):
|
|
5
|
+
|
|
6
|
+
- **Strategy A — key-typed orphan sweep.** Key semantics are PER DIRECTORY,
|
|
7
|
+
never a blanket `uuid == sessionId` rule:
|
|
8
|
+
* sid-keyed dirs (`session-env`, `file-history`, `tasks`, `uploads`):
|
|
9
|
+
orphan = an entry whose name (a sessionId) is not in the PROTECTED sid set.
|
|
10
|
+
That set (H1 safety, `known_sids`) is the union of transcript sids,
|
|
11
|
+
registry `sessions/<pid>.json` + `jobs/<short>/state.json` sids, live sids
|
|
12
|
+
(`claude agents --json`, proc-alive, host-alive jobs), and the current
|
|
13
|
+
session — so the sweep never deletes artifacts of a registry-known, live,
|
|
14
|
+
or current session/agent even when its transcript was dropped.
|
|
15
|
+
* pid-keyed dir (`sessions/<pid>.json`): remove only zombies
|
|
16
|
+
(`not pid_alive`), excluding the current session's pid AND any live pid —
|
|
17
|
+
for a resumed multi-pid sid we drop the dead pid files but keep the alive
|
|
18
|
+
one.
|
|
19
|
+
* `debug/`: its uuids are debug-run ids, NOT sessionIds — never treated as
|
|
20
|
+
sid-orphans (it is simply not in the sid-keyed set).
|
|
21
|
+
- **Strategy B — age sweep** for non-session-keyed global dirs
|
|
22
|
+
(`shell-snapshots`, `telemetry`, `plans`, `backups`, `paste-cache`): remove
|
|
23
|
+
entries with an mtime older than `cfg.cleanup_age_days`.
|
|
24
|
+
|
|
25
|
+
`jobs/` is deliberately NOT auto-orphan-swept (only Phase 6's explicit per-job
|
|
26
|
+
remove touches it). All paths come from `cfg.*` props — no inline path joins.
|
|
27
|
+
|
|
28
|
+
R10 safety: when the "current" session can't be determined (no `/proc`),
|
|
29
|
+
destructive ops here refuse (return empty/no-op) rather than fail open — without
|
|
30
|
+
`/proc` every pid looks dead, so a zombie sweep would delete the live/current
|
|
31
|
+
session's files. Strategy B is mtime-only and session-agnostic, so it is not
|
|
32
|
+
gated on `/proc`.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import os
|
|
38
|
+
import shutil
|
|
39
|
+
import time
|
|
40
|
+
from dataclasses import replace
|
|
41
|
+
|
|
42
|
+
from ..config import cfg
|
|
43
|
+
from ..models import AgentJob, Session, SessionProc
|
|
44
|
+
from . import liveness, proc, registry
|
|
45
|
+
|
|
46
|
+
# Dirs keyed by full sessionId — orphan = name not in the known sid set.
|
|
47
|
+
_SID_DIRS = ("session_env", "file_history", "tasks", "uploads")
|
|
48
|
+
# Dirs swept purely by mtime (not session-keyed).
|
|
49
|
+
_AGE_DIRS = ("shell_snapshots", "telemetry", "plans", "backups", "paste_cache")
|
|
50
|
+
|
|
51
|
+
_SECONDS_PER_DAY = 86400
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _sid_dir_paths() -> list[tuple[str, str]]:
|
|
55
|
+
"""(label, path) for each sid-keyed directory, via cfg props only."""
|
|
56
|
+
return [(name.replace("_", "-"), str(getattr(cfg, f"{name}_dir")))
|
|
57
|
+
for name in _SID_DIRS]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _age_dir_paths() -> list[tuple[str, str]]:
|
|
61
|
+
"""(label, path) for each age-swept directory, via cfg props only."""
|
|
62
|
+
return [(name.replace("_", "-"), str(getattr(cfg, f"{name}_dir")))
|
|
63
|
+
for name in _AGE_DIRS]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _sid_keyed_paths(sid: str) -> list[str]:
|
|
67
|
+
"""The sid-keyed artifact dirs (session-env/file-history/tasks/uploads)."""
|
|
68
|
+
return [os.path.join(p, sid) for _, p in _sid_dir_paths()]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _jobs_path(sid: str) -> str:
|
|
72
|
+
"""The 8-char-prefixed `jobs/<short>` dir for a session id."""
|
|
73
|
+
return os.path.join(str(cfg.jobs_dir), sid[:8])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _session_artifact_paths(sid: str) -> list[str]:
|
|
77
|
+
"""All on-disk artifact paths owned by one session id (cfg-derived).
|
|
78
|
+
|
|
79
|
+
Covers the sid-keyed dirs plus the 8-char-prefixed `jobs/<short>` dir for
|
|
80
|
+
this session. Used by `agent_ops.remove_job` (which has already alive-gated
|
|
81
|
+
the job). `remove_session` does NOT use this — it guards the `jobs/<short>`
|
|
82
|
+
path separately so a LIVE agent worker's jobs dir is never deleted (M3).
|
|
83
|
+
"""
|
|
84
|
+
return _sid_keyed_paths(sid) + [_jobs_path(sid)]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _remove_path(path: str) -> bool:
|
|
88
|
+
"""Remove a file or directory; True iff something was removed."""
|
|
89
|
+
if os.path.isdir(path):
|
|
90
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
91
|
+
return True
|
|
92
|
+
if os.path.isfile(path):
|
|
93
|
+
try:
|
|
94
|
+
os.remove(path)
|
|
95
|
+
return True
|
|
96
|
+
except OSError:
|
|
97
|
+
return False
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# --- Strategy A: sid-keyed orphan dirs (H1 protected-sid set) --------------
|
|
102
|
+
|
|
103
|
+
def _live_session_procs(max_age: float = 5.0) -> list[SessionProc]:
|
|
104
|
+
"""Registry session files with `/proc` liveness injected (swallow-error)."""
|
|
105
|
+
try:
|
|
106
|
+
return [
|
|
107
|
+
replace(sp, proc_alive=proc.pid_alive(sp.pid, sp.proc_start))
|
|
108
|
+
for sp in registry.read_session_procs(max_age=max_age)
|
|
109
|
+
]
|
|
110
|
+
except Exception:
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def known_sids(
|
|
115
|
+
sessions: list[Session],
|
|
116
|
+
session_procs: list[SessionProc],
|
|
117
|
+
agent_jobs: list[AgentJob],
|
|
118
|
+
agents_map: dict[str, int | None],
|
|
119
|
+
cur: set[int],
|
|
120
|
+
) -> set[str]:
|
|
121
|
+
"""Sids whose sid-keyed artifacts must NOT be swept (H1 safety) — PURE.
|
|
122
|
+
|
|
123
|
+
A sid-keyed dir is an orphan only when its sid is in NONE of these protected
|
|
124
|
+
sets, so the sweep never deletes artifacts of a registry-known, live, or
|
|
125
|
+
current session/agent (the old `{s.sid for s in sessions}` dropped no-cwd
|
|
126
|
+
bg/bridge stubs and ignored the registry + liveness entirely):
|
|
127
|
+
- transcript scan (`sessions`), incl. the current one
|
|
128
|
+
- registry `sessions/<pid>.json` sids (`session_procs`)
|
|
129
|
+
- registry `jobs/<short>/state.json` sids + resume sids (`agent_jobs`)
|
|
130
|
+
- live per `claude agents --json` (`agents_map`)
|
|
131
|
+
- proc-alive in `session_procs` (defeats pid reuse)
|
|
132
|
+
- host-alive agent jobs
|
|
133
|
+
- the current (csctl-launching) session (`s.current` / pid in `cur`)
|
|
134
|
+
Inputs injected so it stays unit-testable.
|
|
135
|
+
"""
|
|
136
|
+
known: set[str] = {s.sid for s in sessions}
|
|
137
|
+
known |= {s.sid for s in sessions if s.current}
|
|
138
|
+
known |= {sp.sid for sp in session_procs}
|
|
139
|
+
known |= {sp.sid for sp in session_procs if sp.proc_alive}
|
|
140
|
+
known |= {sp.sid for sp in session_procs if sp.pid in cur}
|
|
141
|
+
for j in agent_jobs:
|
|
142
|
+
if j.sid:
|
|
143
|
+
known.add(j.sid)
|
|
144
|
+
if j.resume_sid:
|
|
145
|
+
known.add(j.resume_sid)
|
|
146
|
+
if j.host_alive and j.sid:
|
|
147
|
+
known.add(j.sid)
|
|
148
|
+
known |= {sid for sid in agents_map if sid}
|
|
149
|
+
return known
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _gather_known(
|
|
153
|
+
sessions: list[Session],
|
|
154
|
+
session_procs: list[SessionProc] | None,
|
|
155
|
+
agent_jobs: list[AgentJob] | None,
|
|
156
|
+
agents_map: dict[str, int | None] | None,
|
|
157
|
+
cur: set[int] | None,
|
|
158
|
+
) -> set[str]:
|
|
159
|
+
"""Resolve the protected-sid set, self-fetching any omitted source.
|
|
160
|
+
|
|
161
|
+
Snapshot/view callers inject the shared world data (R11); CLI / no-snapshot
|
|
162
|
+
callers pass nothing and we read the (TTL-cached) registry + `alive_map`. Each
|
|
163
|
+
self-read swallows its own errors → safe empties.
|
|
164
|
+
"""
|
|
165
|
+
if session_procs is None:
|
|
166
|
+
session_procs = _live_session_procs()
|
|
167
|
+
if agent_jobs is None:
|
|
168
|
+
try:
|
|
169
|
+
agent_jobs = registry.read_agent_jobs()
|
|
170
|
+
except Exception:
|
|
171
|
+
agent_jobs = []
|
|
172
|
+
if agents_map is None:
|
|
173
|
+
try:
|
|
174
|
+
agents_map = liveness.alive_map()
|
|
175
|
+
except Exception:
|
|
176
|
+
agents_map = {}
|
|
177
|
+
if cur is None:
|
|
178
|
+
cur = proc.ancestor_pids()
|
|
179
|
+
return known_sids(sessions, session_procs, agent_jobs, agents_map, cur)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def list_orphan_dirs(
|
|
183
|
+
sessions: list[Session],
|
|
184
|
+
*,
|
|
185
|
+
session_procs: list[SessionProc] | None = None,
|
|
186
|
+
agent_jobs: list[AgentJob] | None = None,
|
|
187
|
+
agents_map: dict[str, int | None] | None = None,
|
|
188
|
+
cur: set[int] | None = None,
|
|
189
|
+
) -> list[str]:
|
|
190
|
+
"""Orphan sid-keyed artifact entries (`<dir>/<sid>`), preview list.
|
|
191
|
+
|
|
192
|
+
An entry is an orphan only when its sid is NOT in the protected set (H1).
|
|
193
|
+
Refuses (returns []) when current can't be determined (R10).
|
|
194
|
+
"""
|
|
195
|
+
if not proc.current_determinable():
|
|
196
|
+
return []
|
|
197
|
+
known = _gather_known(sessions, session_procs, agent_jobs, agents_map, cur)
|
|
198
|
+
orphans: list[str] = []
|
|
199
|
+
for label, path in _sid_dir_paths():
|
|
200
|
+
if not os.path.isdir(path):
|
|
201
|
+
continue
|
|
202
|
+
for name in os.listdir(path):
|
|
203
|
+
if name not in known:
|
|
204
|
+
orphans.append(os.path.join(label, name))
|
|
205
|
+
return sorted(set(orphans))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def remove_orphan_dirs(
|
|
209
|
+
sessions: list[Session],
|
|
210
|
+
*,
|
|
211
|
+
session_procs: list[SessionProc] | None = None,
|
|
212
|
+
agent_jobs: list[AgentJob] | None = None,
|
|
213
|
+
agents_map: dict[str, int | None] | None = None,
|
|
214
|
+
cur: set[int] | None = None,
|
|
215
|
+
) -> int:
|
|
216
|
+
"""Delete orphan sid-keyed artifact entries. Refuses without `/proc`.
|
|
217
|
+
|
|
218
|
+
Protects registry-known / live / current sids (H1) — see `known_sids`.
|
|
219
|
+
"""
|
|
220
|
+
if not proc.current_determinable():
|
|
221
|
+
return 0
|
|
222
|
+
known = _gather_known(sessions, session_procs, agent_jobs, agents_map, cur)
|
|
223
|
+
count = 0
|
|
224
|
+
for _label, path in _sid_dir_paths():
|
|
225
|
+
if not os.path.isdir(path):
|
|
226
|
+
continue
|
|
227
|
+
for name in os.listdir(path):
|
|
228
|
+
if name in known:
|
|
229
|
+
continue
|
|
230
|
+
if _remove_path(os.path.join(path, name)):
|
|
231
|
+
count += 1
|
|
232
|
+
return count
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# --- Strategy A: pid-keyed zombie session files ----------------------------
|
|
236
|
+
|
|
237
|
+
def select_zombie_pids(session_procs: list[SessionProc], cur: set[int]) -> list[int]:
|
|
238
|
+
"""Removable `sessions/<pid>.json` pids — PURE (no IO), for unit tests.
|
|
239
|
+
|
|
240
|
+
A pid file is removable iff its proc is dead (`not proc_alive`) and the pid
|
|
241
|
+
is neither the current session's nor a live one. For a resumed multi-pid sid
|
|
242
|
+
this returns only the dead pid(s); the live pid's file is kept because its
|
|
243
|
+
injected `proc_alive` is True. Inputs are injected (proc liveness already on
|
|
244
|
+
each `SessionProc`, `cur` = ancestor-pid set).
|
|
245
|
+
"""
|
|
246
|
+
out: list[int] = []
|
|
247
|
+
for sp in session_procs:
|
|
248
|
+
if sp.pid in cur: # current session's pid file — protected
|
|
249
|
+
continue
|
|
250
|
+
if sp.proc_alive: # live runtime — keep (multi-pid: keep alive pid)
|
|
251
|
+
continue
|
|
252
|
+
out.append(sp.pid)
|
|
253
|
+
return sorted(set(out))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def remove_zombie_session_files(session_procs: list[SessionProc], cur: set[int]) -> int:
|
|
257
|
+
"""Delete zombie `sessions/<pid>.json` files. Refuses without `/proc`."""
|
|
258
|
+
if not proc.current_determinable():
|
|
259
|
+
return 0
|
|
260
|
+
count = 0
|
|
261
|
+
for pid in select_zombie_pids(session_procs, cur):
|
|
262
|
+
if _remove_path(os.path.join(str(cfg.sessions_dir), f"{pid}.json")):
|
|
263
|
+
count += 1
|
|
264
|
+
return count
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# --- Strategy B: age sweep -------------------------------------------------
|
|
268
|
+
|
|
269
|
+
def _age_cutoff(now: float) -> float:
|
|
270
|
+
return now - cfg.cleanup_age_days * _SECONDS_PER_DAY
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def list_aged_entries(now: float | None = None) -> list[str]:
|
|
274
|
+
"""Age-swept entries (`<dir>/<name>`) older than `cfg.cleanup_age_days`."""
|
|
275
|
+
cutoff = _age_cutoff(time.time() if now is None else now)
|
|
276
|
+
out: list[str] = []
|
|
277
|
+
for label, path in _age_dir_paths():
|
|
278
|
+
if not os.path.isdir(path):
|
|
279
|
+
continue
|
|
280
|
+
for name in os.listdir(path):
|
|
281
|
+
try:
|
|
282
|
+
if os.stat(os.path.join(path, name)).st_mtime < cutoff:
|
|
283
|
+
out.append(os.path.join(label, name))
|
|
284
|
+
except OSError:
|
|
285
|
+
pass
|
|
286
|
+
return sorted(out)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def remove_aged_entries(now: float | None = None) -> int:
|
|
290
|
+
"""Delete age-swept entries older than `cfg.cleanup_age_days`."""
|
|
291
|
+
cutoff = _age_cutoff(time.time() if now is None else now)
|
|
292
|
+
count = 0
|
|
293
|
+
for _label, path in _age_dir_paths():
|
|
294
|
+
if not os.path.isdir(path):
|
|
295
|
+
continue
|
|
296
|
+
for name in os.listdir(path):
|
|
297
|
+
full = os.path.join(path, name)
|
|
298
|
+
try:
|
|
299
|
+
if os.stat(full).st_mtime >= cutoff:
|
|
300
|
+
continue
|
|
301
|
+
except OSError:
|
|
302
|
+
continue
|
|
303
|
+
if _remove_path(full):
|
|
304
|
+
count += 1
|
|
305
|
+
return count
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# --- Session prune + full delete -------------------------------------------
|
|
309
|
+
|
|
310
|
+
def prune_sessions(sessions: list[Session], max_prompts: int = 0) -> list[Session]:
|
|
311
|
+
"""Prunable sessions: not alive, not current, <= max_prompts, not recent.
|
|
312
|
+
|
|
313
|
+
Refuses (returns []) when current can't be determined (R10): without `/proc`
|
|
314
|
+
`current` is unreliable, so we must not propose deleting anything.
|
|
315
|
+
"""
|
|
316
|
+
if not proc.current_determinable():
|
|
317
|
+
return []
|
|
318
|
+
alive_sids = {s.sid for s in sessions if s.alive}
|
|
319
|
+
now = time.time()
|
|
320
|
+
return [
|
|
321
|
+
s for s in sessions
|
|
322
|
+
if s.prompts <= max_prompts
|
|
323
|
+
and s.sid not in alive_sids
|
|
324
|
+
and not s.current
|
|
325
|
+
and (now - s.mtime) > 600
|
|
326
|
+
]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def remove_session(s: Session) -> bool:
|
|
330
|
+
"""Delete one session: its `.jsonl`, companion dir, and sid artifacts.
|
|
331
|
+
|
|
332
|
+
Returns True iff something was removed; False when it refused (R10) or there
|
|
333
|
+
was nothing to remove (L4 — the view reports honestly).
|
|
334
|
+
|
|
335
|
+
Refuses (no-op, False) when current can't be determined (R10) — without
|
|
336
|
+
`/proc` we cannot prove `s` is not the launching session.
|
|
337
|
+
|
|
338
|
+
M3: the `jobs/<short>` dir is removed ONLY when the sid has no LIVE host pid,
|
|
339
|
+
so a live background worker's jobs dir is protected exactly like
|
|
340
|
+
`agent_ops.remove_job` protects it (do not bypass the jobs/ guard).
|
|
341
|
+
"""
|
|
342
|
+
if not proc.current_determinable():
|
|
343
|
+
return False
|
|
344
|
+
removed = False
|
|
345
|
+
try:
|
|
346
|
+
os.remove(s.file)
|
|
347
|
+
removed = True
|
|
348
|
+
except OSError:
|
|
349
|
+
pass
|
|
350
|
+
if _remove_path(s.file[:-6]): # companion dir (drop the .jsonl suffix)
|
|
351
|
+
removed = True
|
|
352
|
+
for p in _sid_keyed_paths(s.sid):
|
|
353
|
+
if _remove_path(p):
|
|
354
|
+
removed = True
|
|
355
|
+
# M3: never delete a LIVE agent worker's jobs/<short> dir.
|
|
356
|
+
_, host_alive = registry.host_pid_for_sid(s.sid, _live_session_procs())
|
|
357
|
+
if not host_alive and _remove_path(_jobs_path(s.sid)):
|
|
358
|
+
removed = True
|
|
359
|
+
return removed
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# --- Classified counts -----------------------------------------------------
|
|
363
|
+
|
|
364
|
+
def cleanup_stats(sessions: list[Session]) -> dict[str, int]:
|
|
365
|
+
"""Summary counts for the Sessions cleanup submenu (view-facing contract).
|
|
366
|
+
|
|
367
|
+
Keeps the established 4-key shape (`total/empty/short/orphans`) that the
|
|
368
|
+
view reads; `orphans` is the sid-keyed orphan count (Strategy A).
|
|
369
|
+
"""
|
|
370
|
+
return {
|
|
371
|
+
"total": len(sessions),
|
|
372
|
+
"empty": sum(1 for s in sessions if s.prompts == 0),
|
|
373
|
+
"short": sum(1 for s in sessions if 0 < s.prompts <= 2),
|
|
374
|
+
"orphans": len(list_orphan_dirs(sessions)),
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def cleanup_classified(
|
|
379
|
+
sessions: list[Session],
|
|
380
|
+
session_procs: list[SessionProc],
|
|
381
|
+
cur: set[int],
|
|
382
|
+
agent_jobs: list[AgentJob] | None = None,
|
|
383
|
+
agents_map: dict[str, int | None] | None = None,
|
|
384
|
+
now: float | None = None,
|
|
385
|
+
) -> dict[str, int]:
|
|
386
|
+
"""Per-category cleanup counts (D6). Deps injected so it stays unit-testable.
|
|
387
|
+
|
|
388
|
+
Breaks the cleanup surface into its categories — empty/short sessions,
|
|
389
|
+
sid-keyed orphan dirs, pid-keyed zombie session files, and age-swept global
|
|
390
|
+
entries — for the (Phase 7) workbench view to surface. The shared world data
|
|
391
|
+
(`session_procs`/`agent_jobs`/`agents_map`/`cur`) feeds the H1 protected-sid
|
|
392
|
+
set so the orphan count never includes live/registry-known sids.
|
|
393
|
+
"""
|
|
394
|
+
return {
|
|
395
|
+
"empty": sum(1 for s in sessions if s.prompts == 0),
|
|
396
|
+
"short": sum(1 for s in sessions if 0 < s.prompts <= 2),
|
|
397
|
+
"orphan_dirs": len(list_orphan_dirs(
|
|
398
|
+
sessions, session_procs=session_procs, agent_jobs=agent_jobs,
|
|
399
|
+
agents_map=agents_map, cur=cur)),
|
|
400
|
+
"zombie_procs": len(select_zombie_pids(session_procs, cur)),
|
|
401
|
+
"aged_entries": len(list_aged_entries(now)),
|
|
402
|
+
}
|