alissa-tools-github-devloop 0.8.44__py3-none-any.whl → 0.8.46__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.
- alissa/tools/github/devloop/loop.py +209 -6
- alissa/tools/github/devloop/loop_events.py +1 -0
- alissa/tools/github/devloop/parking.py +1 -1
- alissa/tools/github/devloop/version +1 -1
- alissa/tools/github/devloop/webui/server.py +18 -4
- alissa/tools/github/devloop/webui/sources.py +12 -5
- {alissa_tools_github_devloop-0.8.44.dist-info → alissa_tools_github_devloop-0.8.46.dist-info}/METADATA +27 -1
- {alissa_tools_github_devloop-0.8.44.dist-info → alissa_tools_github_devloop-0.8.46.dist-info}/RECORD +13 -13
- {alissa_tools_github_devloop-0.8.44.dist-info → alissa_tools_github_devloop-0.8.46.dist-info}/WHEEL +0 -0
- {alissa_tools_github_devloop-0.8.44.dist-info → alissa_tools_github_devloop-0.8.46.dist-info}/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.8.44.dist-info → alissa_tools_github_devloop-0.8.46.dist-info}/licenses/LICENSE +0 -0
- {alissa_tools_github_devloop-0.8.44.dist-info → alissa_tools_github_devloop-0.8.46.dist-info}/licenses/NOTICE +0 -0
- {alissa_tools_github_devloop-0.8.44.dist-info → alissa_tools_github_devloop-0.8.46.dist-info}/top_level.txt +0 -0
|
@@ -1964,6 +1964,16 @@ def write_discovery_marker(session: str, plans_repo: str, number: int, discovery
|
|
|
1964
1964
|
# `escalation.first-run-dialog`, anchored on the issue.
|
|
1965
1965
|
ESCALATION_FIRST_RUN_DIALOG = "first-run-dialog"
|
|
1966
1966
|
|
|
1967
|
+
# The claude-exited wedge (issue #169): a session the roster lists ALIVE
|
|
1968
|
+
# whose pane is back at its login shell -- `claude` exited, the tmux session
|
|
1969
|
+
# stayed. WEDGE_CLAUDE_EXITED is the classification the loop logs and puts
|
|
1970
|
+
# on the activity line; ESCALATION_CLAUDE_EXITED is its once-per-episode
|
|
1971
|
+
# marker (see claude_exited_kind), an ACT like ESCALATION_FIRST_RUN_DIALOG:
|
|
1972
|
+
# it dedupes the WARNING and the activity line, and the loop-events emitter
|
|
1973
|
+
# turns it into `escalation.claude_exited`, anchored on the issue.
|
|
1974
|
+
WEDGE_CLAUDE_EXITED = "wedged:claude-exited"
|
|
1975
|
+
ESCALATION_CLAUDE_EXITED = "claude-exited"
|
|
1976
|
+
|
|
1967
1977
|
# The maintenance edge's cap-out kind prefix. The `maintain-` prefix keeps
|
|
1968
1978
|
# rows from ever colliding with the issue edge's `cap` when an issue and a
|
|
1969
1979
|
# PR share a number; the epoch suffix is maintain_cap_kind's job below.
|
|
@@ -2353,6 +2363,15 @@ def first_run_dialog_kind(attempt: int) -> str:
|
|
|
2353
2363
|
return f"{ESCALATION_FIRST_RUN_DIALOG}:a{attempt}"
|
|
2354
2364
|
|
|
2355
2365
|
|
|
2366
|
+
def claude_exited_kind(attempt: int) -> str:
|
|
2367
|
+
"""The ledger kind for ONE claude-exited wedge episode (issue #169).
|
|
2368
|
+
|
|
2369
|
+
first_run_dialog_kind's episode reasoning: every attempt's claude can
|
|
2370
|
+
exit, and attempt k's row must not silence attempt k+1's WARNING.
|
|
2371
|
+
"""
|
|
2372
|
+
return f"{ESCALATION_CLAUDE_EXITED}:a{attempt}"
|
|
2373
|
+
|
|
2374
|
+
|
|
2356
2375
|
def deferral_activity_kind(session: str) -> str:
|
|
2357
2376
|
"""The activity-log kind that dedupes ONE capacity-deferral episode's line.
|
|
2358
2377
|
|
|
@@ -4016,7 +4035,10 @@ class DevWatcher:
|
|
|
4016
4035
|
# terminal one.
|
|
4017
4036
|
resume_pr: "PullRequest | None" = None
|
|
4018
4037
|
dead_session: "str | None" = None
|
|
4019
|
-
|
|
4038
|
+
# A wedge kill that clears the lane THIS pass (the first-run dialog,
|
|
4039
|
+
# a claude that exited) logged its own WARNING: the re-enqueue's
|
|
4040
|
+
# "absent from the tmux listing" line below would misstate it.
|
|
4041
|
+
wedge_killed = False
|
|
4020
4042
|
if age is not None:
|
|
4021
4043
|
# `exhaustive` only when a resume could actually fire: the probe's
|
|
4022
4044
|
# default early break answers the boolean the deferral needs, but
|
|
@@ -4043,7 +4065,25 @@ class DevWatcher:
|
|
|
4043
4065
|
issue.full_name, number
|
|
4044
4066
|
)
|
|
4045
4067
|
liveness = self.session_liveness(row_session, roster)
|
|
4046
|
-
if
|
|
4068
|
+
if liveness is Liveness.ALIVE and row_session:
|
|
4069
|
+
killed, exited_resume = self._claude_exited(
|
|
4070
|
+
issue, attempts, row_session, linked
|
|
4071
|
+
)
|
|
4072
|
+
if killed:
|
|
4073
|
+
# Issue #169: listed, but its claude is gone -- the
|
|
4074
|
+
# kill landed, so the lane is DEAD from here on, and
|
|
4075
|
+
# the `_resumable_pr` answer the kill's outcome line
|
|
4076
|
+
# was written from is the one this pass acts on: a
|
|
4077
|
+
# still-draft PR is resumed below, anything else
|
|
4078
|
+
# keeps the (now dead) defer.
|
|
4079
|
+
wedge_killed = True
|
|
4080
|
+
liveness = Liveness.DEAD
|
|
4081
|
+
resume_pr = exited_resume
|
|
4082
|
+
if (
|
|
4083
|
+
self.config.resume_enabled
|
|
4084
|
+
and liveness is Liveness.DEAD
|
|
4085
|
+
and not wedge_killed
|
|
4086
|
+
):
|
|
4047
4087
|
resume_pr = self._resumable_pr(issue, linked)
|
|
4048
4088
|
elif (
|
|
4049
4089
|
self.config.resume_enabled
|
|
@@ -4165,7 +4205,19 @@ class DevWatcher:
|
|
|
4165
4205
|
# successful listing without this name) -- so the retry below
|
|
4166
4206
|
# can name the session it proved gone instead of presuming.
|
|
4167
4207
|
dead_session = live_session
|
|
4168
|
-
if
|
|
4208
|
+
if (
|
|
4209
|
+
liveness is Liveness.ALIVE
|
|
4210
|
+
and self._claude_exited(
|
|
4211
|
+
issue, attempts, live_session, None
|
|
4212
|
+
)[0]
|
|
4213
|
+
):
|
|
4214
|
+
# Issue #169: ALIVE is a tmux session, not a claude. The
|
|
4215
|
+
# pane is back at its login shell, the kill landed: fall
|
|
4216
|
+
# through to the cap check and the re-enqueue below as
|
|
4217
|
+
# the CONFIRMED-dead path would.
|
|
4218
|
+
wedge_killed = True
|
|
4219
|
+
liveness = Liveness.DEAD
|
|
4220
|
+
elif liveness is Liveness.ALIVE:
|
|
4169
4221
|
# Issue #123: ALIVE is a process, not a worker. A session
|
|
4170
4222
|
# sitting on Claude Code's first-run "trust this folder?"
|
|
4171
4223
|
# (or bypass-permissions) gate is listed, idle, and will
|
|
@@ -4177,7 +4229,7 @@ class DevWatcher:
|
|
|
4177
4229
|
if self._wedged_on_first_run_dialog(
|
|
4178
4230
|
issue, attempts, live_session
|
|
4179
4231
|
):
|
|
4180
|
-
|
|
4232
|
+
wedge_killed = True
|
|
4181
4233
|
# The kill landed and the hub is seeded: fall through
|
|
4182
4234
|
# to the cap check and the re-enqueue below as the
|
|
4183
4235
|
# CONFIRMED-dead path would -- one WARNING for the
|
|
@@ -4270,7 +4322,7 @@ class DevWatcher:
|
|
|
4270
4322
|
attempts,
|
|
4271
4323
|
)
|
|
4272
4324
|
|
|
4273
|
-
if age is not None and resume_pr is not None:
|
|
4325
|
+
if age is not None and resume_pr is not None and not wedge_killed:
|
|
4274
4326
|
log.warning(
|
|
4275
4327
|
"%s attempt %d has been in flight %.0f min behind its still-"
|
|
4276
4328
|
"draft open PR %s and its session %s is absent from the tmux "
|
|
@@ -4283,7 +4335,7 @@ class DevWatcher:
|
|
|
4283
4335
|
dead_session,
|
|
4284
4336
|
attempts + 1,
|
|
4285
4337
|
)
|
|
4286
|
-
elif age is not None and not
|
|
4338
|
+
elif age is not None and not wedge_killed:
|
|
4287
4339
|
log.warning(
|
|
4288
4340
|
"%s attempt %d has been in flight %.0f min with no completion "
|
|
4289
4341
|
"and no linked open PR, and its session %s is absent from the "
|
|
@@ -5083,6 +5135,157 @@ class DevWatcher:
|
|
|
5083
5135
|
return None
|
|
5084
5136
|
return min(youngest, max(0.0, now - committed))
|
|
5085
5137
|
|
|
5138
|
+
def _claude_exited(
|
|
5139
|
+
self,
|
|
5140
|
+
issue: Issue,
|
|
5141
|
+
attempts: int,
|
|
5142
|
+
session: str,
|
|
5143
|
+
linked: "list[str] | None",
|
|
5144
|
+
) -> "tuple[bool, PullRequest | None]":
|
|
5145
|
+
"""Issue #169: is this stale, ALIVE session's `claude` gone -- and
|
|
5146
|
+
if so, clear the lane.
|
|
5147
|
+
|
|
5148
|
+
ALIVE means the roster LISTS the session; it says nothing about the
|
|
5149
|
+
process the session was spawned for. A claude that exits (a crash,
|
|
5150
|
+
a `/exit`, a kill from inside) leaves the tmux session open with its
|
|
5151
|
+
pane back at the login shell, and the session read alive for as long
|
|
5152
|
+
as anyone cared to look -- 2026-10-13, four worker sessions for
|
|
5153
|
+
1.5-2 h, behind the "work may be alive" and "may still be running"
|
|
5154
|
+
defers, until an operator killed them by hand. The probe is the one
|
|
5155
|
+
`display-message` `_first_turn_death` already uses: a pane whose
|
|
5156
|
+
`#{pane_current_command}` is one of `SHELL_COMMANDS` has no claude
|
|
5157
|
+
(nor the `alissa code` wrapper) in the foreground. A pane that
|
|
5158
|
+
reads anything else -- `node`, the wrapper -- is a live worker and
|
|
5159
|
+
keeps every defer exactly as before; "" (tmux could not answer) is
|
|
5160
|
+
absence of evidence, never a dead claude, and keeps them too. A
|
|
5161
|
+
usage-limited session is never killed (issue #144).
|
|
5162
|
+
|
|
5163
|
+
A match is `wedged:claude-exited`: ONE WARNING per episode (keyed
|
|
5164
|
+
claude_exited_kind(attempts); a failed kill retried next poll logs
|
|
5165
|
+
at INFO), then the row's own session is killed (`alissa tmux kill
|
|
5166
|
+
<name>`, the path every wedge kill takes, never a sweep), one
|
|
5167
|
+
activity-comment line records the act, and a True first element
|
|
5168
|
+
tells the caller to treat the session as CONFIRMED dead NOW. The
|
|
5169
|
+
pre-PR branch (`linked` None) re-enqueues attempt k+1. The linked-PR
|
|
5170
|
+
branch passes its open PRs, and `_resumable_pr` is asked HERE,
|
|
5171
|
+
before a word is logged, because only its answer says what the lane
|
|
5172
|
+
does next: a still-draft PR is resumed as attempt k+1 (the PR is the
|
|
5173
|
+
second element, which the caller acts on rather than asking again),
|
|
5174
|
+
while a handed-off PR, several candidates or `resume_enabled: false`
|
|
5175
|
+
leave the lane deferring, now read dead -- and the WARNING and the
|
|
5176
|
+
activity line say THAT, never a resume the lane will not get. A
|
|
5177
|
+
re-enqueue or a resume counts toward attempt_cap, and on the
|
|
5178
|
+
terminal attempt the caller's cap check pages the operator instead,
|
|
5179
|
+
which the lines say rather than promising a retry (a deferring lane
|
|
5180
|
+
never reaches that check, so it is never told it caps out).
|
|
5181
|
+
Dry-run classifies and logs but kills nothing and answers False
|
|
5182
|
+
(the defer holds); so does a kill that fails, retried next poll.
|
|
5183
|
+
|
|
5184
|
+
Not done here: the process groups the session's claude started
|
|
5185
|
+
that outlived it (a test run re-parented to init). Nothing on the
|
|
5186
|
+
daemon's side ties such a group to a session once its parent is
|
|
5187
|
+
gone, so they are left to the operator (see the README)."""
|
|
5188
|
+
command = self.alissa.pane_command(session)
|
|
5189
|
+
if command not in SHELL_COMMANDS:
|
|
5190
|
+
return False, None
|
|
5191
|
+
if self.state.session_limited(session):
|
|
5192
|
+
return False, None
|
|
5193
|
+
kind = claude_exited_kind(attempts)
|
|
5194
|
+
resume_pr = (
|
|
5195
|
+
self._resumable_pr(issue, linked)
|
|
5196
|
+
if linked is not None and self.config.resume_enabled
|
|
5197
|
+
else None
|
|
5198
|
+
)
|
|
5199
|
+
deferring = linked is not None and resume_pr is None
|
|
5200
|
+
capping = not deferring and attempts >= self.config.attempt_cap
|
|
5201
|
+
if deferring:
|
|
5202
|
+
outcome = (
|
|
5203
|
+
"leaving the lane to the review lifecycle: no linked PR is "
|
|
5204
|
+
"resumable (a handed-off PR is not resumed), so it keeps its "
|
|
5205
|
+
"defer, now reading dead"
|
|
5206
|
+
)
|
|
5207
|
+
activity_outcome = (
|
|
5208
|
+
"no linked PR is resumable, so the lane waits on the review "
|
|
5209
|
+
"lifecycle"
|
|
5210
|
+
)
|
|
5211
|
+
elif capping:
|
|
5212
|
+
outcome = (
|
|
5213
|
+
f"NOT re-enqueuing: attempt {attempts} was the last "
|
|
5214
|
+
f"(attempt_cap {self.config.attempt_cap}), so the lane caps "
|
|
5215
|
+
f"out and pages the operator"
|
|
5216
|
+
)
|
|
5217
|
+
activity_outcome = (
|
|
5218
|
+
f"attempt_cap {self.config.attempt_cap} reached — the lane "
|
|
5219
|
+
f"caps out and pages the operator instead of re-enqueuing"
|
|
5220
|
+
)
|
|
5221
|
+
elif resume_pr is not None:
|
|
5222
|
+
outcome = (
|
|
5223
|
+
f"resuming its still-draft PR {resume_pr.url} as attempt "
|
|
5224
|
+
f"{attempts + 1} (counting toward attempt_cap "
|
|
5225
|
+
f"{self.config.attempt_cap})"
|
|
5226
|
+
)
|
|
5227
|
+
activity_outcome = (
|
|
5228
|
+
f"its still-draft PR is resumed as attempt {attempts + 1}"
|
|
5229
|
+
)
|
|
5230
|
+
else:
|
|
5231
|
+
outcome = (
|
|
5232
|
+
f"re-enqueuing as attempt {attempts + 1} (counting toward "
|
|
5233
|
+
f"attempt_cap {self.config.attempt_cap})"
|
|
5234
|
+
)
|
|
5235
|
+
activity_outcome = f"re-enqueued as attempt {attempts + 1}"
|
|
5236
|
+
if self.state.escalated(issue.full_name, issue.number, kind):
|
|
5237
|
+
log.info(
|
|
5238
|
+
"%s: session %s (%s attempt %d) still has no claude in its "
|
|
5239
|
+
"pane — retrying the kill",
|
|
5240
|
+
WEDGE_CLAUDE_EXITED, session, issue.issue_slug, attempts,
|
|
5241
|
+
)
|
|
5242
|
+
else:
|
|
5243
|
+
log.warning(
|
|
5244
|
+
"%s: session %s (%s attempt %d) reads alive but its pane is "
|
|
5245
|
+
"back at the shell (%s) — its claude exited and nothing will "
|
|
5246
|
+
"ever be published. Killing it and %s",
|
|
5247
|
+
WEDGE_CLAUDE_EXITED, session, issue.issue_slug, attempts,
|
|
5248
|
+
command, outcome,
|
|
5249
|
+
)
|
|
5250
|
+
self.state.record_escalation(issue.full_name, issue.number, kind)
|
|
5251
|
+
if self.config.dry_run:
|
|
5252
|
+
log.info(
|
|
5253
|
+
"[dry-run] would kill %s (claude exited) and %s",
|
|
5254
|
+
session,
|
|
5255
|
+
"keep the defer" if deferring
|
|
5256
|
+
else f"cap out (attempt_cap {self.config.attempt_cap})"
|
|
5257
|
+
if capping
|
|
5258
|
+
else f"resume attempt {attempts + 1}" if resume_pr is not None
|
|
5259
|
+
else f"re-enqueue attempt {attempts + 1}",
|
|
5260
|
+
)
|
|
5261
|
+
return False, None
|
|
5262
|
+
try:
|
|
5263
|
+
self.alissa.kill_session(session)
|
|
5264
|
+
except CommandError as exc:
|
|
5265
|
+
log.warning(
|
|
5266
|
+
"%s: could not kill %s (%s) — kills are best-effort; the lane "
|
|
5267
|
+
"keeps its defer and the kill is retried next poll",
|
|
5268
|
+
WEDGE_CLAUDE_EXITED, session, exc,
|
|
5269
|
+
)
|
|
5270
|
+
return False, None
|
|
5271
|
+
if not self.state.activity_logged(issue.full_name, issue.number, kind):
|
|
5272
|
+
landed = self._append_activity(
|
|
5273
|
+
issue.owner,
|
|
5274
|
+
issue.repo,
|
|
5275
|
+
issue.number,
|
|
5276
|
+
self._activity_line(
|
|
5277
|
+
session,
|
|
5278
|
+
"issue edge",
|
|
5279
|
+
f"{ACTIVITY_WEDGE_KILLED} — {WEDGE_CLAUDE_EXITED}: the "
|
|
5280
|
+
f"session read alive but its pane was back at the shell "
|
|
5281
|
+
f"(claude exited); killed, {activity_outcome}",
|
|
5282
|
+
),
|
|
5283
|
+
issue.issue_slug,
|
|
5284
|
+
)
|
|
5285
|
+
if landed:
|
|
5286
|
+
self.state.record_activity(issue.full_name, issue.number, kind)
|
|
5287
|
+
return True, resume_pr
|
|
5288
|
+
|
|
5086
5289
|
def _wedged_on_first_run_dialog(
|
|
5087
5290
|
self, issue: Issue, attempts: int, session: str
|
|
5088
5291
|
) -> bool:
|
|
@@ -187,6 +187,7 @@ _LIMIT_LIFTED_RE = re.compile(r"^usage-limit-lifted:(?P<session>[^@]+)@(?P<since
|
|
|
187
187
|
# points the operator at an unrelated PR or at nothing.
|
|
188
188
|
_ISSUE_ANCHORED = (
|
|
189
189
|
"cap", "assignment", "stalled", "live-stalled", "wedged", "first-run-dialog",
|
|
190
|
+
"claude-exited",
|
|
190
191
|
)
|
|
191
192
|
|
|
192
193
|
|
|
@@ -25,7 +25,7 @@ serves it on ``GET /api/state`` and runs the drain handshake):
|
|
|
25
25
|
null as a refusal.
|
|
26
26
|
* **The drain.** A flag with a TTL in `state.db` (`State.set_drain`), cleared
|
|
27
27
|
when the daemon boots. While it holds, every spawn path stands down before
|
|
28
|
-
its first side effect (`DevWatcher.
|
|
28
|
+
its first side effect (`DevWatcher._drain_gated`).
|
|
29
29
|
"""
|
|
30
30
|
|
|
31
31
|
from __future__ import annotations
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.8.
|
|
1
|
+
0.8.46
|
|
@@ -234,7 +234,16 @@ class App:
|
|
|
234
234
|
down and the answer is `drained: false` with the reasons. When the
|
|
235
235
|
drain holds, a vitals snapshot carrying `idle.drainedAt` is pushed
|
|
236
236
|
BEFORE the answer, because that snapshot is what Studio re-checks
|
|
237
|
-
before it removes anything; the answer says how the push went.
|
|
237
|
+
before it removes anything; the answer says how the push went.
|
|
238
|
+
|
|
239
|
+
A refused drain lowers the flag, not just its own raise: the clear is
|
|
240
|
+
unscoped (`State.clear_drain`). A refused RE-drain therefore also
|
|
241
|
+
lowers an earlier drain that held and had answered `drained: true`,
|
|
242
|
+
and so does a second, overlapping `/action/drain` that is refused.
|
|
243
|
+
That is intended (seat-parking §2.4: when the block breaks D2 "the
|
|
244
|
+
seat clears the flag itself and answers `drained: false`"): a seat
|
|
245
|
+
that is no longer idle must not stay drained, whoever raised the
|
|
246
|
+
flag."""
|
|
238
247
|
if ttl_s is None:
|
|
239
248
|
ttl = parking.DRAIN_TTL_DEFAULT
|
|
240
249
|
elif (
|
|
@@ -265,7 +274,7 @@ class App:
|
|
|
265
274
|
except sqlite3.Error as exc:
|
|
266
275
|
self._audit("drain", {"ok": False, "error": str(exc)})
|
|
267
276
|
return 503, {"drained": False, "error": "state_unavailable"}
|
|
268
|
-
listing = self.sources.
|
|
277
|
+
listing = self.sources.listing()
|
|
269
278
|
idle = self.sources.idle(listing=listing)
|
|
270
279
|
refusals = parking.idle_refusals(
|
|
271
280
|
idle, poll_interval=self.sources.config.poll_interval, now=now,
|
|
@@ -305,13 +314,18 @@ class App:
|
|
|
305
314
|
def undrain(self) -> "tuple[int, dict]":
|
|
306
315
|
"""`POST /action/undrain`: lower the drain flag -- orcloop's step 4
|
|
307
316
|
on any failure of a park. Idempotent: undraining a seat that is not
|
|
308
|
-
drained answers `cleared: false`. (status, payload), audited.
|
|
317
|
+
drained answers `cleared: false`. (status, payload), audited.
|
|
318
|
+
|
|
319
|
+
A ledger that cannot be written answers 503 with `drained: null`:
|
|
320
|
+
nobody knows then whether the flag is up (there may be no drain at
|
|
321
|
+
all), and unknown is never a value (design D2). The 503 already says
|
|
322
|
+
the undrain did not land; the TTL still bounds any flag left up."""
|
|
309
323
|
try:
|
|
310
324
|
with State(self.sources.config.state_db) as st:
|
|
311
325
|
cleared = st.clear_drain()
|
|
312
326
|
except sqlite3.Error as exc:
|
|
313
327
|
self._audit("undrain", {"ok": False, "error": str(exc)})
|
|
314
|
-
return 503, {"drained":
|
|
328
|
+
return 503, {"drained": None, "error": "state_unavailable"}
|
|
315
329
|
self._audit("undrain", {"ok": True, "cleared": cleared})
|
|
316
330
|
return 200, {"drained": False, "cleared": cleared}
|
|
317
331
|
|
|
@@ -384,6 +384,15 @@ class Sources:
|
|
|
384
384
|
except ValueError:
|
|
385
385
|
return None
|
|
386
386
|
|
|
387
|
+
def listing(self) -> "object":
|
|
388
|
+
"""The parsed ``alissa tmux ls --json`` listing, or None when it could
|
|
389
|
+
not be read (the command failed, printed nothing, or printed something
|
|
390
|
+
that is not JSON). The console's one reader of the tmux roster: the
|
|
391
|
+
sessions panel, the idle block, the dashboard and the drain handshake
|
|
392
|
+
all read it here. The value is whatever the CLI printed -- callers
|
|
393
|
+
check it is a list before trusting its shape."""
|
|
394
|
+
return self._safe_json(["alissa", "tmux", "ls", "--json"])
|
|
395
|
+
|
|
387
396
|
def sessions(self, listing: "list[dict] | None" = None) -> "list[dict]":
|
|
388
397
|
"""The managed sessions with their edge, age and `/proc` footprint.
|
|
389
398
|
|
|
@@ -393,9 +402,7 @@ class Sources:
|
|
|
393
402
|
console (no listing) lists for itself. Either way the rows go
|
|
394
403
|
through the same shaping, so the two never disagree about what a
|
|
395
404
|
session record looks like."""
|
|
396
|
-
raw = listing if listing is not None else (
|
|
397
|
-
self._safe_json(["alissa", "tmux", "ls", "--json"]) or []
|
|
398
|
-
)
|
|
405
|
+
raw = listing if listing is not None else (self.listing() or [])
|
|
399
406
|
if not isinstance(raw, list):
|
|
400
407
|
return []
|
|
401
408
|
now = int(self._wall())
|
|
@@ -463,7 +470,7 @@ class Sources:
|
|
|
463
470
|
snaps = self.snapshots(1)
|
|
464
471
|
latest = snaps[0] if snaps else None
|
|
465
472
|
if listing is _UNREAD:
|
|
466
|
-
listing = self.
|
|
473
|
+
listing = self.listing()
|
|
467
474
|
roster = listing if isinstance(listing, list) else None
|
|
468
475
|
snapshot = latest if isinstance(latest, dict) else None
|
|
469
476
|
queues = self._safe_json(["alissa", "tmux", "queue", "ls", "--json"])
|
|
@@ -678,7 +685,7 @@ class Sources:
|
|
|
678
685
|
latest = snaps[0] if snaps else None
|
|
679
686
|
ledgers = self.ledgers()
|
|
680
687
|
# One listing feeds both the sessions panel and the idle block.
|
|
681
|
-
listing = self.
|
|
688
|
+
listing = self.listing()
|
|
682
689
|
sessions = self.sessions(
|
|
683
690
|
listing=listing if isinstance(listing, list) else []
|
|
684
691
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alissa-tools-github-devloop
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.46
|
|
4
4
|
Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
|
|
5
5
|
Home-page: https://alissa.app
|
|
6
6
|
Author: Fahera
|
|
@@ -220,6 +220,32 @@ kills or reaps one. The fleet-vitals push applies the same rule to
|
|
|
220
220
|
`sessions.managed` and `idle.managed` (Studio's park guard reads both) and
|
|
221
221
|
leaves `gone` out, since Studio's idle schema is strict.
|
|
222
222
|
|
|
223
|
+
## Wedges: alive is not working
|
|
224
|
+
|
|
225
|
+
A stale issue-edge session the tmux roster lists ALIVE is not trusted on
|
|
226
|
+
that alone. The daemon kills the row's own session (`alissa tmux kill
|
|
227
|
+
<name>`, never a sweep) and moves the lane on when the session is wedged:
|
|
228
|
+
|
|
229
|
+
- **`wedged:claude-exited`** (issue #169) — the pane's
|
|
230
|
+
`#{pane_current_command}` is a login shell (`alissa.SHELL_COMMANDS`): the
|
|
231
|
+
`claude` the session was spawned for exited and the tmux session stayed.
|
|
232
|
+
Checked on both stale branches (with and without a linked open PR), no
|
|
233
|
+
`wedge_minutes` needed; the session reads DEAD in the same pass, so the
|
|
234
|
+
pre-PR lane re-enqueues attempt k+1 and a draft-PR lane resumes its PR as
|
|
235
|
+
attempt k+1, counting toward `attempt_cap`. Ledger kind
|
|
236
|
+
`claude-exited:a<k>`.
|
|
237
|
+
- **`wedged:first-run-dialog`** (issue #123) — the pane sits on Claude
|
|
238
|
+
Code's trust / bypass-permissions gate: killed, hub pre-trusted,
|
|
239
|
+
re-enqueued in the same pass.
|
|
240
|
+
- **footprint wedges** (issues #110, #127; opt-in `wedge_minutes`) — no
|
|
241
|
+
activity footprint inside the window: killed, and the next pass resumes
|
|
242
|
+
or retries.
|
|
243
|
+
|
|
244
|
+
A pane tmux cannot read keeps the defer, a usage-limited session is never
|
|
245
|
+
killed (issue #144), and dry-run only logs. The root README's *Claude
|
|
246
|
+
exited, the session stayed* section has the details. Processes a dead claude started that
|
|
247
|
+
outlived it (re-parented to init) are not reaped — a known follow-up.
|
|
248
|
+
|
|
223
249
|
## License
|
|
224
250
|
|
|
225
251
|
Licensed under the
|
{alissa_tools_github_devloop-0.8.44.dist-info → alissa_tools_github_devloop-0.8.46.dist-info}/RECORD
RENAMED
|
@@ -8,25 +8,25 @@ alissa/tools/github/devloop/fleet_vitals.py,sha256=5mWnRkJh8u9soSplud3UMxkelT6Eq
|
|
|
8
8
|
alissa/tools/github/devloop/ghclient.py,sha256=tYUHwmoy9CBBt-jSOz9nuAl8lpMvm1LH4A-wHx25bss,71842
|
|
9
9
|
alissa/tools/github/devloop/hubs.py,sha256=yjnsnSHLG7GNNP8X2hILeAMV6P5n4JGc_IaAxpSP_YQ,18974
|
|
10
10
|
alissa/tools/github/devloop/janitor.py,sha256=R8REoHBeY2cfW7fYJVhKr-F5z9Mjlc-vmv-z4KtvBZc,27360
|
|
11
|
-
alissa/tools/github/devloop/loop.py,sha256=
|
|
12
|
-
alissa/tools/github/devloop/loop_events.py,sha256=
|
|
13
|
-
alissa/tools/github/devloop/parking.py,sha256=
|
|
11
|
+
alissa/tools/github/devloop/loop.py,sha256=I5rZy082JqplmFm1t2XgYkstc7IdkJJdX-FXAfwqcKk,624114
|
|
12
|
+
alissa/tools/github/devloop/loop_events.py,sha256=pKtBl1HK8gYPB2gKiI29xUkx-JuW2A48KwRlbdcf_lo,37942
|
|
13
|
+
alissa/tools/github/devloop/parking.py,sha256=7h3Gg2XQ0eqlvn0M6K6NeEg9xbqnlUPrZNs9gRofzxc,12410
|
|
14
14
|
alissa/tools/github/devloop/proc.py,sha256=nzfsxJSHFWRQcrXmfv588KazFysv8dVQrhMPH1q5eRk,3134
|
|
15
15
|
alissa/tools/github/devloop/prompts.py,sha256=VzN6ec5eF2wwg7QSCRg52WLcxo-tGbGRgQbc4HSahUc,55169
|
|
16
16
|
alissa/tools/github/devloop/state.py,sha256=uVwhbkogUTjWXXFq-COKzUeYrpuGbHzi05CjfEzAtPA,88668
|
|
17
|
-
alissa/tools/github/devloop/version,sha256=
|
|
17
|
+
alissa/tools/github/devloop/version,sha256=YpzpT7756Lld10FKXeESQnDWJA9HvJScK9ngOSfSk8s,7
|
|
18
18
|
alissa/tools/github/devloop/version.py,sha256=znuuOaitXGrBpbskNy78_rlVpFdtmKmk_3k1ACxZrl0,1418
|
|
19
19
|
alissa/tools/github/devloop/webui/__init__.py,sha256=vdXYoyGYzD7BrJ5lV6XdH8Bpw_pej8-RK0hjfxop7-8,1513
|
|
20
20
|
alissa/tools/github/devloop/webui/__main__.py,sha256=wORyW5AkgsniK0TZcrOrvm_P7Q4Mr-fH-4S6qj9XcgA,6137
|
|
21
21
|
alissa/tools/github/devloop/webui/auth.py,sha256=g9YEDao9nKt-cBEENp7vFqwJ-T8olN6uLiCzVIN8yhA,7890
|
|
22
22
|
alissa/tools/github/devloop/webui/page.py,sha256=4OD3e01ByvVM4Wo04AZyFuyOYTFEMT2lagktZAvyCKk,30862
|
|
23
|
-
alissa/tools/github/devloop/webui/server.py,sha256=
|
|
24
|
-
alissa/tools/github/devloop/webui/sources.py,sha256=
|
|
23
|
+
alissa/tools/github/devloop/webui/server.py,sha256=3aGYGesjUSrPrgcpfy-oYDefBNKnAj3lGfRzwsRkv_I,27031
|
|
24
|
+
alissa/tools/github/devloop/webui/sources.py,sha256=Uw_2B5QGSmW6f66WfVbQb6yLLYmSv6uuF7HNJ5OSr_I,39314
|
|
25
25
|
alissa/tools/github/devloop/webui/sysinfo.py,sha256=ptlogH9alFv0vZ-pQfyUaA3w-K7LQS3tQHGv0d8Tdlk,9649
|
|
26
|
-
alissa_tools_github_devloop-0.8.
|
|
27
|
-
alissa_tools_github_devloop-0.8.
|
|
28
|
-
alissa_tools_github_devloop-0.8.
|
|
29
|
-
alissa_tools_github_devloop-0.8.
|
|
30
|
-
alissa_tools_github_devloop-0.8.
|
|
31
|
-
alissa_tools_github_devloop-0.8.
|
|
32
|
-
alissa_tools_github_devloop-0.8.
|
|
26
|
+
alissa_tools_github_devloop-0.8.46.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
27
|
+
alissa_tools_github_devloop-0.8.46.dist-info/licenses/NOTICE,sha256=FKstNs3uw_tQod4_1XcFLD1FUTJQXf4FC0oXJAOA_HE,260
|
|
28
|
+
alissa_tools_github_devloop-0.8.46.dist-info/METADATA,sha256=0V0tseoJcEXAfIvuBYJmDowXYvWG0FaMgnUNbASbxVE,13628
|
|
29
|
+
alissa_tools_github_devloop-0.8.46.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
30
|
+
alissa_tools_github_devloop-0.8.46.dist-info/entry_points.txt,sha256=8aC7Ajzx4bu9ZRR37a40IDfuStacvZP5KsAxY1dbFKU,145
|
|
31
|
+
alissa_tools_github_devloop-0.8.46.dist-info/top_level.txt,sha256=DodjDg-l-TWQnlxG-Vuc3G5sB4O7cWGQwe6XrVx3u-A,7
|
|
32
|
+
alissa_tools_github_devloop-0.8.46.dist-info/RECORD,,
|
{alissa_tools_github_devloop-0.8.44.dist-info → alissa_tools_github_devloop-0.8.46.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|