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.
@@ -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
- wedged_dialog = False
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 self.config.resume_enabled and liveness is Liveness.DEAD:
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 liveness is Liveness.ALIVE:
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
- wedged_dialog = True
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 wedged_dialog:
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.43
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 `_supports_self_scope`.
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
- # Tri-state: None = not probed yet (see `_supports_self_scope`).
254
- self._self_scope: "bool | None" = None
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 _supports_self_scope(self) -> bool:
464
- """Whether the installed `alissa` CLI understands `task list --self`.
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 the flag and fall back when the call fails --
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 sponsor-scoped call on every unlucky refresh, which is the cost
470
- this whole path exists to avoid.
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 unscoped call
495
+ only, instead of pinning the console to the expensive unflagged call
476
496
  until someone restarts it."""
477
- if self._self_scope is None:
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 False
483
- self._self_scope = TASKS_SELF_FLAG in helptext
484
- return self._self_scope
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 worker_tasks(self, limit: int = 25) -> "list[dict]":
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
- Self-scoped (`--self`) wherever the installed CLI offers it, so a poll
493
- reads the console actor's own rows rather than the sponsor's whole
494
- non-terminal corpus -- the pane only ever displays the former. An
495
- older CLI, or a `--self` call that comes back empty-handed, degrades to
496
- the unflagged call: a slower, larger read beats a blank pane.
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
- unscoped = ["alissa", "task", "list", "--json"]
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.43
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
@@ -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=uMfuomSdIPC8KkWUGF0OrW3RR9OSC1txKVnXGZ1SeIc,613973
12
- alissa/tools/github/devloop/loop_events.py,sha256=tmso0OALqY2yAPW2Fo6ae0sYetQIdIeIYooJ2Sc2cc4,37921
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=w6XCzs4ibf7WAa6RiS5RhCLRVAdNmHRvxJaC9NKDINI,7
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=8_f5kQv6DGD0A2Y4goKH-TSIQAab-O5uoO3f2nNRhI0,37209
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.43.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
27
- alissa_tools_github_devloop-0.8.43.dist-info/licenses/NOTICE,sha256=FKstNs3uw_tQod4_1XcFLD1FUTJQXf4FC0oXJAOA_HE,260
28
- alissa_tools_github_devloop-0.8.43.dist-info/METADATA,sha256=ostOBBFoEkmV42blENKqNX5CrVK2dCdPHLIxfYVrzuY,12222
29
- alissa_tools_github_devloop-0.8.43.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
30
- alissa_tools_github_devloop-0.8.43.dist-info/entry_points.txt,sha256=8aC7Ajzx4bu9ZRR37a40IDfuStacvZP5KsAxY1dbFKU,145
31
- alissa_tools_github_devloop-0.8.43.dist-info/top_level.txt,sha256=DodjDg-l-TWQnlxG-Vuc3G5sB4O7cWGQwe6XrVx3u-A,7
32
- alissa_tools_github_devloop-0.8.43.dist-info/RECORD,,
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,,