alissa-tools-github-devloop 0.8.9__tar.gz → 0.8.11__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.
Files changed (34) hide show
  1. {alissa_tools_github_devloop-0.8.9/src/main/alissa_tools_github_devloop.egg-info → alissa_tools_github_devloop-0.8.11}/PKG-INFO +1 -1
  2. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/alissa.py +32 -0
  3. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/config.py +86 -0
  4. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/loop.py +599 -36
  5. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/state.py +129 -11
  6. alissa_tools_github_devloop-0.8.11/src/main/alissa/tools/github/devloop/version +1 -0
  7. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/webui/page.py +10 -3
  8. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/webui/sources.py +23 -2
  9. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11/src/main/alissa_tools_github_devloop.egg-info}/PKG-INFO +1 -1
  10. alissa_tools_github_devloop-0.8.9/src/main/alissa/tools/github/devloop/version +0 -1
  11. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/LICENSE +0 -0
  12. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/MANIFEST.in +0 -0
  13. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/NOTICE +0 -0
  14. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/README.md +0 -0
  15. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/requirements.txt +0 -0
  16. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/setup.cfg +0 -0
  17. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/setup.py +0 -0
  18. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
  19. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/__main__.py +0 -0
  20. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/alissa_client.py +0 -0
  21. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/bows.py +0 -0
  22. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/ghclient.py +0 -0
  23. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/janitor.py +0 -0
  24. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/proc.py +0 -0
  25. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/version.py +0 -0
  26. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
  27. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
  28. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
  29. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
  30. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
  31. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa_tools_github_devloop.egg-info/SOURCES.txt +0 -0
  32. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa_tools_github_devloop.egg-info/dependency_links.txt +0 -0
  33. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa_tools_github_devloop.egg-info/entry_points.txt +0 -0
  34. {alissa_tools_github_devloop-0.8.9 → alissa_tools_github_devloop-0.8.11}/src/main/alissa_tools_github_devloop.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-devloop
3
- Version: 0.8.9
3
+ Version: 0.8.11
4
4
  Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -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(