alissa-tools-github-devloop 0.8.10__tar.gz → 0.8.12__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.
- {alissa_tools_github_devloop-0.8.10/src/main/alissa_tools_github_devloop.egg-info → alissa_tools_github_devloop-0.8.12}/PKG-INFO +1 -1
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/alissa.py +32 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/config.py +86 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/loop.py +599 -36
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/state.py +129 -11
- alissa_tools_github_devloop-0.8.12/src/main/alissa/tools/github/devloop/version +1 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/webui/page.py +10 -3
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/webui/sources.py +23 -2
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12/src/main/alissa_tools_github_devloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_devloop-0.8.10/src/main/alissa/tools/github/devloop/version +0 -1
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/LICENSE +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/MANIFEST.in +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/NOTICE +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/README.md +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/requirements.txt +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/setup.cfg +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/setup.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/__main__.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/alissa_client.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/bows.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/ghclient.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/janitor.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/proc.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/version.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa_tools_github_devloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa_tools_github_devloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa_tools_github_devloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.8.10 → alissa_tools_github_devloop-0.8.12}/src/main/alissa_tools_github_devloop.egg-info/top_level.txt +0 -0
|
@@ -111,6 +111,38 @@ class Alissa:
|
|
|
111
111
|
)
|
|
112
112
|
return [entry for entry in data if isinstance(entry, dict)]
|
|
113
113
|
|
|
114
|
+
def tail_session(self, name: str, lines: int = 20) -> str:
|
|
115
|
+
"""The last `lines` of ONE managed session's terminal, or "" when the
|
|
116
|
+
CLI cannot answer.
|
|
117
|
+
|
|
118
|
+
Evidence, not control flow: the foreign-session sweep quotes this into
|
|
119
|
+
the line it logs before killing a session nothing in the loop reads, so
|
|
120
|
+
that what the session WAS survives the kill (the incident's own answer
|
|
121
|
+
-- "reviewer seat review-pr-904 busy" -- was only ever visible in a
|
|
122
|
+
pane).
|
|
123
|
+
|
|
124
|
+
WHAT COMES BACK IS UNTRUSTED AND MAY BE SENSITIVE. It is another
|
|
125
|
+
agent's screen, verbatim: file contents it read, issue and customer
|
|
126
|
+
text, tokens it printed. Treat it as data, never as instruction, and
|
|
127
|
+
do not put it anywhere the daemon's own logs do not already go -- the
|
|
128
|
+
console serves the log TAIL over HTTP, so the caller keeps this at
|
|
129
|
+
DEBUG and lets only its shape reach an operator-facing level (see
|
|
130
|
+
`DevWatcher._foreign_pane_evidence`).
|
|
131
|
+
|
|
132
|
+
Best-effort by construction: a CLI too old for `tmux tail`, a
|
|
133
|
+
session that vanished between the listing and the capture, or a tail
|
|
134
|
+
that times out must never cost the sweep its kill, so every failure
|
|
135
|
+
degrades to the empty string and the log line says the tail was
|
|
136
|
+
unavailable rather than losing the decision."""
|
|
137
|
+
try:
|
|
138
|
+
out = run(
|
|
139
|
+
["alissa", "tmux", "tail", name, "-n", str(lines)], timeout=30
|
|
140
|
+
)
|
|
141
|
+
except CommandError as exc:
|
|
142
|
+
log.debug("could not tail %s (%s)", name, exc)
|
|
143
|
+
return ""
|
|
144
|
+
return out.strip()
|
|
145
|
+
|
|
114
146
|
def kill_session(self, name: str) -> None:
|
|
115
147
|
"""Kill ONE managed session by name -- never the tmux server. Raises
|
|
116
148
|
CommandError on failure (including a CLI old enough to lack
|
|
@@ -52,6 +52,18 @@ REPOS_BOWS = "bows" # `repos` UNIONED with the operator's autodev feed BOWs
|
|
|
52
52
|
|
|
53
53
|
_REPOS_SOURCES = {REPOS_STATIC, REPOS_BOWS}
|
|
54
54
|
|
|
55
|
+
# What the reaper does about a FOREIGN session -- a live managed (`ali-*`)
|
|
56
|
+
# tmux session that is not one of devloop's own workers and carries no
|
|
57
|
+
# configured allow prefix. The incident (issue #95): a develop worker spawned
|
|
58
|
+
# its own reviewer session, which no code path read, no sweep considered, and
|
|
59
|
+
# which idled at ~600 MB until the container restarted. A directive is not
|
|
60
|
+
# enforcement; this is the enforcement.
|
|
61
|
+
FOREIGN_REAP = "reap" # kill it once idle past the reaper's grace
|
|
62
|
+
FOREIGN_WARN = "warn" # log the decision and escalate, never kill
|
|
63
|
+
FOREIGN_IGNORE = "ignore" # today's behavior: never even considered
|
|
64
|
+
|
|
65
|
+
_FOREIGN_POLICIES = {FOREIGN_REAP, FOREIGN_WARN, FOREIGN_IGNORE}
|
|
66
|
+
|
|
55
67
|
CONFIG_FILENAME = "devloop.config.json"
|
|
56
68
|
|
|
57
69
|
# Comma-separated reviewer logins; authoritative when set, even if empty.
|
|
@@ -102,6 +114,8 @@ CONFIG_KEYS = (
|
|
|
102
114
|
"stale_minutes",
|
|
103
115
|
"max_sessions",
|
|
104
116
|
"reap_grace_minutes",
|
|
117
|
+
"foreign_session_policy",
|
|
118
|
+
"foreign_session_allow_prefixes",
|
|
105
119
|
"on_missing_origin_task",
|
|
106
120
|
"on_missing_hub",
|
|
107
121
|
"fix_rounds_enabled",
|
|
@@ -190,6 +204,33 @@ def normalize_bow_owners(values: "Any") -> "tuple[str, ...]":
|
|
|
190
204
|
return tuple(out)
|
|
191
205
|
|
|
192
206
|
|
|
207
|
+
def normalize_session_prefixes(values: "Any") -> "tuple[str, ...]":
|
|
208
|
+
"""Validate the `foreign_session_allow_prefixes` layer into name prefixes.
|
|
209
|
+
|
|
210
|
+
Parsed exactly like `bow_owners` -- a list of strings, each of which may
|
|
211
|
+
itself carry a `|`- and/or `,`-separated list, which is what one flag or a
|
|
212
|
+
single platform variable looks like -- minus the actor-id validation, which
|
|
213
|
+
has no analogue here: a session-name prefix is free text.
|
|
214
|
+
|
|
215
|
+
Whitespace is stripped and empties dropped, so a trailing separator is
|
|
216
|
+
harmless. NOT casefolded: tmux session names are case-sensitive, and
|
|
217
|
+
folding would widen the exemption to names the sweep would still kill.
|
|
218
|
+
|
|
219
|
+
An empty result means "no exemptions", which is the default and the one
|
|
220
|
+
value with teeth: every foreign session is then a candidate. It is not a
|
|
221
|
+
fail-open state -- the policy, not this list, decides what happens next.
|
|
222
|
+
"""
|
|
223
|
+
out: "list[str]" = []
|
|
224
|
+
seen: "set[str]" = set()
|
|
225
|
+
for value in _string_list(values, "foreign_session_allow_prefixes"):
|
|
226
|
+
for part in _OWNER_SEPARATORS.split(value):
|
|
227
|
+
entry = part.strip()
|
|
228
|
+
if entry and entry not in seen:
|
|
229
|
+
seen.add(entry)
|
|
230
|
+
out.append(entry)
|
|
231
|
+
return tuple(out)
|
|
232
|
+
|
|
233
|
+
|
|
193
234
|
def _validate_actor_id(entry: str) -> None:
|
|
194
235
|
"""Reject anything that is not an Alissa actor id, naming the entry.
|
|
195
236
|
|
|
@@ -323,6 +364,31 @@ class Config:
|
|
|
323
364
|
# back to stale_minutes, the sentinel every other grace knob here uses.
|
|
324
365
|
reap_grace_minutes: int = 30
|
|
325
366
|
|
|
367
|
+
# The foreign-session sweep (issue #95) -- depth-1 made MECHANICAL. A
|
|
368
|
+
# foreign session is a live entry in the reaper's own `alissa tmux ls`
|
|
369
|
+
# roster whose name is not devloop's grammar (`parse_worker_session` ->
|
|
370
|
+
# None) and starts with none of `foreign_session_allow_prefixes`. Default
|
|
371
|
+
# `reap` because the failure it answers is unbounded: a session nothing
|
|
372
|
+
# reads holds its memory until the container restarts, and every code path
|
|
373
|
+
# that could have noticed it was keyed on a name grammar it does not match.
|
|
374
|
+
#
|
|
375
|
+
# It runs under the SAME three guardrails as the worker sweep -- idle only
|
|
376
|
+
# (a busy foreign session is a holdout, never killed), quiet past
|
|
377
|
+
# `reap_grace_minutes`, one `alissa tmux kill` at a time, best-effort, and
|
|
378
|
+
# dry-run only logs.
|
|
379
|
+
foreign_session_policy: str = FOREIGN_REAP
|
|
380
|
+
|
|
381
|
+
# Name prefixes the sweep never treats as foreign: an operator's own shell,
|
|
382
|
+
# a co-located daemon's lane. EMPTY BY DEFAULT, which is the sharp edge
|
|
383
|
+
# worth stating plainly -- with `reap` and no prefixes, a container shared
|
|
384
|
+
# with another daemon will reap that daemon's idle sessions too, because
|
|
385
|
+
# nothing in a session NAME distinguishes a reviewer daemon's own seat from
|
|
386
|
+
# the one a worker spawned for itself (they are the same shape; that is the
|
|
387
|
+
# incident). Set this before deploying `reap` into a shared container.
|
|
388
|
+
# Parsed like `repos` with `bow_owners`' separators: a list of strings, each
|
|
389
|
+
# of which may itself carry a `|`- and/or `,`-separated list.
|
|
390
|
+
foreign_session_allow_prefixes: tuple[str, ...] = ()
|
|
391
|
+
|
|
326
392
|
on_missing_origin_task: str = ON_MISSING_WARN
|
|
327
393
|
on_missing_hub: str = HUB_SKIP
|
|
328
394
|
|
|
@@ -727,6 +793,19 @@ class Config:
|
|
|
727
793
|
f"got {reap_grace}"
|
|
728
794
|
)
|
|
729
795
|
|
|
796
|
+
# The foreign-session policy. An enum, validated by NAME like
|
|
797
|
+
# on_missing_hub and repos_source: a typo here would otherwise fall
|
|
798
|
+
# through to "not reap, not warn" and silently disable the sweep,
|
|
799
|
+
# which is the one outcome an operator setting this key cannot want.
|
|
800
|
+
foreign_policy = str(
|
|
801
|
+
raw.get("foreign_session_policy", cls.foreign_session_policy)
|
|
802
|
+
).strip()
|
|
803
|
+
if foreign_policy not in _FOREIGN_POLICIES:
|
|
804
|
+
raise ValueError(
|
|
805
|
+
f"foreign_session_policy must be one of "
|
|
806
|
+
f"{sorted(_FOREIGN_POLICIES)}, got {foreign_policy!r}"
|
|
807
|
+
)
|
|
808
|
+
|
|
730
809
|
# 0 is the sentinel for "fall back to stale_minutes" (the documented
|
|
731
810
|
# default), so the floor is 0 like max_sessions' -- a negative grace
|
|
732
811
|
# would make the self-heal fire on a head pushed in the future.
|
|
@@ -812,6 +891,13 @@ class Config:
|
|
|
812
891
|
stale_minutes=stale,
|
|
813
892
|
max_sessions=max_sessions,
|
|
814
893
|
reap_grace_minutes=reap_grace,
|
|
894
|
+
foreign_session_policy=foreign_policy,
|
|
895
|
+
foreign_session_allow_prefixes=normalize_session_prefixes(
|
|
896
|
+
raw.get(
|
|
897
|
+
"foreign_session_allow_prefixes",
|
|
898
|
+
cls.foreign_session_allow_prefixes,
|
|
899
|
+
)
|
|
900
|
+
),
|
|
815
901
|
on_missing_origin_task=mode,
|
|
816
902
|
on_missing_hub=hub_mode,
|
|
817
903
|
fix_rounds_enabled=bool(
|