alissa-tools-github-devloop 0.8.21__tar.gz → 0.8.23__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_devloop-0.8.21/src/main/alissa_tools_github_devloop.egg-info → alissa_tools_github_devloop-0.8.23}/PKG-INFO +1 -1
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/loop.py +298 -2
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/state.py +71 -6
- alissa_tools_github_devloop-0.8.23/src/main/alissa/tools/github/devloop/version +1 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23/src/main/alissa_tools_github_devloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_devloop-0.8.21/src/main/alissa/tools/github/devloop/version +0 -1
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/LICENSE +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/MANIFEST.in +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/NOTICE +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/README.md +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/requirements.txt +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/setup.cfg +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/setup.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/__main__.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/alissa.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/alissa_client.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/bows.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/config.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/fleet_vitals.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/ghclient.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/janitor.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/loop_events.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/proc.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/version.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/webui/page.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/webui/sources.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa_tools_github_devloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa_tools_github_devloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa_tools_github_devloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.8.21 → alissa_tools_github_devloop-0.8.23}/src/main/alissa_tools_github_devloop.egg-info/top_level.txt +0 -0
|
@@ -810,6 +810,24 @@ RESUME_DIRECTIVE = (
|
|
|
810
810
|
"(best-effort — the daemon's reaper sweep collects stragglers)."
|
|
811
811
|
)
|
|
812
812
|
|
|
813
|
+
# The handover clause (issue #121), shared by FIX_DIRECTIVE and
|
|
814
|
+
# FIX_RESUME_DIRECTIVE. When a later review round lands on the head a fix
|
|
815
|
+
# session is still working (the 2026-09-15 studio#1243 shape: a phantom round
|
|
816
|
+
# 2 on round 1's head, minutes after round 1), the daemon no longer spawns a
|
|
817
|
+
# second session onto the same branch -- it posts FIX_HANDOVER_COMMENT on the
|
|
818
|
+
# PR and moves the live session's ledger row to the new round. The triage
|
|
819
|
+
# loop reads review THREADS, not the PR's issue comments, so the contract has
|
|
820
|
+
# to say it: look for a newer round before the terminal re-request.
|
|
821
|
+
_NEWER_ROUNDS = (
|
|
822
|
+
"Before you re-request review, check the PR for a NEWER review round: "
|
|
823
|
+
"if another `request_changes` round (or a daemon comment beginning "
|
|
824
|
+
"`Round N+1 arrived while the round-N fix session … is still working "
|
|
825
|
+
"on this head`) landed while you worked, the daemon has handed that "
|
|
826
|
+
"round to THIS session instead of spawning a second one on your branch "
|
|
827
|
+
"— triage and address its findings here as well, and re-request review "
|
|
828
|
+
"ONCE, after the newest round is answered. "
|
|
829
|
+
)
|
|
830
|
+
|
|
813
831
|
FIX_DIRECTIVE = (
|
|
814
832
|
"You are an IMPLEMENTER in a fix round, not a reviewer. PR {pr_url} "
|
|
815
833
|
"received a `request_changes` review (round {round}; this is fix attempt "
|
|
@@ -831,6 +849,7 @@ FIX_DIRECTIVE = (
|
|
|
831
849
|
"one reasoned `[triage:*]` reply on each finding's thread — post any that "
|
|
832
850
|
"are missing. Then fix the pursued findings on the same branch and VERIFY "
|
|
833
851
|
"locally (run the repo's test/style/type scripts). "
|
|
852
|
+
+ _NEWER_ROUNDS
|
|
834
853
|
+ _FIX_DELIVERY_NOTE
|
|
835
854
|
+ " "
|
|
836
855
|
+ _APPROVE_FOLLOWUP
|
|
@@ -887,6 +906,7 @@ FIX_RESUME_DIRECTIVE = (
|
|
|
887
906
|
"the ORIGINAL acceptance criteria the issue set, and VERIFY locally (run "
|
|
888
907
|
"the repo's test/style/type scripts). NEVER force-push: history stays "
|
|
889
908
|
"append-only, or the review threads lose their anchors. "
|
|
909
|
+
+ _NEWER_ROUNDS
|
|
890
910
|
+ _FIX_DELIVERY_NOTE
|
|
891
911
|
+ " "
|
|
892
912
|
+ _APPROVE_FOLLOWUP
|
|
@@ -1000,6 +1020,29 @@ FIX_STALLED_COMMENT = (
|
|
|
1000
1020
|
"close the PR to park the work."
|
|
1001
1021
|
)
|
|
1002
1022
|
|
|
1023
|
+
# The hidden marker that identifies a fix-round handover comment (issue #121)
|
|
1024
|
+
# -- a machine-findable anchor for the live session and for anyone auditing
|
|
1025
|
+
# the PR, distinct from ACTIVITY_MARKER so the two kinds of comment never
|
|
1026
|
+
# collide on one PR. Dedupe is NOT by marker but by the fix-handover:r<k>
|
|
1027
|
+
# ledger row (see fix_handover_kind): once per (PR, round).
|
|
1028
|
+
FIX_HANDOVER_MARKER = "<!-- alissa-devloop:fix-handover -->"
|
|
1029
|
+
|
|
1030
|
+
# The steering comment the fix edge posts INSTEAD of spawning when a later
|
|
1031
|
+
# review round lands on the head a fix session is still working. The first
|
|
1032
|
+
# sentence is the issue's mandated text verbatim; the trailer says what the
|
|
1033
|
+
# daemon did and what happens if the session dies anyway.
|
|
1034
|
+
FIX_HANDOVER_COMMENT = (
|
|
1035
|
+
FIX_HANDOVER_MARKER + "\n"
|
|
1036
|
+
"Round {new_round} arrived while the round-{round} fix session "
|
|
1037
|
+
"`{session}` is still working on this head — handing it over: address "
|
|
1038
|
+
"the round-{new_round} findings in the same session and re-request "
|
|
1039
|
+
"review once.\n\n"
|
|
1040
|
+
"_devloop: no second fix session was spawned for round {new_round} (head "
|
|
1041
|
+
"`{head}` is the one `{session}` is working). If that session dies "
|
|
1042
|
+
"without re-requesting review, the stale path re-enqueues round "
|
|
1043
|
+
"{new_round} as its next attempt._"
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1003
1046
|
MAINTAIN_CAP_COMMENT = (
|
|
1004
1047
|
"**Maintain loop cap-out** — {attempts} maintenance attempt(s) were "
|
|
1005
1048
|
"spawned on this PR without any of them completing (attempt cap: {cap}; "
|
|
@@ -1190,6 +1233,11 @@ ESCALATION_ASSIGNMENT = "assignment"
|
|
|
1190
1233
|
ESCALATION_STALLED = "stalled"
|
|
1191
1234
|
ESCALATION_FIX_CAP = "fix-cap"
|
|
1192
1235
|
ESCALATION_FIX_STALLED = "fix-stalled"
|
|
1236
|
+
# The round handover's once-per-(PR, round) row (issue #121, see
|
|
1237
|
+
# fix_handover_kind). A steering comment, not an operator page -- it lives in
|
|
1238
|
+
# the escalations table because that is the daemon's dedupe gate for
|
|
1239
|
+
# comments that must land exactly once and survive a degraded ledger.
|
|
1240
|
+
ESCALATION_FIX_HANDOVER = "fix-handover"
|
|
1193
1241
|
|
|
1194
1242
|
# The pre-PR liveness deferral's own kind (see live_stalled_kind). Distinct
|
|
1195
1243
|
# from ESCALATION_STALLED because the two stories are different and so are
|
|
@@ -1503,6 +1551,21 @@ def fix_stalled_kind(round_: int, attempt: int) -> str:
|
|
|
1503
1551
|
return f"{ESCALATION_FIX_STALLED}:r{round_}a{attempt}"
|
|
1504
1552
|
|
|
1505
1553
|
|
|
1554
|
+
def fix_handover_kind(round_: int) -> str:
|
|
1555
|
+
"""The ledger kind that dedupes ONE round's handover comment (issue #121).
|
|
1556
|
+
|
|
1557
|
+
Keyed per round (`fix-handover:r<k>`): round k+1 handed to a live session
|
|
1558
|
+
must not silence a round k+2 handed to the same session later, and the
|
|
1559
|
+
`fix-` prefix keeps the row from colliding with issue-edge kinds when an
|
|
1560
|
+
issue and a PR share a number. Delivery contract as fix_stalled_kind's:
|
|
1561
|
+
the row lands only AFTER the comment posts, so a transient failure
|
|
1562
|
+
retries next poll and the steering lands exactly once per round. The
|
|
1563
|
+
ledger round bump rides BEHIND the comment for the same reason -- a
|
|
1564
|
+
bumped row would close this edge's fresh-in-flight gate before the
|
|
1565
|
+
comment ever landed."""
|
|
1566
|
+
return f"{ESCALATION_FIX_HANDOVER}:r{round_}"
|
|
1567
|
+
|
|
1568
|
+
|
|
1506
1569
|
def stalled_kind(attempt: int) -> str:
|
|
1507
1570
|
"""The ledger kind for the stalled ping of ONE deferral episode.
|
|
1508
1571
|
|
|
@@ -3947,15 +4010,35 @@ class DevWatcher:
|
|
|
3947
4010
|
age = self.state.fix_spawn_age(pr.full_name, number, round_)
|
|
3948
4011
|
|
|
3949
4012
|
if age is not None and age < self.config.stale_minutes * 60:
|
|
4013
|
+
# The row's own session name, not one re-derived from the
|
|
4014
|
+
# attempt: after a handover (below) the row answering round k+1
|
|
4015
|
+
# still carries its `-r<k>-` name, and the console reads this.
|
|
3950
4016
|
return Decision(
|
|
3951
4017
|
Action.IN_FLIGHT,
|
|
3952
4018
|
f"fix attempt {attempts} for round {round_} enqueued "
|
|
3953
4019
|
f"{int(age)}s ago",
|
|
3954
4020
|
attempts,
|
|
3955
|
-
session=
|
|
4021
|
+
session=self.state.newest_fix_spawn_session(
|
|
4022
|
+
pr.full_name, number, round_
|
|
4023
|
+
) or fix_session_name(pr, round_, attempts),
|
|
3956
4024
|
round=round_,
|
|
3957
4025
|
)
|
|
3958
4026
|
|
|
4027
|
+
# A round nothing was spawned for yet -- but the PREVIOUS round's fix
|
|
4028
|
+
# session may still be alive on this very head (issue #121, the
|
|
4029
|
+
# studio#1243 shape: the reviewer daemon posted a phantom round 2 on
|
|
4030
|
+
# round 1's head four minutes after round 1, and this edge spawned a
|
|
4031
|
+
# second session onto the branch the first was mid-work on). A live
|
|
4032
|
+
# session on an unchanged head gets the new round handed to it with
|
|
4033
|
+
# one steering comment; the spawn below is for a head that moved or a
|
|
4034
|
+
# session that is gone. Above the external-claim check because it
|
|
4035
|
+
# spends nothing (ledger + the pass's roster) and a live local session
|
|
4036
|
+
# is the strongest evidence this edge can hold.
|
|
4037
|
+
if age is None:
|
|
4038
|
+
handed = self._hand_over_round(pr, round_, reviews, roster)
|
|
4039
|
+
if handed is not None:
|
|
4040
|
+
return handed
|
|
4041
|
+
|
|
3959
4042
|
# The cross-instance claim check, the issue edge's placement
|
|
3960
4043
|
# transposed: past the fresh-in-flight return, the local ledger no
|
|
3961
4044
|
# longer vouches for this round, and a peer instance's fresh spawn
|
|
@@ -4038,7 +4121,8 @@ class DevWatcher:
|
|
|
4038
4121
|
f"session may be there, already triaging (its CR8 "
|
|
4039
4122
|
f"replies are the footprint); not respawning",
|
|
4040
4123
|
attempts,
|
|
4041
|
-
session=
|
|
4124
|
+
session=row_session
|
|
4125
|
+
or fix_session_name(pr, round_, attempts),
|
|
4042
4126
|
round=round_,
|
|
4043
4127
|
defer_token=loop_events_mod.DEFER_IN_FLIGHT,
|
|
4044
4128
|
)
|
|
@@ -5554,6 +5638,215 @@ class DevWatcher:
|
|
|
5554
5638
|
pr.full_name, pr.number, fix_stalled_kind(round_, attempts)
|
|
5555
5639
|
)
|
|
5556
5640
|
|
|
5641
|
+
def _hand_over_round(
|
|
5642
|
+
self,
|
|
5643
|
+
pr: PullRequest,
|
|
5644
|
+
round_: int,
|
|
5645
|
+
reviews: list[Review],
|
|
5646
|
+
roster: "SessionLiveness | None",
|
|
5647
|
+
) -> "Decision | None":
|
|
5648
|
+
"""Round `round_` just landed and nothing was spawned for it. Is the
|
|
5649
|
+
previous round's fix session still ALIVE on this head? Then the round
|
|
5650
|
+
is ITS to answer (issue #121): post one steering comment, move the
|
|
5651
|
+
session's ledger row to this round, and spawn nothing. None means
|
|
5652
|
+
"no handover -- spawn as always".
|
|
5653
|
+
|
|
5654
|
+
The gates, cheapest first, and every one of them is a reason to keep
|
|
5655
|
+
the legacy spawn:
|
|
5656
|
+
|
|
5657
|
+
* no roster, or a listing that failed -- INDETERMINATE, and the issue
|
|
5658
|
+
keeps today's behaviour for it (a spawn); the fail-safe direction
|
|
5659
|
+
is inverted here on purpose: the wedge and resume gates defer
|
|
5660
|
+
because their remedy is a MUTATION on a possibly-live branch, while
|
|
5661
|
+
this gate's remedy is a comment, so ambiguity must not turn a real
|
|
5662
|
+
new round into silence;
|
|
5663
|
+
* the ledger holds no earlier round for this PR, or its newest round
|
|
5664
|
+
is not below this one (a review deleted outright, or a ledger
|
|
5665
|
+
written by a newer daemon -- NOT a dismissal: SUBMITTED_STATES
|
|
5666
|
+
keeps DISMISSED records, so dismissing never shrinks the count);
|
|
5667
|
+
* the newest session of that earlier round is not ALIVE -- DEAD is
|
|
5668
|
+
the ordinary "session finished, next round" flow, and it spawns;
|
|
5669
|
+
* the head is not that session's (see _head_is_sessions) -- the
|
|
5670
|
+
round is on work someone else pushed, and it spawns.
|
|
5671
|
+
|
|
5672
|
+
The comment lands BEFORE the ledger moves, and the move is skipped
|
|
5673
|
+
when the comment fails: a moved row closes the fresh-in-flight gate
|
|
5674
|
+
above, so a comment that failed once would never be retried. With the
|
|
5675
|
+
fix-handover:r<k> row down, the retry re-enters here with the comment
|
|
5676
|
+
already deduped and just moves the row -- the same retry covers a
|
|
5677
|
+
move that found no row to move (logged as an error: the ledger is
|
|
5678
|
+
then not what the daemon just said it was). Dry-run comments nothing
|
|
5679
|
+
and moves nothing. Both decisions ride Action.IN_FLIGHT with
|
|
5680
|
+
DEFER_IN_FLIGHT: a handover is a hold behind own work already on the
|
|
5681
|
+
lane, and the console must be able to tell it from a spawn.
|
|
5682
|
+
|
|
5683
|
+
What the live session does with it: FIX_DIRECTIVE's _NEWER_ROUNDS
|
|
5684
|
+
clause tells it to look for a newer round before its terminal
|
|
5685
|
+
re-request. If it dies without one, the moved row is what the stale
|
|
5686
|
+
path ages: `stale_minutes` after the handover, with the session gone,
|
|
5687
|
+
the edge re-enqueues attempt k+1 of THIS round (never of the old
|
|
5688
|
+
one), because the row now says this is the round in flight."""
|
|
5689
|
+
if roster is None or not roster.listed:
|
|
5690
|
+
return None
|
|
5691
|
+
previous = self.state.max_fix_round(pr.full_name, pr.number)
|
|
5692
|
+
if not previous or previous >= round_:
|
|
5693
|
+
return None
|
|
5694
|
+
row = self.state.newest_fix_spawn(pr.full_name, pr.number, previous)
|
|
5695
|
+
if row is None:
|
|
5696
|
+
return None
|
|
5697
|
+
session = str(row["session"])
|
|
5698
|
+
if roster.of(session) is not Liveness.ALIVE:
|
|
5699
|
+
return None
|
|
5700
|
+
if not self._head_is_sessions(pr, row, reviews):
|
|
5701
|
+
log.info(
|
|
5702
|
+
"%s round %d landed while the round-%d fix session %s is "
|
|
5703
|
+
"still listed, but head %s is not that session's — spawning "
|
|
5704
|
+
"as usual",
|
|
5705
|
+
pr.pr_slug,
|
|
5706
|
+
round_,
|
|
5707
|
+
previous,
|
|
5708
|
+
session,
|
|
5709
|
+
pr.head_sha[:8],
|
|
5710
|
+
)
|
|
5711
|
+
return None
|
|
5712
|
+
|
|
5713
|
+
kind = fix_handover_kind(round_)
|
|
5714
|
+
body = FIX_HANDOVER_COMMENT.format(
|
|
5715
|
+
new_round=round_,
|
|
5716
|
+
round=previous,
|
|
5717
|
+
session=session,
|
|
5718
|
+
head=pr.head_sha[:8],
|
|
5719
|
+
)
|
|
5720
|
+
if self.config.dry_run:
|
|
5721
|
+
log.info(
|
|
5722
|
+
"[dry-run] would hand round %d of %s to the live fix session "
|
|
5723
|
+
"%s and comment:\n%s",
|
|
5724
|
+
round_,
|
|
5725
|
+
pr.pr_slug,
|
|
5726
|
+
session,
|
|
5727
|
+
body,
|
|
5728
|
+
)
|
|
5729
|
+
elif not self.state.escalated(pr.full_name, pr.number, kind):
|
|
5730
|
+
log.warning(
|
|
5731
|
+
"%s round %d arrived while the round-%d fix session %s is "
|
|
5732
|
+
"ALIVE on the same head %s — handing the round to it (one "
|
|
5733
|
+
"steering comment), NOT spawning a second session",
|
|
5734
|
+
pr.pr_slug,
|
|
5735
|
+
round_,
|
|
5736
|
+
previous,
|
|
5737
|
+
session,
|
|
5738
|
+
pr.head_sha[:8],
|
|
5739
|
+
)
|
|
5740
|
+
try:
|
|
5741
|
+
self.github.comment(pr.owner, pr.repo, pr.number, body)
|
|
5742
|
+
except CommandError as exc:
|
|
5743
|
+
log.error(
|
|
5744
|
+
"could not post the round-%d handover comment on %s: %s "
|
|
5745
|
+
"— not moving the ledger row; the handover retries next "
|
|
5746
|
+
"poll",
|
|
5747
|
+
round_,
|
|
5748
|
+
pr.pr_slug,
|
|
5749
|
+
exc,
|
|
5750
|
+
)
|
|
5751
|
+
return Decision(
|
|
5752
|
+
Action.IN_FLIGHT,
|
|
5753
|
+
f"round {round_} is the live round-{previous} fix "
|
|
5754
|
+
f"session {session}'s to answer, but the steering comment "
|
|
5755
|
+
f"could not be posted — retrying next poll",
|
|
5756
|
+
int(row["attempt"]),
|
|
5757
|
+
session=session,
|
|
5758
|
+
round=round_,
|
|
5759
|
+
defer_token=loop_events_mod.DEFER_IN_FLIGHT,
|
|
5760
|
+
)
|
|
5761
|
+
self.state.record_escalation(pr.full_name, pr.number, kind)
|
|
5762
|
+
|
|
5763
|
+
if not self.config.dry_run:
|
|
5764
|
+
moved = self.state.hand_over_fix_spawn(
|
|
5765
|
+
repo_slug=pr.full_name,
|
|
5766
|
+
number=pr.number,
|
|
5767
|
+
session=session,
|
|
5768
|
+
round_=round_,
|
|
5769
|
+
head_sha=pr.head_sha,
|
|
5770
|
+
)
|
|
5771
|
+
if not moved:
|
|
5772
|
+
log.error(
|
|
5773
|
+
"%s round %d handover: no fix_spawns row carries session "
|
|
5774
|
+
"%s any more — the ledger was not moved (the row is "
|
|
5775
|
+
"still on round %d); retrying the move next poll",
|
|
5776
|
+
pr.pr_slug,
|
|
5777
|
+
round_,
|
|
5778
|
+
session,
|
|
5779
|
+
previous,
|
|
5780
|
+
)
|
|
5781
|
+
return Decision(
|
|
5782
|
+
Action.IN_FLIGHT,
|
|
5783
|
+
f"round {round_} is the live round-{previous} fix "
|
|
5784
|
+
f"session {session}'s to answer (steering comment "
|
|
5785
|
+
f"posted), but its ledger row could not be moved — "
|
|
5786
|
+
f"retrying next poll",
|
|
5787
|
+
int(row["attempt"]),
|
|
5788
|
+
session=session,
|
|
5789
|
+
round=round_,
|
|
5790
|
+
defer_token=loop_events_mod.DEFER_IN_FLIGHT,
|
|
5791
|
+
)
|
|
5792
|
+
# The moved row is attempt 1 of the new round (see
|
|
5793
|
+
# State.hand_over_fix_spawn); dry-run moved nothing and reports the
|
|
5794
|
+
# row as it stands.
|
|
5795
|
+
return Decision(
|
|
5796
|
+
Action.IN_FLIGHT,
|
|
5797
|
+
f"round {round_} handed to the live round-{previous} fix session "
|
|
5798
|
+
f"{session} on the same head {pr.head_sha[:8]} (steering comment "
|
|
5799
|
+
f"posted once; no second session)",
|
|
5800
|
+
int(row["attempt"]) if self.config.dry_run else 1,
|
|
5801
|
+
session=session,
|
|
5802
|
+
round=round_,
|
|
5803
|
+
defer_token=loop_events_mod.DEFER_IN_FLIGHT,
|
|
5804
|
+
)
|
|
5805
|
+
|
|
5806
|
+
def _head_is_sessions(
|
|
5807
|
+
self, pr: PullRequest, row: "sqlite3.Row", reviews: list[Review]
|
|
5808
|
+
) -> bool:
|
|
5809
|
+
"""Is the PR's current head the one the row's session is working --
|
|
5810
|
+
the head it was spawned against, or one it has pushed since?
|
|
5811
|
+
|
|
5812
|
+
Three tiers, cheapest first, and the first two cost nothing:
|
|
5813
|
+
|
|
5814
|
+
1. the ledger's spawn head (`head_sha`, recorded since issue #121;
|
|
5815
|
+
'' on older rows, which skip this tier);
|
|
5816
|
+
2. the PREVIOUS verdict's head: the round before this one was
|
|
5817
|
+
reviewed on `commit_id`, and a head still equal to it has not
|
|
5818
|
+
moved between the rounds -- the issue's "simply" test, and the
|
|
5819
|
+
#1243 shape exactly (round 2 on round 1's head);
|
|
5820
|
+
3. a head that POSTDATES the session's spawn: one branch, one live
|
|
5821
|
+
session, so a commit made after the session started is that
|
|
5822
|
+
session's own push (the directives commit and push within
|
|
5823
|
+
seconds). One GET, on this rare path only, and any failure to
|
|
5824
|
+
read the instant answers False -- the legacy spawn.
|
|
5825
|
+
|
|
5826
|
+
False means "the head moved" for the handover's purposes, which keeps
|
|
5827
|
+
today's behaviour (issue #121 §4)."""
|
|
5828
|
+
spawn_head = str(row["head_sha"] or "")
|
|
5829
|
+
if spawn_head and pr.head_sha == spawn_head:
|
|
5830
|
+
return True
|
|
5831
|
+
if len(reviews) >= 2:
|
|
5832
|
+
before = reviews[-2]
|
|
5833
|
+
if before.commit_id and before.commit_id == pr.head_sha:
|
|
5834
|
+
return True
|
|
5835
|
+
try:
|
|
5836
|
+
committed = self.github.commit_time(pr.owner, pr.repo, pr.head_sha)
|
|
5837
|
+
except CommandError as exc:
|
|
5838
|
+
log.warning(
|
|
5839
|
+
"could not read the commit instant of %s head %s (%s) — "
|
|
5840
|
+
"treating the head as moved",
|
|
5841
|
+
pr.pr_slug,
|
|
5842
|
+
pr.head_sha[:8],
|
|
5843
|
+
exc,
|
|
5844
|
+
)
|
|
5845
|
+
return False
|
|
5846
|
+
if committed is None:
|
|
5847
|
+
return False
|
|
5848
|
+
return committed >= int(row["spawned_at"])
|
|
5849
|
+
|
|
5557
5850
|
def _verdict(self, pr: PullRequest, newest: Review) -> "str | None":
|
|
5558
5851
|
"""The newest review's verdict, mirroring reviewloop's precedence:
|
|
5559
5852
|
the GitHub review state is authoritative where it can speak, and
|
|
@@ -5787,6 +6080,9 @@ class DevWatcher:
|
|
|
5787
6080
|
round_=round_,
|
|
5788
6081
|
attempt=attempt,
|
|
5789
6082
|
session=name,
|
|
6083
|
+
# The head this session takes the round against -- the
|
|
6084
|
+
# handover check's first tier (issue #121).
|
|
6085
|
+
head_sha=pr.head_sha,
|
|
5790
6086
|
)
|
|
5791
6087
|
|
|
5792
6088
|
resumed = (
|
|
@@ -8,7 +8,11 @@ can be retried, and to remember which escalations were already raised.
|
|
|
8
8
|
The review-response edge keeps its own additive table, `fix_spawns`, keyed
|
|
9
9
|
(repo_slug, number, round, attempt): `number` is the PULL REQUEST number (the
|
|
10
10
|
edge has no issue), `round` is the substantive-review count the fix session
|
|
11
|
-
|
|
11
|
+
is CURRENTLY ANSWERING (the round it was spawned for, until a handover -- see
|
|
12
|
+
`hand_over_fix_spawn` -- moves the row to a later round without spawning),
|
|
13
|
+
`attempt` retries a presumed-dead fix session within one round, and
|
|
14
|
+
`head_sha` is the PR head the row's session took that round against (the
|
|
15
|
+
spawn head, refreshed on handover; '' on rows written before the column).
|
|
12
16
|
A separate table rather than new columns on `spawns` because the two edges
|
|
13
17
|
age and dedupe on different keys (issue vs PR+round), and an additive table
|
|
14
18
|
leaves every existing ledger untouched. Fix-round cap-outs reuse the
|
|
@@ -135,6 +139,7 @@ CREATE TABLE IF NOT EXISTS fix_spawns (
|
|
|
135
139
|
attempt INTEGER NOT NULL,
|
|
136
140
|
session TEXT NOT NULL,
|
|
137
141
|
spawned_at INTEGER NOT NULL,
|
|
142
|
+
head_sha TEXT NOT NULL DEFAULT '',
|
|
138
143
|
PRIMARY KEY (repo_slug, number, round, attempt)
|
|
139
144
|
);
|
|
140
145
|
|
|
@@ -193,6 +198,10 @@ CREATE TABLE IF NOT EXISTS poll_snapshots (
|
|
|
193
198
|
_COLUMN_MIGRATIONS = (
|
|
194
199
|
("poll_snapshots", "deferred", "INTEGER NOT NULL DEFAULT 0"),
|
|
195
200
|
("poll_snapshots", "foreign_reaped", "INTEGER NOT NULL DEFAULT 0"),
|
|
201
|
+
# The head a fix session took its round against (issue #121): rows
|
|
202
|
+
# written before the column read '' and the handover check falls back to
|
|
203
|
+
# the previous verdict's head for them.
|
|
204
|
+
("fix_spawns", "head_sha", "TEXT NOT NULL DEFAULT ''"),
|
|
196
205
|
)
|
|
197
206
|
|
|
198
207
|
|
|
@@ -558,18 +567,74 @@ class State:
|
|
|
558
567
|
return row is not None
|
|
559
568
|
|
|
560
569
|
def record_fix_spawn(
|
|
561
|
-
self, *, repo_slug: str, number: int, round_: int, attempt: int, session: str
|
|
570
|
+
self, *, repo_slug: str, number: int, round_: int, attempt: int, session: str,
|
|
571
|
+
head_sha: str = "",
|
|
562
572
|
) -> None:
|
|
563
573
|
"""Record a fix-session spawn for one review round. `repo_slug` is
|
|
564
|
-
`owner/repo`; `number` is the PR number
|
|
574
|
+
`owner/repo`; `number` is the PR number; `head_sha` is the PR head the
|
|
575
|
+
session was spawned against (the handover check's first tier -- see
|
|
576
|
+
loop.DevWatcher._hand_over_round)."""
|
|
565
577
|
self._db.execute(
|
|
566
578
|
"INSERT OR REPLACE INTO fix_spawns "
|
|
567
|
-
"(repo_slug, number, round, attempt, session, spawned_at) "
|
|
568
|
-
"VALUES (
|
|
569
|
-
(repo_slug, number, round_, attempt, session, int(time.time())
|
|
579
|
+
"(repo_slug, number, round, attempt, session, spawned_at, head_sha) "
|
|
580
|
+
"VALUES (?,?,?,?,?,?,?)",
|
|
581
|
+
(repo_slug, number, round_, attempt, session, int(time.time()),
|
|
582
|
+
head_sha or ""),
|
|
570
583
|
)
|
|
571
584
|
self._db.commit()
|
|
572
585
|
|
|
586
|
+
def newest_fix_spawn(
|
|
587
|
+
self, repo_slug: str, number: int, round_: int
|
|
588
|
+
) -> "sqlite3.Row | None":
|
|
589
|
+
"""The newest fix-spawn row of THIS PR round (session, attempt,
|
|
590
|
+
spawned_at, head_sha), or None when the round was never spawned --
|
|
591
|
+
newest_fix_spawn_session with the rest of the row, for the handover
|
|
592
|
+
check that needs the head and the spawn instant too."""
|
|
593
|
+
return self._db.execute(
|
|
594
|
+
"SELECT session, attempt, spawned_at, head_sha FROM fix_spawns "
|
|
595
|
+
"WHERE repo_slug=? AND number=? AND round=? "
|
|
596
|
+
"ORDER BY spawned_at DESC, attempt DESC LIMIT 1",
|
|
597
|
+
(repo_slug, number, round_),
|
|
598
|
+
).fetchone()
|
|
599
|
+
|
|
600
|
+
def hand_over_fix_spawn(
|
|
601
|
+
self, *, repo_slug: str, number: int, session: str, round_: int,
|
|
602
|
+
head_sha: str,
|
|
603
|
+
) -> bool:
|
|
604
|
+
"""Move a live fix session's row to the round it is now answering
|
|
605
|
+
(issue #121): a later review round landed on the head that session is
|
|
606
|
+
still working, so the daemon handed the round to it instead of
|
|
607
|
+
spawning a second session. The row's `round` becomes `round_`, its
|
|
608
|
+
`head_sha` the head handed over, and `spawned_at` NOW -- the staleness
|
|
609
|
+
clock restarts at the handover, because the session took the new
|
|
610
|
+
round at that instant and the stale path must not fire on it one poll
|
|
611
|
+
later just because the session was already old.
|
|
612
|
+
|
|
613
|
+
ONE row moves, not a copy: the console's inbox then shows one session
|
|
614
|
+
answering one round, `max_fix_round` reads the handed-over round (so
|
|
615
|
+
the reaper never counts the session as superseded by the round it is
|
|
616
|
+
answering), and the round's attempt budget starts at this row -- so
|
|
617
|
+
the row is RENUMBERED to attempt 1 (PR #122 review round 1). The
|
|
618
|
+
attempt ledger's contract is that a round's attempts are contiguous
|
|
619
|
+
from 1 (`fix_attempt_count` is a COUNT, and `attempts + 1` must be a
|
|
620
|
+
free slot for the next spawn); a resumed session's row is attempt 2,
|
|
621
|
+
and moving it as attempt 2 would leave the new round with one row
|
|
622
|
+
that COUNTs as one attempt -- the stale respawn would then
|
|
623
|
+
`INSERT OR REPLACE` over it as attempt 2 every stale window, and the
|
|
624
|
+
attempt cap would never fire. The target key (repo_slug, number,
|
|
625
|
+
round_, 1) is free by construction: the caller only hands over a
|
|
626
|
+
round nothing was spawned for. Strict like record_fix_spawn: a row
|
|
627
|
+
the daemon believes it moved but did not would spawn over the live
|
|
628
|
+
session next poll. Returns False when no row carries `session` on
|
|
629
|
+
this PR."""
|
|
630
|
+
cursor = self._db.execute(
|
|
631
|
+
"UPDATE fix_spawns SET round=?, attempt=1, head_sha=?, spawned_at=? "
|
|
632
|
+
"WHERE repo_slug=? AND number=? AND session=?",
|
|
633
|
+
(round_, head_sha or "", int(time.time()), repo_slug, number, session),
|
|
634
|
+
)
|
|
635
|
+
self._db.commit()
|
|
636
|
+
return cursor.rowcount > 0
|
|
637
|
+
|
|
573
638
|
def fix_attempt_count(self, repo_slug: str, number: int, round_: int) -> int:
|
|
574
639
|
"""Fix attempts already spawned for THIS round of this PR. Keyed per
|
|
575
640
|
round on purpose: a new review round starts a fresh attempt budget."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.8.23
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.8.21
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|