alissa-tools-github-devloop 0.8.43__py3-none-any.whl → 0.8.45__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/version +1 -1
- alissa/tools/github/devloop/webui/sources.py +66 -29
- {alissa_tools_github_devloop-0.8.43.dist-info → alissa_tools_github_devloop-0.8.45.dist-info}/METADATA +27 -1
- {alissa_tools_github_devloop-0.8.43.dist-info → alissa_tools_github_devloop-0.8.45.dist-info}/RECORD +11 -11
- {alissa_tools_github_devloop-0.8.43.dist-info → alissa_tools_github_devloop-0.8.45.dist-info}/WHEEL +0 -0
- {alissa_tools_github_devloop-0.8.43.dist-info → alissa_tools_github_devloop-0.8.45.dist-info}/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.8.43.dist-info → alissa_tools_github_devloop-0.8.45.dist-info}/licenses/LICENSE +0 -0
- {alissa_tools_github_devloop-0.8.43.dist-info → alissa_tools_github_devloop-0.8.45.dist-info}/licenses/NOTICE +0 -0
- {alissa_tools_github_devloop-0.8.43.dist-info → alissa_tools_github_devloop-0.8.45.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
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.8.
|
|
1
|
+
0.8.45
|
|
@@ -31,6 +31,7 @@ logic lives here -- the console only moves a clock the daemon already reads.
|
|
|
31
31
|
from __future__ import annotations
|
|
32
32
|
|
|
33
33
|
import json
|
|
34
|
+
import re
|
|
34
35
|
import sqlite3
|
|
35
36
|
import time
|
|
36
37
|
import urllib.error
|
|
@@ -111,8 +112,20 @@ VERSION_CACHE_TTL = 600.0
|
|
|
111
112
|
# non-terminal task corpus 1,440x/day.
|
|
112
113
|
TASKS_CACHE_TTL = 300.0
|
|
113
114
|
# The flag that narrows `alissa task list` to the calling actor's own rows
|
|
114
|
-
# instead of the sponsor's corpus. Newer CLIs only -- see `
|
|
115
|
+
# instead of the sponsor's corpus. Newer CLIs only -- see `_task_list_flags`.
|
|
115
116
|
TASKS_SELF_FLAG = "--self"
|
|
117
|
+
# The pane shows live work only, so the read asks for exactly those statuses
|
|
118
|
+
# rather than every non-terminal row: the pending_validation backlog alone was
|
|
119
|
+
# ~2,600 rows (~3.5 MB) per refresh on prod (issue #168).
|
|
120
|
+
TASKS_STATUS_FLAG = "--status"
|
|
121
|
+
TASKS_LIVE_STATUSES = ("draft", "committed", "in_progress", "blocked")
|
|
122
|
+
# A row cap, a small multiple of what the pane displays, for a CLI that offers
|
|
123
|
+
# one. Kept within the 1..100 the CLI's paged listings accept.
|
|
124
|
+
TASKS_LIMIT_FLAG = "--limit"
|
|
125
|
+
TASKS_LIMIT_FACTOR = 4
|
|
126
|
+
TASKS_LIMIT_MAX = 100
|
|
127
|
+
# The pane's displayed row count (`worker_tasks`' default limit).
|
|
128
|
+
TASKS_DISPLAYED = 25
|
|
116
129
|
# PyPI JSON endpoint for the running-vs-latest drift chip.
|
|
117
130
|
PYPI_URL = "https://pypi.org/pypi/alissa-tools-github-devloop/json"
|
|
118
131
|
# How many minutes past the stale window retry-now ages a row: the daemon
|
|
@@ -194,6 +207,12 @@ def edge_of(name: "str | None") -> "str | None":
|
|
|
194
207
|
return None
|
|
195
208
|
|
|
196
209
|
|
|
210
|
+
def _offers_flag(helptext: str, flag: str) -> bool:
|
|
211
|
+
"""Whether a CLI `--help` text lists `flag` as an option of its own --
|
|
212
|
+
`--self` must not be read off a longer `--self-x` it happens to prefix."""
|
|
213
|
+
return re.search(re.escape(flag) + r"(?![\w-])", helptext) is not None
|
|
214
|
+
|
|
215
|
+
|
|
197
216
|
def _default_http_get(url: str, timeout: float) -> "bytes | None":
|
|
198
217
|
try:
|
|
199
218
|
with urllib.request.urlopen(url, timeout=timeout) as resp: # noqa: S310
|
|
@@ -250,8 +269,8 @@ class Sources:
|
|
|
250
269
|
self._rate_cache = _Cache(RATE_CACHE_TTL, clock)
|
|
251
270
|
self._version_cache = _Cache(VERSION_CACHE_TTL, clock)
|
|
252
271
|
self._tasks_cache = _Cache(TASKS_CACHE_TTL, clock)
|
|
253
|
-
#
|
|
254
|
-
self.
|
|
272
|
+
# None = not probed yet (see `_task_list_flags`).
|
|
273
|
+
self._list_flags: "frozenset[str] | None" = None
|
|
255
274
|
self.boot_wall = int(wall_clock())
|
|
256
275
|
|
|
257
276
|
# -- thin, failure-tolerant subprocess helpers -------------------------
|
|
@@ -460,40 +479,67 @@ class Sources:
|
|
|
460
479
|
drain=self.drain(), now=self._wall(),
|
|
461
480
|
)
|
|
462
481
|
|
|
463
|
-
def
|
|
464
|
-
"""
|
|
482
|
+
def _task_list_flags(self) -> "frozenset[str]":
|
|
483
|
+
"""Which of the narrowing flags (`--self`, `--status`, `--limit`) the
|
|
484
|
+
installed `alissa` CLI offers on `task list`.
|
|
465
485
|
|
|
466
486
|
Read off the CLI's own `--help`, which is local, tokenless and free.
|
|
467
|
-
The alternative -- send
|
|
487
|
+
The alternative -- send a flag and fall back when the call fails --
|
|
468
488
|
cannot tell an unknown flag from an auth hiccup, so it would spend a
|
|
469
|
-
second
|
|
470
|
-
|
|
489
|
+
second, wider call on every unlucky refresh, which is the cost this
|
|
490
|
+
whole path exists to avoid.
|
|
471
491
|
|
|
472
492
|
A probe that *answers* is memoized for the process (the CLI cannot
|
|
473
493
|
change under a running console). A probe that fails is deliberately
|
|
474
494
|
NOT memoized: a transient `alissa` failure then degrades this refresh
|
|
475
|
-
only, instead of pinning the console to the expensive
|
|
495
|
+
only, instead of pinning the console to the expensive unflagged call
|
|
476
496
|
until someone restarts it."""
|
|
477
|
-
if self.
|
|
497
|
+
if self._list_flags is None:
|
|
478
498
|
helptext = self._safe_run(
|
|
479
499
|
["alissa", "task", "list", "--help"], timeout=20
|
|
480
500
|
)
|
|
481
501
|
if helptext is None:
|
|
482
|
-
return
|
|
483
|
-
self.
|
|
484
|
-
|
|
502
|
+
return frozenset()
|
|
503
|
+
self._list_flags = frozenset(
|
|
504
|
+
flag for flag in
|
|
505
|
+
(TASKS_SELF_FLAG, TASKS_STATUS_FLAG, TASKS_LIMIT_FLAG)
|
|
506
|
+
if _offers_flag(helptext, flag)
|
|
507
|
+
)
|
|
508
|
+
return self._list_flags
|
|
485
509
|
|
|
486
|
-
def
|
|
510
|
+
def _supports_self_scope(self) -> bool:
|
|
511
|
+
"""Whether the installed `alissa` CLI understands `task list --self`."""
|
|
512
|
+
return TASKS_SELF_FLAG in self._task_list_flags()
|
|
513
|
+
|
|
514
|
+
def _task_list_argv(self) -> "list[str]":
|
|
515
|
+
"""The `alissa task list` read the pane refreshes from: narrowed by
|
|
516
|
+
every flag the CLI offers -- the actor's own rows (`--self`), the live
|
|
517
|
+
statuses the pane shows (`--status`) and a row cap a small multiple
|
|
518
|
+
of the displayed count (`--limit`). A flag the CLI lacks is simply left
|
|
519
|
+
off; that per-flag degrade is the ONLY way the read widens."""
|
|
520
|
+
flags = self._task_list_flags()
|
|
521
|
+
argv = ["alissa", "task", "list", "--json"]
|
|
522
|
+
if TASKS_SELF_FLAG in flags:
|
|
523
|
+
argv.append(TASKS_SELF_FLAG)
|
|
524
|
+
if TASKS_STATUS_FLAG in flags:
|
|
525
|
+
argv += [TASKS_STATUS_FLAG, ",".join(TASKS_LIVE_STATUSES)]
|
|
526
|
+
if TASKS_LIMIT_FLAG in flags:
|
|
527
|
+
cap = min(TASKS_DISPLAYED * TASKS_LIMIT_FACTOR, TASKS_LIMIT_MAX)
|
|
528
|
+
argv += [TASKS_LIMIT_FLAG, str(cap)]
|
|
529
|
+
return argv
|
|
530
|
+
|
|
531
|
+
def worker_tasks(self, limit: int = TASKS_DISPLAYED) -> "list[dict]":
|
|
487
532
|
"""The worker actor's own tasks via `alissa task list` -- the
|
|
488
533
|
actor-partition bridge (the console runs as the operator, whose actor
|
|
489
534
|
owns the implementation tasks the dev sessions created). Best-effort:
|
|
490
535
|
an old CLI or an auth hiccup just yields an empty list.
|
|
491
536
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
537
|
+
Narrowed (`_task_list_argv`) to what the pane displays wherever the
|
|
538
|
+
installed CLI offers the flags, so a refresh reads the console actor's
|
|
539
|
+
live rows rather than the sponsor's whole non-terminal corpus. Only a
|
|
540
|
+
CLI that lacks a flag reads wider. A narrowed call that fails is never
|
|
541
|
+
retried unflagged (issue #168: that fallback re-read the sponsor's
|
|
542
|
+
whole corpus), and a valid but empty `[]` is a real answer.
|
|
497
543
|
|
|
498
544
|
Cached (TASKS_CACHE_TTL) exactly like the two other remote checks: this
|
|
499
545
|
call rides the shared ALISSA_API_TOKEN the daemon uses, so on the ~10s
|
|
@@ -501,16 +547,7 @@ class Sources:
|
|
|
501
547
|
the API budget. A fetch that fails returns None and keeps the last good
|
|
502
548
|
list (the _Cache contract), never overwriting it with junk."""
|
|
503
549
|
def fetch() -> "list[dict] | None":
|
|
504
|
-
|
|
505
|
-
self_scoped = self._supports_self_scope()
|
|
506
|
-
argv = (unscoped + [TASKS_SELF_FLAG]) if self_scoped else unscoped
|
|
507
|
-
raw = self._safe_json(argv, timeout=45)
|
|
508
|
-
if raw is None and self_scoped:
|
|
509
|
-
# The CLI advertised the flag but the call brought nothing
|
|
510
|
-
# back -- an API that does not serve the scope yet, say. One
|
|
511
|
-
# unflagged retry keeps the pane populated. A valid but empty
|
|
512
|
-
# `[]` is a real answer, not a failure, and is never retried.
|
|
513
|
-
raw = self._safe_json(unscoped, timeout=45)
|
|
550
|
+
raw = self._safe_json(self._task_list_argv(), timeout=45)
|
|
514
551
|
if not isinstance(raw, list):
|
|
515
552
|
return None
|
|
516
553
|
rows = [t for t in raw if isinstance(t, dict)]
|
|
@@ -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.45
|
|
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.43.dist-info → alissa_tools_github_devloop-0.8.45.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=
|
|
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
13
|
alissa/tools/github/devloop/parking.py,sha256=vEf-qprN14-I4vu4M53tIFaA3827FiHLpKetBAgN4LQ,12409
|
|
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=SvuxweSl5ewBHlA8sLXW9Cnb1QXZFR16TSaeQVx76Nw,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
23
|
alissa/tools/github/devloop/webui/server.py,sha256=YuUmcng5qdWqSurcuyxH3FKKkBBfrpDtvtZLidJOGiU,26226
|
|
24
|
-
alissa/tools/github/devloop/webui/sources.py,sha256=
|
|
24
|
+
alissa/tools/github/devloop/webui/sources.py,sha256=JJXJEkLKh2qRbpTaIyQwCTu90DuKq_Z3nz6DwLhzlr4,38900
|
|
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.45.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
27
|
+
alissa_tools_github_devloop-0.8.45.dist-info/licenses/NOTICE,sha256=FKstNs3uw_tQod4_1XcFLD1FUTJQXf4FC0oXJAOA_HE,260
|
|
28
|
+
alissa_tools_github_devloop-0.8.45.dist-info/METADATA,sha256=UUG6d1BW6ztPi_whOMe8pL-eLbLK7LpT8BEFUECIp0s,13628
|
|
29
|
+
alissa_tools_github_devloop-0.8.45.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
30
|
+
alissa_tools_github_devloop-0.8.45.dist-info/entry_points.txt,sha256=8aC7Ajzx4bu9ZRR37a40IDfuStacvZP5KsAxY1dbFKU,145
|
|
31
|
+
alissa_tools_github_devloop-0.8.45.dist-info/top_level.txt,sha256=DodjDg-l-TWQnlxG-Vuc3G5sB4O7cWGQwe6XrVx3u-A,7
|
|
32
|
+
alissa_tools_github_devloop-0.8.45.dist-info/RECORD,,
|
{alissa_tools_github_devloop-0.8.43.dist-info → alissa_tools_github_devloop-0.8.45.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|