alissa-tools-github-revloop 0.16.16__tar.gz → 0.18.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {alissa_tools_github_revloop-0.16.16/src/main/alissa_tools_github_revloop.egg-info → alissa_tools_github_revloop-0.18.0}/PKG-INFO +1 -1
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/__main__.py +45 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/alissa.py +215 -12
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/config.py +90 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/loop.py +668 -27
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/prreview.py +7 -1
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/state.py +247 -0
- alissa_tools_github_revloop-0.18.0/src/main/alissa/tools/github/revloop/version +1 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/webui/page.py +1 -1
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0/src/main/alissa_tools_github_revloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_revloop-0.16.16/src/main/alissa/tools/github/revloop/version +0 -1
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/LICENSE +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/MANIFEST.in +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/NOTICE +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/README.md +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/requirements.txt +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/setup.cfg +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/setup.py +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/__init__.py +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/ghclient.py +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/proc.py +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/version.py +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/webui/__init__.py +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/webui/__main__.py +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/webui/auth.py +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/webui/server.py +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/webui/sources.py +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa/tools/github/revloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa_tools_github_revloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa_tools_github_revloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa_tools_github_revloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_revloop-0.16.16 → alissa_tools_github_revloop-0.18.0}/src/main/alissa_tools_github_revloop.egg-info/top_level.txt +0 -0
|
@@ -137,6 +137,40 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
137
137
|
"rollup is still running (or unreadable) before recording the verdict "
|
|
138
138
|
"as a comment instead; a red rollup never waits and never approves",
|
|
139
139
|
)
|
|
140
|
+
over.add_argument(
|
|
141
|
+
"--checks-spawn-wait-seconds",
|
|
142
|
+
type=int,
|
|
143
|
+
metavar="SECONDS",
|
|
144
|
+
help="how long an owed round waits for the head's CI to conclude before "
|
|
145
|
+
"its reviewer is queued at all, so a session cannot approve ahead of the "
|
|
146
|
+
"evidence; 0 queues immediately and relies on the directive alone",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
over.add_argument(
|
|
150
|
+
"--review-task-miss-ttl-polls",
|
|
151
|
+
type=int,
|
|
152
|
+
metavar="N",
|
|
153
|
+
help="how many polls a PR with NO review task is taken on trust before "
|
|
154
|
+
"the task corpus is searched for one again; must be >= 1",
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
scope = over.add_mutually_exclusive_group()
|
|
158
|
+
scope.add_argument(
|
|
159
|
+
"--task-list-self-scope",
|
|
160
|
+
dest="task_list_self_scope",
|
|
161
|
+
action="store_true",
|
|
162
|
+
default=None,
|
|
163
|
+
help="narrow `alissa task list` to this actor's own rows (--self), "
|
|
164
|
+
"dropping the sponsor's corpus. Only for deployments where EVERY "
|
|
165
|
+
"review task is created by this daemon's own sessions: a review task "
|
|
166
|
+
"the list cannot see is a round the daemon cannot count",
|
|
167
|
+
)
|
|
168
|
+
scope.add_argument(
|
|
169
|
+
"--no-task-list-self-scope",
|
|
170
|
+
dest="task_list_self_scope",
|
|
171
|
+
action="store_false",
|
|
172
|
+
help="list the sponsor-union corpus even if the config narrows it",
|
|
173
|
+
)
|
|
140
174
|
|
|
141
175
|
dry = over.add_mutually_exclusive_group()
|
|
142
176
|
dry.add_argument(
|
|
@@ -174,6 +208,9 @@ def overrides_from(args: argparse.Namespace) -> dict:
|
|
|
174
208
|
"reap_session_cap": args.reap_session_cap,
|
|
175
209
|
"max_concurrent_sessions": args.max_concurrent_sessions,
|
|
176
210
|
"checks_wait_seconds": args.checks_wait_seconds,
|
|
211
|
+
"checks_spawn_wait_seconds": args.checks_spawn_wait_seconds,
|
|
212
|
+
"review_task_miss_ttl_polls": args.review_task_miss_ttl_polls,
|
|
213
|
+
"task_list_self_scope": args.task_list_self_scope,
|
|
177
214
|
"dry_run": args.dry_run,
|
|
178
215
|
}
|
|
179
216
|
|
|
@@ -217,6 +254,14 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
217
254
|
|
|
218
255
|
if args.pr:
|
|
219
256
|
owner, repo, number = parse_pr_ref(args.pr)
|
|
257
|
+
# This mode exists to tell "the search did not find it" apart from
|
|
258
|
+
# "the decision was no", so it must never be answered by the
|
|
259
|
+
# negative cache -- a suppressed pass would report "no review task"
|
|
260
|
+
# without looking, which is precisely the confusion the flag is for.
|
|
261
|
+
# Re-arming here (rather than plumbing a bypass through `evaluate`)
|
|
262
|
+
# keeps the poll path with exactly one way in, and costs the daemon
|
|
263
|
+
# one corpus fetch on a hand-run diagnostic.
|
|
264
|
+
watcher.state.forget_review_task_miss(f"{owner}/{repo}", number)
|
|
220
265
|
decision = watcher.evaluate(owner, repo, number)
|
|
221
266
|
print(f"\n{args.pr} → {decision.action.value}")
|
|
222
267
|
print(f" round: {decision.round}")
|
|
@@ -16,6 +16,72 @@ log = logging.getLogger(__name__)
|
|
|
16
16
|
# A review task is "open" while it can still receive a verdict.
|
|
17
17
|
OPEN_STATUSES = {"committed", "in_progress", "pending_validation", "todo"}
|
|
18
18
|
|
|
19
|
+
# -- narrowing the `alissa task list` call (issue #87) ------------------------
|
|
20
|
+
#
|
|
21
|
+
# `list_tasks` is the widest query this daemon issues -- no query string at all,
|
|
22
|
+
# the actor's entire non-terminal corpus, sponsor-union scoped -- and it was the
|
|
23
|
+
# single largest contributor to the Alissa deployment's #1 Database-I/O offender
|
|
24
|
+
# over 2026-08-12..16. Everything below is applied ONLY when the installed CLI
|
|
25
|
+
# advertises it (see Alissa.probe_task_list): an issue's claim about a flag is
|
|
26
|
+
# not evidence, and this daemon turns a non-zero `alissa` exit into a SKIPPED
|
|
27
|
+
# decision, so sending a flag the CLI does not have costs a review.
|
|
28
|
+
|
|
29
|
+
# Statuses a LIVE review task can hold. Deliberately OPEN_STATUSES itself and
|
|
30
|
+
# not a hand-written list: `is_review_task_for` already rejects every other
|
|
31
|
+
# status client-side, so filtering server-side on exactly this set cannot change
|
|
32
|
+
# which task the daemon resolves -- it only stops shipping the rows over the
|
|
33
|
+
# wire. Any status added to `is_open` is added here by construction.
|
|
34
|
+
TASK_LIST_STATUS_FLAG = "--status"
|
|
35
|
+
TASK_LIST_STATUS_FILTER = ",".join(sorted(OPEN_STATUSES))
|
|
36
|
+
|
|
37
|
+
# `--self` drops the SPONSOR's corpus and keeps only the calling actor's rows.
|
|
38
|
+
#
|
|
39
|
+
# NOT enabled by default, and the reason is measured rather than cautious. On
|
|
40
|
+
# the live fleet corpus (2026-08-16, 932 non-terminal rows) `--self` removes 36
|
|
41
|
+
# rows, 4% of the payload -- and 3 of the 371 `Review PR ...` tasks in it are
|
|
42
|
+
# among the rows it removes: they are owned by another actor, not by the agent
|
|
43
|
+
# actor whose sessions write the other 368. Review tasks are therefore
|
|
44
|
+
# PREDOMINANTLY actor-owned but not exclusively so, and a review task this call
|
|
45
|
+
# cannot see is a round the daemon cannot count. 4% of the wire is not worth
|
|
46
|
+
# that, so the flag is opt-in per deployment (`task_list_self_scope`).
|
|
47
|
+
TASK_LIST_SELF_FLAG = "--self"
|
|
48
|
+
|
|
49
|
+
# A lean projection of each row. The daemon keeps only taskNumber/title/status
|
|
50
|
+
# (see `_task_from_row`), so a digest view is pure saving with no semantics --
|
|
51
|
+
# which is why it is adopted whenever it exists and has no knob. It ships in the
|
|
52
|
+
# studio repo separately; until then the probe simply does not find it.
|
|
53
|
+
TASK_LIST_VIEW_FLAG = "--view"
|
|
54
|
+
TASK_LIST_DIGEST_VIEW = "digest"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(frozen=True)
|
|
58
|
+
class TaskListFlags:
|
|
59
|
+
"""What the installed `alissa task list` advertises in its own help.
|
|
60
|
+
|
|
61
|
+
All-False is both the "old CLI" answer and the "the probe could not run"
|
|
62
|
+
answer, and they are deliberately the same value: each means "make the call
|
|
63
|
+
the daemon has always made".
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
status: bool = False
|
|
67
|
+
self_scope: bool = False
|
|
68
|
+
digest: bool = False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _advertises(helptext: str, flag: str) -> bool:
|
|
72
|
+
"""Whether `flag` appears as an OPTION in a CLI help listing.
|
|
73
|
+
|
|
74
|
+
Anchored to the start of a help line (allowing a short alias in front, as in
|
|
75
|
+
`-h, --help`) so a flag merely NAMED in some other option's prose -- "Pair
|
|
76
|
+
with --include-shared" is in this very help text -- is not read as an offer
|
|
77
|
+
of that flag. The trailing guard rejects a longer flag that merely starts
|
|
78
|
+
with this one (`--self-only` is not `--self`).
|
|
79
|
+
"""
|
|
80
|
+
return re.search(
|
|
81
|
+
rf"(?m)^\s*(?:-\w,\s+)?{re.escape(flag)}(?![\w-])", helptext
|
|
82
|
+
) is not None
|
|
83
|
+
|
|
84
|
+
|
|
19
85
|
# CR6 verdict envelope outcomes.
|
|
20
86
|
VERDICT_APPROVE = "approve"
|
|
21
87
|
VERDICT_REQUEST_CHANGES = "request_changes"
|
|
@@ -213,19 +279,156 @@ class TaskDetail:
|
|
|
213
279
|
|
|
214
280
|
|
|
215
281
|
class Alissa:
|
|
216
|
-
def
|
|
217
|
-
"""
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
282
|
+
def __init__(self, *, task_list_self_scope: bool = False) -> None:
|
|
283
|
+
"""`task_list_self_scope` opts the list call into `--self`.
|
|
284
|
+
|
|
285
|
+
Default OFF, and that default is evidence, not caution -- see
|
|
286
|
+
TASK_LIST_SELF_FLAG. It is still a knob because ownership is a property
|
|
287
|
+
of a DEPLOYMENT (who creates its review tasks), not of this code, and an
|
|
288
|
+
operator who knows their review tasks are all actor-owned should be able
|
|
289
|
+
to say so.
|
|
290
|
+
"""
|
|
291
|
+
self._task_list_self_scope = bool(task_list_self_scope)
|
|
292
|
+
# The probe's answer, memoized for the process; None = not probed yet.
|
|
293
|
+
# A probe that FAILS is deliberately not memoized (see probe_task_list).
|
|
294
|
+
self._task_list_flags: "TaskListFlags | None" = None
|
|
295
|
+
# Set when a narrowed call has been disproved at RUNTIME -- the CLI
|
|
296
|
+
# advertised a flag whose call then failed or came back empty. From then
|
|
297
|
+
# on this process makes the plain call, because a list that answers
|
|
298
|
+
# wrongly is worse than a list that is large: `find_review_task` reads an
|
|
299
|
+
# empty corpus as "this PR has no review task".
|
|
300
|
+
self._task_list_narrowing_disabled = False
|
|
301
|
+
|
|
302
|
+
# -- the `alissa task list` narrowing probe -----------------------------
|
|
303
|
+
|
|
304
|
+
def probe_task_list(self) -> "TaskListFlags":
|
|
305
|
+
"""Which narrowing flags the INSTALLED `alissa task list` advertises.
|
|
306
|
+
|
|
307
|
+
Read off the CLI's own `--help`, which is local, tokenless and free.
|
|
308
|
+
The alternative -- send the flag and fall back when the call fails --
|
|
309
|
+
cannot tell an unknown flag from an auth hiccup, and this daemon turns a
|
|
310
|
+
non-zero `alissa` exit into a SKIPPED decision, so a mis-sent flag does
|
|
311
|
+
not cost a slower call, it costs a REVIEW.
|
|
312
|
+
|
|
313
|
+
Probed off the help OUTPUT rather than the exit status on purpose: this
|
|
314
|
+
CLI is commander-based and answers an unknown *subcommand* by printing
|
|
315
|
+
the parent help and exiting 0, so "it exited 0" reports every old CLI as
|
|
316
|
+
capable. (Flags are stricter than subcommands here, but the rule is the
|
|
317
|
+
same one and there is no reason to keep two.)
|
|
318
|
+
|
|
319
|
+
A probe that ANSWERS is memoized for the process -- the CLI cannot
|
|
320
|
+
change under a running daemon. A probe that FAILS is not: a transient
|
|
321
|
+
`alissa` failure then degrades one pass instead of pinning the daemon to
|
|
322
|
+
the widest call until someone restarts it.
|
|
323
|
+
"""
|
|
324
|
+
if self._task_list_flags is not None:
|
|
325
|
+
return self._task_list_flags
|
|
326
|
+
try:
|
|
327
|
+
helptext = run(["alissa", "task", "list", "--help"], timeout=20)
|
|
328
|
+
except CommandError as exc:
|
|
329
|
+
log.warning(
|
|
330
|
+
"could not probe `alissa task list --help` (%s) — this pass "
|
|
331
|
+
"lists tasks unnarrowed, as the daemon always did", exc,
|
|
332
|
+
)
|
|
333
|
+
return TaskListFlags()
|
|
334
|
+
except Exception: # pragma: no cover - defence in depth
|
|
335
|
+
log.exception("unexpected failure probing `alissa task list --help`")
|
|
336
|
+
return TaskListFlags()
|
|
337
|
+
|
|
338
|
+
flags = TaskListFlags(
|
|
339
|
+
status=_advertises(helptext, TASK_LIST_STATUS_FLAG),
|
|
340
|
+
self_scope=_advertises(helptext, TASK_LIST_SELF_FLAG),
|
|
341
|
+
digest=_advertises(helptext, TASK_LIST_VIEW_FLAG),
|
|
342
|
+
)
|
|
343
|
+
self._task_list_flags = flags
|
|
344
|
+
return flags
|
|
345
|
+
|
|
346
|
+
def task_list_argv(self, *, narrow_status: bool = True) -> list[str]:
|
|
347
|
+
"""The narrowest `alissa task list` this CLI actually supports.
|
|
348
|
+
|
|
349
|
+
Every addition is probe-gated, so an older CLI -- today's, which offers
|
|
350
|
+
none of them -- produces exactly the call the daemon has always made.
|
|
227
351
|
"""
|
|
228
|
-
|
|
352
|
+
argv = ["alissa", "task", "list", "--json"]
|
|
353
|
+
if self._task_list_narrowing_disabled:
|
|
354
|
+
return argv
|
|
355
|
+
flags = self.probe_task_list()
|
|
356
|
+
if flags.status and narrow_status:
|
|
357
|
+
argv += [TASK_LIST_STATUS_FLAG, TASK_LIST_STATUS_FILTER]
|
|
358
|
+
if flags.self_scope and self._task_list_self_scope:
|
|
359
|
+
argv.append(TASK_LIST_SELF_FLAG)
|
|
360
|
+
if flags.digest:
|
|
361
|
+
argv += [TASK_LIST_VIEW_FLAG, TASK_LIST_DIGEST_VIEW]
|
|
362
|
+
return argv
|
|
363
|
+
|
|
364
|
+
def list_tasks(self, *, narrow_status: bool = True) -> list[Task]:
|
|
365
|
+
"""This actor's live task corpus -- the expensive call.
|
|
366
|
+
|
|
367
|
+
`alissa task list` (CLI 0.1.0) exposed no server-side narrowing at all:
|
|
368
|
+
its only flags were `--json` and `--include-terminal`, and omitting the
|
|
369
|
+
latter -- already the default -- was the whole of the available
|
|
370
|
+
filtering. Newer CLIs offer more, so the call is now assembled from a
|
|
371
|
+
boot-time probe of the installed CLI's help (`task_list_argv`): a status
|
|
372
|
+
filter covering exactly the statuses a live review task can hold, a lean
|
|
373
|
+
`--view digest`, and `--self` when the deployment says its review tasks
|
|
374
|
+
are actor-owned. None of it is required; an absent flag is simply not
|
|
375
|
+
sent.
|
|
376
|
+
|
|
377
|
+
Narrowing is still the SECOND line of defence, not the first. Even a
|
|
378
|
+
perfectly narrowed call is the actor's whole review-task corpus, so the
|
|
379
|
+
daemon's job remains to call this RARELY: see loop._review_task (the
|
|
380
|
+
persisted PR -> task mapping), loop._pass_task_list (at most one fetch
|
|
381
|
+
per poll pass) and the negative cache behind them (state's
|
|
382
|
+
`review_task_misses`, which bounds the ONE case where none of those
|
|
383
|
+
help -- a PR that has no review task at all).
|
|
384
|
+
|
|
385
|
+
`narrow_status=False` is for callers that must see review tasks the
|
|
386
|
+
daemon's own `is_open` predicate would reject (prreview reads a task's
|
|
387
|
+
verdict envelope after the round is over). It suppresses only the status
|
|
388
|
+
filter; every other narrowing still applies.
|
|
389
|
+
|
|
390
|
+
A narrowed call that FAILS, or that answers with an empty corpus, is
|
|
391
|
+
retried once unnarrowed and turns the narrowing off for the rest of the
|
|
392
|
+
process. Both are how a CLI that advertises a flag its API does not
|
|
393
|
+
serve would present, and either would otherwise read as "this actor has
|
|
394
|
+
no review tasks" -- which is a skipped review, not a slower one.
|
|
395
|
+
"""
|
|
396
|
+
argv = self.task_list_argv(narrow_status=narrow_status)
|
|
397
|
+
plain = ["alissa", "task", "list", "--json"]
|
|
398
|
+
try:
|
|
399
|
+
data = run_json(argv, timeout=90) or []
|
|
400
|
+
except CommandError:
|
|
401
|
+
if argv == plain:
|
|
402
|
+
raise
|
|
403
|
+
log.warning(
|
|
404
|
+
"`%s` failed — retrying the plain task list and dropping the "
|
|
405
|
+
"narrowing for this process", " ".join(argv),
|
|
406
|
+
)
|
|
407
|
+
self._task_list_narrowing_disabled = True
|
|
408
|
+
data = run_json(plain, timeout=90) or []
|
|
409
|
+
|
|
410
|
+
tasks = self._tasks_from(data)
|
|
411
|
+
if tasks or argv == plain:
|
|
412
|
+
return tasks
|
|
413
|
+
|
|
414
|
+
# An empty answer from a narrowed call. A genuinely empty corpus is
|
|
415
|
+
# possible and costs one extra list; a filter the API does not serve
|
|
416
|
+
# would cost every review this actor owns.
|
|
417
|
+
log.warning(
|
|
418
|
+
"`%s` returned no tasks — retrying the plain task list to tell an "
|
|
419
|
+
"empty corpus from a filter this API does not serve", " ".join(argv),
|
|
420
|
+
)
|
|
421
|
+
tasks = self._tasks_from(run_json(plain, timeout=90) or [])
|
|
422
|
+
if tasks:
|
|
423
|
+
self._task_list_narrowing_disabled = True
|
|
424
|
+
log.warning(
|
|
425
|
+
"the plain task list returned %d task(s) — the narrowed call is "
|
|
426
|
+
"dropping rows, so this process stops narrowing", len(tasks),
|
|
427
|
+
)
|
|
428
|
+
return tasks
|
|
429
|
+
|
|
430
|
+
@staticmethod
|
|
431
|
+
def _tasks_from(data: object) -> list[Task]:
|
|
229
432
|
tasks = []
|
|
230
433
|
for row in data if isinstance(data, list) else []:
|
|
231
434
|
task = _task_from_row(row)
|
|
@@ -131,6 +131,9 @@ CONFIG_KEYS = (
|
|
|
131
131
|
"reap_session_cap",
|
|
132
132
|
"max_concurrent_sessions",
|
|
133
133
|
"checks_wait_seconds",
|
|
134
|
+
"checks_spawn_wait_seconds",
|
|
135
|
+
"review_task_miss_ttl_polls",
|
|
136
|
+
"task_list_self_scope",
|
|
134
137
|
"dry_run",
|
|
135
138
|
)
|
|
136
139
|
|
|
@@ -204,6 +207,44 @@ DEFAULT_MAX_CONCURRENT_SESSIONS = 4
|
|
|
204
207
|
# not of the daemon.
|
|
205
208
|
DEFAULT_CHECKS_WAIT_SECONDS = 30 * 60
|
|
206
209
|
|
|
210
|
+
# How long an owed round waits for the head's checks to CONCLUDE before its
|
|
211
|
+
# reviewer is queued at all (issue #84). The bound above protects the verdict
|
|
212
|
+
# the DAEMON posts; this one protects the verdict the reviewer SESSION posts,
|
|
213
|
+
# which is the normal path and the one that has no gate anywhere else: on
|
|
214
|
+
# studio #560 a session approved a head 29 seconds before that same head's
|
|
215
|
+
# `test` job failed, and the red PR carried a green approval for two hours. A
|
|
216
|
+
# session that has not started cannot approve early, so the wait is applied to
|
|
217
|
+
# the spawn.
|
|
218
|
+
#
|
|
219
|
+
# 15 minutes, not the 30 above, and the asymmetry is deliberate: this bound is
|
|
220
|
+
# paid as LATENCY on every round of every PR whose checks are running, while the
|
|
221
|
+
# verdict bound is paid only by a round that has already finished reviewing. CI
|
|
222
|
+
# in this fleet concludes in 3-5 minutes, so 15 is several times the normal wait
|
|
223
|
+
# and still bounded well inside one review round. 0 disables the pre-spawn wait
|
|
224
|
+
# entirely -- the round is queued immediately and its directive carries the
|
|
225
|
+
# still-running rollup, which is the directive-only posture.
|
|
226
|
+
DEFAULT_CHECKS_SPAWN_WAIT_SECONDS = 15 * 60
|
|
227
|
+
|
|
228
|
+
# How many polls a PR with NO review task may be taken on trust before the
|
|
229
|
+
# daemon searches the task corpus for one again (issue #87).
|
|
230
|
+
#
|
|
231
|
+
# The review-task cache can only remember an answer that EXISTS, so a PR with no
|
|
232
|
+
# review task -- a third-party PR, one whose task was validated or retitled --
|
|
233
|
+
# missed it on every pass and paid the widest read this daemon makes for the
|
|
234
|
+
# same answer every time: at a 60s poll, 1,440 full-corpus reads a day from one
|
|
235
|
+
# unmapped PR, forever.
|
|
236
|
+
#
|
|
237
|
+
# 10 is the trade, and the thing being traded is LATENCY, not correctness: a
|
|
238
|
+
# review task created while the window is open is picked up on the next search
|
|
239
|
+
# rather than the next poll, so the cost of the default is up to ten minutes
|
|
240
|
+
# before that PR's round 1 is queued -- against a 99% cut in the reads. Tune it
|
|
241
|
+
# down on a deployment where review tasks appear after their PR does and the
|
|
242
|
+
# wait is felt; tune it up on one where they are created up front.
|
|
243
|
+
#
|
|
244
|
+
# There is no "off" value: the floor is 1 (suppress one poll), because 0 would
|
|
245
|
+
# not disable a cache, it would write rows nothing ever reads.
|
|
246
|
+
DEFAULT_REVIEW_TASK_MISS_TTL_POLLS = 10
|
|
247
|
+
|
|
207
248
|
|
|
208
249
|
def default_state_path(workspace_root: Path) -> Path:
|
|
209
250
|
return Path(workspace_root) / ".revloop" / "state.db"
|
|
@@ -264,6 +305,28 @@ class Config:
|
|
|
264
305
|
# first poll that would have posted it.
|
|
265
306
|
checks_wait_seconds: int = DEFAULT_CHECKS_WAIT_SECONDS
|
|
266
307
|
|
|
308
|
+
# The bound on holding an owed round's SPAWN while the head's rollup is
|
|
309
|
+
# still running; see DEFAULT_CHECKS_SPAWN_WAIT_SECONDS. 0 is legal and means
|
|
310
|
+
# "never hold the spawn": the round is queued at once and told what the
|
|
311
|
+
# rollup was. The re-check cadence is `poll_interval` -- a held round is
|
|
312
|
+
# re-decided by the poll like every other owed round, so there is no second
|
|
313
|
+
# timer to configure.
|
|
314
|
+
checks_spawn_wait_seconds: int = DEFAULT_CHECKS_SPAWN_WAIT_SECONDS
|
|
315
|
+
|
|
316
|
+
# How many polls a PR with no review task is taken on trust before the
|
|
317
|
+
# corpus is searched again; see DEFAULT_REVIEW_TASK_MISS_TTL_POLLS. Floor 1
|
|
318
|
+
# -- there is no value that turns the negative cache off.
|
|
319
|
+
review_task_miss_ttl_polls: int = DEFAULT_REVIEW_TASK_MISS_TTL_POLLS
|
|
320
|
+
|
|
321
|
+
# Whether `alissa task list` may be narrowed to THIS actor's own rows
|
|
322
|
+
# (`--self`), dropping the sponsor's corpus. Off by default because on the
|
|
323
|
+
# live fleet a small minority of review tasks are NOT owned by the actor the
|
|
324
|
+
# daemon runs as, and a review task it cannot see is a round it cannot count
|
|
325
|
+
# -- see alissa.TASK_LIST_SELF_FLAG for the measurement. Turn it on only
|
|
326
|
+
# where every review task is created by this daemon's own sessions. Ignored
|
|
327
|
+
# when the installed CLI does not advertise the flag.
|
|
328
|
+
task_list_self_scope: bool = False
|
|
329
|
+
|
|
267
330
|
dry_run: bool = False
|
|
268
331
|
|
|
269
332
|
def __post_init__(self) -> None:
|
|
@@ -408,6 +471,30 @@ class Config:
|
|
|
408
471
|
if checks_wait < 0:
|
|
409
472
|
raise ValueError(f"checks_wait_seconds must be >= 0, got {checks_wait}")
|
|
410
473
|
|
|
474
|
+
spawn_wait = int(
|
|
475
|
+
raw.get("checks_spawn_wait_seconds", cls.checks_spawn_wait_seconds)
|
|
476
|
+
)
|
|
477
|
+
if spawn_wait < 0:
|
|
478
|
+
raise ValueError(
|
|
479
|
+
f"checks_spawn_wait_seconds must be >= 0, got {spawn_wait}"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
miss_ttl = int(
|
|
483
|
+
raw.get("review_task_miss_ttl_polls", cls.review_task_miss_ttl_polls)
|
|
484
|
+
)
|
|
485
|
+
if miss_ttl < 1:
|
|
486
|
+
# Refused rather than clamped: 0 reads as "turn the negative cache
|
|
487
|
+
# off", but the cache is what bounds the widest read this daemon
|
|
488
|
+
# makes, and a 0 would keep writing ledger rows nothing consults. An
|
|
489
|
+
# operator who means "search every poll" has said something the
|
|
490
|
+
# daemon cannot do, and should hear that at load rather than
|
|
491
|
+
# discover it in the I/O bill.
|
|
492
|
+
raise ValueError(
|
|
493
|
+
f"review_task_miss_ttl_polls must be >= 1 (it is a number of "
|
|
494
|
+
f"polls to suppress, and there is no value that disables the "
|
|
495
|
+
f"negative cache), got {miss_ttl}"
|
|
496
|
+
)
|
|
497
|
+
|
|
411
498
|
token_env = raw.get("reviewer_token_env")
|
|
412
499
|
if token_env is not None:
|
|
413
500
|
token_env = str(token_env).strip()
|
|
@@ -453,6 +540,9 @@ class Config:
|
|
|
453
540
|
reap_session_cap=session_cap,
|
|
454
541
|
max_concurrent_sessions=max_sessions,
|
|
455
542
|
checks_wait_seconds=checks_wait,
|
|
543
|
+
checks_spawn_wait_seconds=spawn_wait,
|
|
544
|
+
review_task_miss_ttl_polls=miss_ttl,
|
|
545
|
+
task_list_self_scope=bool(raw.get("task_list_self_scope", False)),
|
|
456
546
|
dry_run=bool(raw.get("dry_run", False)),
|
|
457
547
|
)
|
|
458
548
|
|