alissa-tools-github-devloop 0.8.42__py3-none-any.whl → 0.8.44__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/fleet_vitals.py +13 -3
- alissa/tools/github/devloop/parking.py +112 -15
- alissa/tools/github/devloop/version +1 -1
- alissa/tools/github/devloop/webui/sources.py +78 -33
- {alissa_tools_github_devloop-0.8.42.dist-info → alissa_tools_github_devloop-0.8.44.dist-info}/METADATA +27 -1
- {alissa_tools_github_devloop-0.8.42.dist-info → alissa_tools_github_devloop-0.8.44.dist-info}/RECORD +11 -11
- {alissa_tools_github_devloop-0.8.42.dist-info → alissa_tools_github_devloop-0.8.44.dist-info}/WHEEL +0 -0
- {alissa_tools_github_devloop-0.8.42.dist-info → alissa_tools_github_devloop-0.8.44.dist-info}/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.8.42.dist-info → alissa_tools_github_devloop-0.8.44.dist-info}/licenses/LICENSE +0 -0
- {alissa_tools_github_devloop-0.8.42.dist-info → alissa_tools_github_devloop-0.8.44.dist-info}/licenses/NOTICE +0 -0
- {alissa_tools_github_devloop-0.8.42.dist-info → alissa_tools_github_devloop-0.8.44.dist-info}/top_level.txt +0 -0
|
@@ -257,7 +257,8 @@ def build_snapshot(
|
|
|
257
257
|
`queued` (the undispatched alissa queue items, None when unread) and
|
|
258
258
|
`drain` (the holding drain row, or None) feed the `idle` block, whose
|
|
259
259
|
owed / timers come from the latest poll snapshot and whose live /
|
|
260
|
-
managed come from `roster` -- the same reading as `sessions
|
|
260
|
+
managed come from `roster` -- the same reading as `sessions`, with a dead
|
|
261
|
+
row backing no held lane left out of `managed` (issue #166).
|
|
261
262
|
|
|
262
263
|
`config` is the seat's `config_block`, or None to leave the key out
|
|
263
264
|
(the console's push, which has no derived answer to give)."""
|
|
@@ -275,13 +276,20 @@ def build_snapshot(
|
|
|
275
276
|
if session and session not in repo_by_session:
|
|
276
277
|
repo_by_session[str(session)] = str(row["repo_slug"])
|
|
277
278
|
|
|
279
|
+
# A dead row counts as managed only while it backs a lane the seat still
|
|
280
|
+
# holds (issue #166): Studio's park guard reads `sessions.managed` as well
|
|
281
|
+
# as the idle block's, so both follow the console's rule.
|
|
282
|
+
roster_live, roster_managed, _gone = parking.roster_counts(
|
|
283
|
+
roster, parking.lane_sessions(latest, ledgers=ledgers),
|
|
284
|
+
)
|
|
285
|
+
|
|
278
286
|
sessions_block: "dict | None" = None
|
|
279
287
|
session_list: "list[dict] | None" = None
|
|
280
288
|
if roster is not None:
|
|
281
289
|
records = sources.sessions(listing=[dict(r) for r in roster])
|
|
282
290
|
sessions_block = {
|
|
283
291
|
"live": sum(1 for r in records if r.get("live")),
|
|
284
|
-
"managed":
|
|
292
|
+
"managed": roster_managed,
|
|
285
293
|
}
|
|
286
294
|
session_list = [
|
|
287
295
|
_session_row(r, repo_by_session=repo_by_session, parse_session=parse)
|
|
@@ -307,11 +315,13 @@ def build_snapshot(
|
|
|
307
315
|
+ int(latest.get("revert_candidates") or 0)
|
|
308
316
|
)
|
|
309
317
|
|
|
310
|
-
roster_live, roster_managed = parking.roster_counts(roster)
|
|
311
318
|
idle = parking.idle_block(
|
|
312
319
|
latest, live=roster_live, managed=roster_managed, queued=queued,
|
|
313
320
|
drain=drain, now=now_ts,
|
|
314
321
|
)
|
|
322
|
+
# `gone` is the console's observation; Studio's idle schema is strict and
|
|
323
|
+
# does not carry it.
|
|
324
|
+
idle.pop("gone", None)
|
|
315
325
|
|
|
316
326
|
drift = sources.drift()
|
|
317
327
|
snapshot = {
|
|
@@ -8,8 +8,8 @@ the devloop half of that contract, shared by the daemon (which computes the
|
|
|
8
8
|
verdict per pass and pushes it with the fleet vitals) and the console (which
|
|
9
9
|
serves it on ``GET /api/state`` and runs the drain handshake):
|
|
10
10
|
|
|
11
|
-
* **The idle block.** ``{asOf, passAt, owed, live, managed, queued,
|
|
12
|
-
drainedAt}``. `owed` counts the candidates whose LAST evaluation was an
|
|
11
|
+
* **The idle block.** ``{asOf, passAt, owed, live, managed, queued, gone,
|
|
12
|
+
timers, drainedAt}``. `owed` counts the candidates whose LAST evaluation was an
|
|
13
13
|
action or a timer the seat owns (a spawn, a fresh spawn row in its stale
|
|
14
14
|
window, an owed respawn, a deferral, a pending stall ping, a grace the seat
|
|
15
15
|
is waiting out, an evaluation that failed); a candidate the seat has
|
|
@@ -17,7 +17,9 @@ serves it on ``GET /api/state`` and runs the drain handshake):
|
|
|
17
17
|
owed. Each pass records the verdict per candidate on its poll snapshot's
|
|
18
18
|
stage (`owed`, `timer`), so the block is read back from the snapshot and
|
|
19
19
|
costs no GitHub call. `live` / `managed` / `queued` are read from tmux and
|
|
20
|
-
the alissa queue by whoever builds the block.
|
|
20
|
+
the alissa queue by whoever builds the block. `managed` is the live rows
|
|
21
|
+
plus the dead rows whose session backs a lane the seat still holds; a dead
|
|
22
|
+
row backing nothing is reported under `gone` instead (`roster_counts`).
|
|
21
23
|
* **Unknown is never idle.** No snapshot yet → `owed` is null; a listing that
|
|
22
24
|
failed → `live` / `managed` / `queued` are null. `idle_refusals` reads every
|
|
23
25
|
null as a refusal.
|
|
@@ -29,7 +31,7 @@ serves it on ``GET /api/state`` and runs the drain handshake):
|
|
|
29
31
|
from __future__ import annotations
|
|
30
32
|
|
|
31
33
|
from datetime import datetime, timezone
|
|
32
|
-
from typing import Iterable, Mapping, Sequence
|
|
34
|
+
from typing import Collection, Iterable, Mapping, Sequence
|
|
33
35
|
|
|
34
36
|
# The timer kinds of the design's idle block (§2.1). devloop owns the first
|
|
35
37
|
# three; the last two are revloop's and are listed so the union is stated once.
|
|
@@ -111,19 +113,107 @@ def queued_items(queues: "object") -> "int | None":
|
|
|
111
113
|
return count
|
|
112
114
|
|
|
113
115
|
|
|
116
|
+
# The tmux prefix `alissa tmux` puts on a managed session's real name
|
|
117
|
+
# (`alissa.MANAGED_PREFIX`, restated so this module imports nothing of the
|
|
118
|
+
# daemon's). A listing row carries both spellings (`name` bare, `session`
|
|
119
|
+
# prefixed), a snapshot stage the bare one, a queue the prefixed one.
|
|
120
|
+
MANAGED_PREFIX = "ali-"
|
|
121
|
+
|
|
122
|
+
# The spawn ledgers (`Sources.ledgers` keys) with the snapshot edge each one
|
|
123
|
+
# feeds, the column holding its issue / PR number, and the slug separator the
|
|
124
|
+
# poll snapshot uses for it (`owner/repo#7`, `owner/repo#pr16`).
|
|
125
|
+
LEDGER_LANES = (
|
|
126
|
+
("spawns", "issue", "issue", "#"),
|
|
127
|
+
("fix_spawns", "fix", "number", "#pr"),
|
|
128
|
+
("maintain_spawns", "maintain", "number", "#pr"),
|
|
129
|
+
("revert_spawns", "revert", "number", "#pr"),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# A stage holding one of these actions has a worker in flight on it whatever
|
|
133
|
+
# its owed verdict says: its session is never an orphan.
|
|
134
|
+
IN_FLIGHT_ACTIONS = frozenset({"spawned", "in-flight"})
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def bare_session(name: "object") -> str:
|
|
138
|
+
"""A session name without its `ali-` tmux prefix ("" for a non-name)."""
|
|
139
|
+
text = str(name or "")
|
|
140
|
+
return text[len(MANAGED_PREFIX):] if text.startswith(MANAGED_PREFIX) else text
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def lane_sessions(
|
|
144
|
+
snapshot: "Mapping | None",
|
|
145
|
+
*,
|
|
146
|
+
ledgers: "Mapping | None" = None,
|
|
147
|
+
queues: "object" = None,
|
|
148
|
+
) -> "frozenset[str] | None":
|
|
149
|
+
"""The bare names of the sessions that back a lane the seat still holds
|
|
150
|
+
(issue #166): the session a stage that is owed or in flight names; every
|
|
151
|
+
session the spawn ledgers record on such a stage's lane (an earlier
|
|
152
|
+
attempt the lane may still resume); and every session the alissa queue
|
|
153
|
+
holds an undispatched item for. None before the first pass -- with no
|
|
154
|
+
snapshot nothing can be called an orphan, so every row stays managed."""
|
|
155
|
+
if snapshot is None:
|
|
156
|
+
return None
|
|
157
|
+
names: "set[str]" = set()
|
|
158
|
+
held: "set[tuple[str, str]]" = set()
|
|
159
|
+
for stage in snapshot.get("stages") or ():
|
|
160
|
+
if not isinstance(stage, Mapping):
|
|
161
|
+
continue
|
|
162
|
+
if not (stage_owed(stage) or stage.get("stage") in IN_FLIGHT_ACTIONS):
|
|
163
|
+
continue
|
|
164
|
+
held.add((str(stage.get("edge") or ""), str(stage.get("slug") or "")))
|
|
165
|
+
if stage.get("session"):
|
|
166
|
+
names.add(bare_session(stage["session"]))
|
|
167
|
+
for table, edge, column, sep in LEDGER_LANES:
|
|
168
|
+
for row in (ledgers or {}).get(table) or ():
|
|
169
|
+
lane = (edge, f"{row.get('repo_slug')}{sep}{row.get(column)}")
|
|
170
|
+
if lane in held and row.get("session"):
|
|
171
|
+
names.add(bare_session(row["session"]))
|
|
172
|
+
if isinstance(queues, list):
|
|
173
|
+
for queue in queues:
|
|
174
|
+
if not isinstance(queue, Mapping):
|
|
175
|
+
continue
|
|
176
|
+
if any(
|
|
177
|
+
isinstance(item, Mapping) and not item.get("lastDispatchedAt")
|
|
178
|
+
for item in queue.get("items") or ()
|
|
179
|
+
):
|
|
180
|
+
names.add(bare_session(queue.get("session")))
|
|
181
|
+
names.discard("")
|
|
182
|
+
return frozenset(names)
|
|
183
|
+
|
|
184
|
+
|
|
114
185
|
def roster_counts(
|
|
115
186
|
roster: "Sequence[Mapping] | None",
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
187
|
+
lanes: "Collection[str] | None" = None,
|
|
188
|
+
) -> "tuple[int | None, int | None, list[str] | None]":
|
|
189
|
+
"""`(live, managed, gone)` from an ``alissa tmux ls --json`` listing.
|
|
190
|
+
|
|
191
|
+
A row is live unless it says otherwise (the fleet-vitals `sessions`
|
|
192
|
+
rule: its `live` flag, else `status != "gone"`). A row is MANAGED when it
|
|
193
|
+
is live or when its session backs a lane the seat still holds -- `lanes`,
|
|
194
|
+
the bare names `lane_sessions` returns. A dead row matching no lane is an
|
|
195
|
+
orphan: it is not managed, and its name is reported in `gone` instead
|
|
196
|
+
(issue #166 -- one such row kept orcloop's `managed == 0` from ever
|
|
197
|
+
holding). `lanes=None` (no completed pass to read lanes from) keeps every
|
|
198
|
+
row managed: unknown is never idle. `(None, None, None)` when the listing
|
|
199
|
+
failed."""
|
|
120
200
|
if roster is None:
|
|
121
|
-
return None, None
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
201
|
+
return None, None, None
|
|
202
|
+
live = managed = 0
|
|
203
|
+
gone: "list[str]" = []
|
|
204
|
+
for row in roster:
|
|
205
|
+
if not isinstance(row, Mapping):
|
|
206
|
+
continue
|
|
207
|
+
if bool(row.get("live", row.get("status") != "gone")):
|
|
208
|
+
live += 1
|
|
209
|
+
managed += 1
|
|
210
|
+
continue
|
|
211
|
+
name = bare_session(row.get("name") or row.get("session"))
|
|
212
|
+
if lanes is None or name in lanes or bare_session(row.get("session")) in lanes:
|
|
213
|
+
managed += 1
|
|
214
|
+
else:
|
|
215
|
+
gone.append(name)
|
|
216
|
+
return live, managed, sorted(gone)
|
|
127
217
|
|
|
128
218
|
|
|
129
219
|
def idle_block(
|
|
@@ -134,10 +224,13 @@ def idle_block(
|
|
|
134
224
|
queued: "int | None",
|
|
135
225
|
drain: "Mapping | None",
|
|
136
226
|
now: "float | int",
|
|
227
|
+
gone: "Sequence[str] | None" = None,
|
|
137
228
|
) -> dict:
|
|
138
229
|
"""The seat's idle block from its latest poll snapshot (None before the
|
|
139
230
|
first pass) and the caller's live readings. `drain` is the holding drain
|
|
140
|
-
row (`State.drain`) or None.
|
|
231
|
+
row (`State.drain`) or None. `gone` is `roster_counts`' orphaned rows,
|
|
232
|
+
reported as `{count, sessions}` (null when the listing was not read) --
|
|
233
|
+
an observation for the operator, never a refusal."""
|
|
141
234
|
stages: "Iterable[Mapping]" = (snapshot or {}).get("stages") or ()
|
|
142
235
|
owed: "int | None" = None
|
|
143
236
|
timers: "list[dict]" = []
|
|
@@ -157,6 +250,10 @@ def idle_block(
|
|
|
157
250
|
"live": live,
|
|
158
251
|
"managed": managed,
|
|
159
252
|
"queued": queued,
|
|
253
|
+
"gone": (
|
|
254
|
+
{"count": len(gone), "sessions": list(gone)}
|
|
255
|
+
if gone is not None else None
|
|
256
|
+
),
|
|
160
257
|
"timers": timers,
|
|
161
258
|
"drainedAt": iso(drain.get("drained_at")) if drain else None,
|
|
162
259
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.8.
|
|
1
|
+
0.8.44
|
|
@@ -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 -------------------------
|
|
@@ -438,54 +457,89 @@ class Sources:
|
|
|
438
457
|
a drain holds. A caller that already holds the listing (None when it
|
|
439
458
|
failed) or the latest snapshot (None before the first pass) hands it
|
|
440
459
|
in; anything not handed in is read here. A failed listing is null
|
|
441
|
-
counts, never zeros.
|
|
460
|
+
counts, never zeros. A dead row backing no lane the seat holds is
|
|
461
|
+
reported under `gone`, not counted as managed (issue #166)."""
|
|
442
462
|
if latest is _UNREAD:
|
|
443
463
|
snaps = self.snapshots(1)
|
|
444
464
|
latest = snaps[0] if snaps else None
|
|
445
465
|
if listing is _UNREAD:
|
|
446
466
|
listing = self._safe_json(["alissa", "tmux", "ls", "--json"])
|
|
447
467
|
roster = listing if isinstance(listing, list) else None
|
|
448
|
-
|
|
468
|
+
snapshot = latest if isinstance(latest, dict) else None
|
|
469
|
+
queues = self._safe_json(["alissa", "tmux", "queue", "ls", "--json"])
|
|
470
|
+
# A dead row counts as managed only while it backs a lane the seat
|
|
471
|
+
# still holds (issue #166).
|
|
472
|
+
lanes = parking.lane_sessions(
|
|
473
|
+
snapshot, ledgers=self.ledgers(), queues=queues,
|
|
474
|
+
)
|
|
475
|
+
live, managed, gone = parking.roster_counts(roster, lanes)
|
|
449
476
|
return parking.idle_block(
|
|
450
|
-
|
|
451
|
-
|
|
477
|
+
snapshot, live=live, managed=managed,
|
|
478
|
+
queued=parking.queued_items(queues), gone=gone,
|
|
452
479
|
drain=self.drain(), now=self._wall(),
|
|
453
480
|
)
|
|
454
481
|
|
|
455
|
-
def
|
|
456
|
-
"""
|
|
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`.
|
|
457
485
|
|
|
458
486
|
Read off the CLI's own `--help`, which is local, tokenless and free.
|
|
459
|
-
The alternative -- send
|
|
487
|
+
The alternative -- send a flag and fall back when the call fails --
|
|
460
488
|
cannot tell an unknown flag from an auth hiccup, so it would spend a
|
|
461
|
-
second
|
|
462
|
-
|
|
489
|
+
second, wider call on every unlucky refresh, which is the cost this
|
|
490
|
+
whole path exists to avoid.
|
|
463
491
|
|
|
464
492
|
A probe that *answers* is memoized for the process (the CLI cannot
|
|
465
493
|
change under a running console). A probe that fails is deliberately
|
|
466
494
|
NOT memoized: a transient `alissa` failure then degrades this refresh
|
|
467
|
-
only, instead of pinning the console to the expensive
|
|
495
|
+
only, instead of pinning the console to the expensive unflagged call
|
|
468
496
|
until someone restarts it."""
|
|
469
|
-
if self.
|
|
497
|
+
if self._list_flags is None:
|
|
470
498
|
helptext = self._safe_run(
|
|
471
499
|
["alissa", "task", "list", "--help"], timeout=20
|
|
472
500
|
)
|
|
473
501
|
if helptext is None:
|
|
474
|
-
return
|
|
475
|
-
self.
|
|
476
|
-
|
|
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
|
|
477
509
|
|
|
478
|
-
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]":
|
|
479
532
|
"""The worker actor's own tasks via `alissa task list` -- the
|
|
480
533
|
actor-partition bridge (the console runs as the operator, whose actor
|
|
481
534
|
owns the implementation tasks the dev sessions created). Best-effort:
|
|
482
535
|
an old CLI or an auth hiccup just yields an empty list.
|
|
483
536
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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.
|
|
489
543
|
|
|
490
544
|
Cached (TASKS_CACHE_TTL) exactly like the two other remote checks: this
|
|
491
545
|
call rides the shared ALISSA_API_TOKEN the daemon uses, so on the ~10s
|
|
@@ -493,16 +547,7 @@ class Sources:
|
|
|
493
547
|
the API budget. A fetch that fails returns None and keeps the last good
|
|
494
548
|
list (the _Cache contract), never overwriting it with junk."""
|
|
495
549
|
def fetch() -> "list[dict] | None":
|
|
496
|
-
|
|
497
|
-
self_scoped = self._supports_self_scope()
|
|
498
|
-
argv = (unscoped + [TASKS_SELF_FLAG]) if self_scoped else unscoped
|
|
499
|
-
raw = self._safe_json(argv, timeout=45)
|
|
500
|
-
if raw is None and self_scoped:
|
|
501
|
-
# The CLI advertised the flag but the call brought nothing
|
|
502
|
-
# back -- an API that does not serve the scope yet, say. One
|
|
503
|
-
# unflagged retry keeps the pane populated. A valid but empty
|
|
504
|
-
# `[]` is a real answer, not a failure, and is never retried.
|
|
505
|
-
raw = self._safe_json(unscoped, timeout=45)
|
|
550
|
+
raw = self._safe_json(self._task_list_argv(), timeout=45)
|
|
506
551
|
if not isinstance(raw, list):
|
|
507
552
|
return None
|
|
508
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.44
|
|
4
4
|
Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
|
|
5
5
|
Home-page: https://alissa.app
|
|
6
6
|
Author: Fahera
|
|
@@ -194,6 +194,32 @@ resolved from the remote name or URL in the directory the push runs in
|
|
|
194
194
|
no marker are untouched. `ALISSA_SHELL_GUARD=off` disables this guard with
|
|
195
195
|
the rest.
|
|
196
196
|
|
|
197
|
+
## Seat parking: what counts as managed (issue #166)
|
|
198
|
+
|
|
199
|
+
The idle block on `GET /api/state` (`parking.idle_block`, issue #153) is what
|
|
200
|
+
orcloop's D2 invariant reads before it drains and parks the seat: idle only
|
|
201
|
+
when `live == managed == queued == owed == 0` with no timer and a fresh pass.
|
|
202
|
+
`live` and `managed` come from `alissa tmux ls --json`:
|
|
203
|
+
|
|
204
|
+
- a row is **live** unless it says otherwise (its `live` flag, else
|
|
205
|
+
`status != "gone"`);
|
|
206
|
+
- a row is **managed** when it is live, **or** when it is dead but its
|
|
207
|
+
session still backs a lane the seat holds — the session named by a stage
|
|
208
|
+
of the latest poll snapshot that is owed or in flight (`spawned` /
|
|
209
|
+
`in-flight`), any session the spawn ledgers record on such a stage's lane
|
|
210
|
+
(an earlier attempt the lane may still resume), or a session the alissa
|
|
211
|
+
queue holds an undispatched item for;
|
|
212
|
+
- a dead row backing none of these is an **orphan**: it is not managed, and
|
|
213
|
+
is reported in the block's `gone` field as `{count, sessions}` so the
|
|
214
|
+
console and orcloop's logs still show it.
|
|
215
|
+
|
|
216
|
+
Unknown is never idle: a failed listing leaves `live`, `managed` and `gone`
|
|
217
|
+
null, and before the first completed pass (no snapshot to read lanes from)
|
|
218
|
+
every dead row stays managed. The console only reports an orphan — it never
|
|
219
|
+
kills or reaps one. The fleet-vitals push applies the same rule to
|
|
220
|
+
`sessions.managed` and `idle.managed` (Studio's park guard reads both) and
|
|
221
|
+
leaves `gone` out, since Studio's idle schema is strict.
|
|
222
|
+
|
|
197
223
|
## License
|
|
198
224
|
|
|
199
225
|
Licensed under the
|
{alissa_tools_github_devloop-0.8.42.dist-info → alissa_tools_github_devloop-0.8.44.dist-info}/RECORD
RENAMED
|
@@ -4,29 +4,29 @@ alissa/tools/github/devloop/alissa.py,sha256=TJU4eSZc2MvP35HewCwBKvhPAel32vKjtt3
|
|
|
4
4
|
alissa/tools/github/devloop/alissa_client.py,sha256=wmpXYWf9ThcLeYnz6gGeDz7gj5BpYjduuVASMc-QOOY,14873
|
|
5
5
|
alissa/tools/github/devloop/bows.py,sha256=pzDNMRk_S78FX7vMNYMlXHxW6VPHPo1Xd2EzFMgVCUM,17325
|
|
6
6
|
alissa/tools/github/devloop/config.py,sha256=j6W0Wrz7smFdFdSs3g5S6xadcxfFS5YEhQDL64yAuJk,75109
|
|
7
|
-
alissa/tools/github/devloop/fleet_vitals.py,sha256=
|
|
7
|
+
alissa/tools/github/devloop/fleet_vitals.py,sha256=5mWnRkJh8u9soSplud3UMxkelT6EqAiz-ivX8UmfIqk,25477
|
|
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
11
|
alissa/tools/github/devloop/loop.py,sha256=uMfuomSdIPC8KkWUGF0OrW3RR9OSC1txKVnXGZ1SeIc,613973
|
|
12
12
|
alissa/tools/github/devloop/loop_events.py,sha256=tmso0OALqY2yAPW2Fo6ae0sYetQIdIeIYooJ2Sc2cc4,37921
|
|
13
|
-
alissa/tools/github/devloop/parking.py,sha256=
|
|
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=va232A0BU1OdMrEBtZ5PyqpxusfsBdgt4whBySGjLHY,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.44.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
27
|
+
alissa_tools_github_devloop-0.8.44.dist-info/licenses/NOTICE,sha256=FKstNs3uw_tQod4_1XcFLD1FUTJQXf4FC0oXJAOA_HE,260
|
|
28
|
+
alissa_tools_github_devloop-0.8.44.dist-info/METADATA,sha256=s-GkeBOMMoSQatUPgO0Hg_HgIyoe5TlcNi-8D-NwSYQ,12222
|
|
29
|
+
alissa_tools_github_devloop-0.8.44.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
30
|
+
alissa_tools_github_devloop-0.8.44.dist-info/entry_points.txt,sha256=8aC7Ajzx4bu9ZRR37a40IDfuStacvZP5KsAxY1dbFKU,145
|
|
31
|
+
alissa_tools_github_devloop-0.8.44.dist-info/top_level.txt,sha256=DodjDg-l-TWQnlxG-Vuc3G5sB4O7cWGQwe6XrVx3u-A,7
|
|
32
|
+
alissa_tools_github_devloop-0.8.44.dist-info/RECORD,,
|
{alissa_tools_github_devloop-0.8.42.dist-info → alissa_tools_github_devloop-0.8.44.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|