alissa-tools-github-orcloop 0.3.10__tar.gz → 0.3.11__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_orcloop-0.3.10/src/main/alissa_tools_github_orcloop.egg-info → alissa_tools_github_orcloop-0.3.11}/PKG-INFO +1 -1
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/loop.py +293 -28
- alissa_tools_github_orcloop-0.3.11/src/main/alissa/tools/github/orcloop/version +1 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11/src/main/alissa_tools_github_orcloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_orcloop-0.3.10/src/main/alissa/tools/github/orcloop/version +0 -1
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/MANIFEST.in +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/README.md +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/requirements.txt +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/setup.cfg +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/setup.py +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/__init__.py +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/__main__.py +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/alissa_client.py +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/config.py +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/ghclient.py +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/markers.py +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/proc.py +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/state.py +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa/tools/github/orcloop/version.py +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa_tools_github_orcloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa_tools_github_orcloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa_tools_github_orcloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_orcloop-0.3.10 → alissa_tools_github_orcloop-0.3.11}/src/main/alissa_tools_github_orcloop.egg-info/top_level.txt +0 -0
|
@@ -428,6 +428,27 @@ ESCALATION_CLOSELOOP_FAILED = "closeloop-failed"
|
|
|
428
428
|
ESCALATION_ALREADY_MERGED = "already-merged"
|
|
429
429
|
ESCALATION_DEDUPE_UNREADABLE = "dedupe-unreadable"
|
|
430
430
|
|
|
431
|
+
# The close edge could not write its evidence AT ALL -- not the enriched
|
|
432
|
+
# delivery record, not the bare confirmation line -- for longer than the
|
|
433
|
+
# close-loop retry floor, so the hand-off completed WITHOUT evidence (O17,
|
|
434
|
+
# issue #44). Terminal by construction: it is raised on the pass that also
|
|
435
|
+
# records `CLOSELOOP_EVIDENCE_DONE`, so there is never a second attempt to page
|
|
436
|
+
# about. That is the whole point -- see `_close_evidence_unwritable`.
|
|
437
|
+
ESCALATION_CLOSE_EVIDENCE_FAILED = "close-evidence-failed"
|
|
438
|
+
|
|
439
|
+
# NOT an operator page: the timestamp of the FIRST permanent evidence rejection
|
|
440
|
+
# on this task, so the floor above it measures how long THE FAILURE has lasted.
|
|
441
|
+
# Same shape and same reasoning as `DEDUPE_UNREADABLE_SINCE` (including the
|
|
442
|
+
# load-bearing `marker:` namespace, which keeps it out of every prefix read of
|
|
443
|
+
# the page kind above), with one difference: this clock is never CLEARED. The
|
|
444
|
+
# dedupe/context clocks time a condition that recurs on every poll of a task
|
|
445
|
+
# still waiting to be released, so a blip that clears must not leave a stale
|
|
446
|
+
# start time behind. The close-evidence step settles exactly once per task
|
|
447
|
+
# (`CLOSELOOP_EVIDENCE_DONE` closes it for good, on every path), so this clock
|
|
448
|
+
# can never be re-read after it settles -- leaving the row is free, and it is
|
|
449
|
+
# the audit record of when the failure began.
|
|
450
|
+
CLOSE_EVIDENCE_FAILED_SINCE = "marker:close-evidence-failed"
|
|
451
|
+
|
|
431
452
|
# NOT an operator page: the timestamp of the FIRST dedupe check that could not
|
|
432
453
|
# answer for this task, so the floor above it measures how long the CONDITION
|
|
433
454
|
# has lasted rather than how old the task is. Task age is a fair proxy for a
|
|
@@ -471,13 +492,22 @@ MAX_CLOSED_TRAILER_CHECKS = 3
|
|
|
471
492
|
# O2 design) purely for its per-(task, kind) dedupe; no comment is ever posted.
|
|
472
493
|
CLOSELOOP_DONE = "closeloop-done"
|
|
473
494
|
|
|
474
|
-
# NOT an operator page: an idempotency marker recording that the close-loop
|
|
475
|
-
#
|
|
476
|
-
#
|
|
477
|
-
#
|
|
478
|
-
#
|
|
479
|
-
#
|
|
480
|
-
#
|
|
495
|
+
# NOT an operator page: an idempotency marker recording that the close-loop's
|
|
496
|
+
# evidence step is SETTLED on the origin task. Gating the evidence write on THIS
|
|
497
|
+
# (rather than on the task still being `committed`) survives a partial-failure
|
|
498
|
+
# retry that died AFTER the `committed → in_progress` step but BEFORE the
|
|
499
|
+
# evidence write -- otherwise the retry re-enters at `in_progress`, reads
|
|
500
|
+
# `first_entry` as False, and skips the evidence permanently.
|
|
501
|
+
#
|
|
502
|
+
# "Settled", not "attached", since O17 (issue #44): the marker is recorded when
|
|
503
|
+
# the enriched record lands, when the DEGRADED bare confirmation lands in its
|
|
504
|
+
# place, and when both have been rejected permanently past the floor and the
|
|
505
|
+
# hand-off gave up on evidence entirely. All three are ends of the same step --
|
|
506
|
+
# there is nothing left for a later poll to attempt -- and one marker for the
|
|
507
|
+
# step is what makes "no double write, no re-page" true on every path. It is
|
|
508
|
+
# never recorded while a retry is still owed: a transient failure, or a
|
|
509
|
+
# permanent one inside the floor, raises instead, leaving the marker absent so
|
|
510
|
+
# the next poll re-enters exactly as before.
|
|
481
511
|
CLOSELOOP_EVIDENCE_DONE = "closeloop-evidence-done"
|
|
482
512
|
|
|
483
513
|
# NOT an operator page: an idempotency marker recording that the daemon closed a
|
|
@@ -634,6 +664,54 @@ CLOSELOOP_FAILED_ISSUE_COMMENT = (
|
|
|
634
664
|
"Retrying; operator help may be needed."
|
|
635
665
|
)
|
|
636
666
|
|
|
667
|
+
# The TERMINAL page of the close edge (O17, issue #44). Everything a page has to
|
|
668
|
+
# say when the daemon stops: what it could not do, what it is doing anyway, that
|
|
669
|
+
# it will not try that again, and what is left for a human. The last part
|
|
670
|
+
# matters most -- the operator's soft validator is what reads evidence, and this
|
|
671
|
+
# task now reaches it without any, so the comment has to say where the record
|
|
672
|
+
# went.
|
|
673
|
+
#
|
|
674
|
+
# EVERY CLAIM IS SCOPED TO WHAT IS TRUE AT POST TIME, and that is a constraint
|
|
675
|
+
# the wording had to be rewritten for (review round 1). The page is posted from
|
|
676
|
+
# `_close_evidence_unwritable`, which runs BEFORE `_mutate_close`'s
|
|
677
|
+
# `set_status(pending_validation)` -- it has to, because the page landing is
|
|
678
|
+
# what licenses the walk to continue. So it must not report the hand-off in the
|
|
679
|
+
# past tense (a hand-off write can fail, transiently or for good, and then the
|
|
680
|
+
# task sits at `in_progress` under a comment claiming otherwise), and it must
|
|
681
|
+
# not promise silence on behalf of the whole loop (a failed hand-off pages
|
|
682
|
+
# `closeloop-failed:a<j>` on the very next line, retracting the retraction).
|
|
683
|
+
#
|
|
684
|
+
# What IS terminal here is exactly the step this page is about: the evidence
|
|
685
|
+
# will never be attempted again -- `CLOSELOOP_EVIDENCE_DONE` is recorded on this
|
|
686
|
+
# same pass, on every path out of it. So the terminality is stated about the
|
|
687
|
+
# evidence, and the close-loop's own retries are named as the separate condition
|
|
688
|
+
# they are. Same standard `_autoclose_heal`'s comment holds for a mere WARNING
|
|
689
|
+
# ("AFTER the close, never before it"), applied to something that outranks it.
|
|
690
|
+
CLOSE_EVIDENCE_FAILED_TASK_COMMENT = (
|
|
691
|
+
"**Orchestrator: close evidence could not be attached** — the released "
|
|
692
|
+
"issue {slug} merged, but attaching the merged-pull-request evidence to "
|
|
693
|
+
"this task was rejected permanently (not a rate limit or an outage) for "
|
|
694
|
+
"over {floor_hours}h, with the full delivery record AND a bare merge "
|
|
695
|
+
"confirmation both refused: {error}. **The orchestrator has given up on "
|
|
696
|
+
"the evidence — it will not be attempted again — and is completing the "
|
|
697
|
+
"close hand-off without it now**, moving this task to `pending_validation`: "
|
|
698
|
+
"the merge is a fact, and holding it at `in_progress` over a missing "
|
|
699
|
+
"write-up is strictly worse than thin evidence. The delivery record is on "
|
|
700
|
+
"the merged pull request {pr_url}; attach it by hand if validating this "
|
|
701
|
+
"task needs it. Usual causes: the orchestrator's token lost its role on "
|
|
702
|
+
"this task, or the evidence endpoint is rejecting the request outright. "
|
|
703
|
+
"(If that hand-off write fails too it is a separate condition and pages "
|
|
704
|
+
"separately as `close-loop failed`; those retries are about the status "
|
|
705
|
+
"transition, never about this evidence.)"
|
|
706
|
+
)
|
|
707
|
+
CLOSE_EVIDENCE_FAILED_ISSUE_COMMENT = (
|
|
708
|
+
"**Orchestrator: close evidence could not be attached** — this issue's "
|
|
709
|
+
"merge is being handed off to its origin Alissa task WITHOUT the "
|
|
710
|
+
"merged-pull-request evidence: writing it was refused permanently and the "
|
|
711
|
+
"orchestrator has stopped trying. Nothing is needed here — the record is "
|
|
712
|
+
"on the merged pull request, and any remaining problem is on the task."
|
|
713
|
+
)
|
|
714
|
+
|
|
637
715
|
# Posted on the issue the daemon closes on GitHub's behalf (O16, issue #40). It
|
|
638
716
|
# says what was true, what the daemon did, and what it did NOT do -- a reader
|
|
639
717
|
# arriving at a mechanically-closed issue must be able to tell this apart from
|
|
@@ -733,14 +811,19 @@ DELIVERY_NOTES_HEADING = "## Delivery notes"
|
|
|
733
811
|
# compatibility commitment.
|
|
734
812
|
#
|
|
735
813
|
# THE HEADROOM IS MEASURED, not assumed, because the size of this write is the
|
|
736
|
-
# one way this feature could newly BLOCK a close
|
|
737
|
-
#
|
|
738
|
-
#
|
|
739
|
-
#
|
|
740
|
-
#
|
|
741
|
-
#
|
|
742
|
-
#
|
|
743
|
-
#
|
|
814
|
+
# one way this feature could newly BLOCK a close -- and O17 (issue #44) did NOT
|
|
815
|
+
# retire that argument, it narrowed it. A permanent rejection now degrades to
|
|
816
|
+
# the bare confirmation line and, past the floor, gives up on evidence rather
|
|
817
|
+
# than stranding the task (`_attach_close_evidence`), but that ladder is driven
|
|
818
|
+
# by the client's status taxonomy: `AlissaTransient` is 408/429/5xx, and the
|
|
819
|
+
# oversize failure below answers **HTTP 500**. An over-cap body is therefore
|
|
820
|
+
# deterministic AND classified transient -- the one shape that retries forever
|
|
821
|
+
# with the identical payload and never reaches the degrade. Nothing here is
|
|
822
|
+
# wrong: statuses are the contract and codes are advisory (see `_classify`), so
|
|
823
|
+
# the guard against that shape is THIS CAP, not the ladder.
|
|
824
|
+
# `AddEvidenceRequest` declares `markdownContent` with a `minLength` and NO
|
|
825
|
+
# `maxLength`, so the ceiling was probed directly against the live endpoint
|
|
826
|
+
# (2026-07-28, disposable task, since cancelled):
|
|
744
827
|
#
|
|
745
828
|
# 32,300 -> 201 131,072 -> 201 524,288 -> 201
|
|
746
829
|
# 65,536 -> 201 262,144 -> 201 1,048,576 -> 500
|
|
@@ -956,20 +1039,31 @@ class Orchestrator:
|
|
|
956
1039
|
task_comment: str,
|
|
957
1040
|
issue: Issue | None = None,
|
|
958
1041
|
issue_comment: str | None = None,
|
|
959
|
-
) ->
|
|
960
|
-
"""Raise an operator escalation once per (task, kind).
|
|
1042
|
+
) -> bool:
|
|
1043
|
+
"""Raise an operator escalation once per (task, kind). Returns whether
|
|
1044
|
+
the page HAS BEEN DELIVERED -- True when a dedupe row exists for it
|
|
1045
|
+
after this call, which (see below) is exactly "the primary comment
|
|
1046
|
+
posted, now or on an earlier pass".
|
|
961
1047
|
|
|
962
1048
|
Primary channel is a comment on the Alissa task; the GitHub issue gets
|
|
963
1049
|
a best-effort comment too when one exists. All dry-run gated. The
|
|
964
1050
|
dedupe row is recorded only AFTER the primary comment posts, so a
|
|
965
1051
|
transient primary failure retries next poll (exactly-once primary
|
|
966
|
-
delivery) rather than latching a page that never landed.
|
|
1052
|
+
delivery) rather than latching a page that never landed.
|
|
1053
|
+
|
|
1054
|
+
That ordering is what makes the return value meaningful rather than
|
|
1055
|
+
decorative: a caller whose next step is IRREVERSIBLE if nobody was told
|
|
1056
|
+
(`_close_evidence_unwritable` settles the evidence step, which stops
|
|
1057
|
+
every future attempt AND every future page) can gate on the receipt
|
|
1058
|
+
instead of re-reading the ledger and hoping the two stay in step. Every
|
|
1059
|
+
pre-existing caller ignores it, which is unchanged behaviour: a page
|
|
1060
|
+
that could not be posted was already retried next poll."""
|
|
967
1061
|
if self.state.escalated(task_number, kind):
|
|
968
|
-
return
|
|
1062
|
+
return True
|
|
969
1063
|
ref = f"TASK-{task_number}"
|
|
970
1064
|
if self.config.dry_run:
|
|
971
1065
|
log.info("[dry-run] would escalate %s on %s:\n%s", kind, ref, task_comment)
|
|
972
|
-
return
|
|
1066
|
+
return False
|
|
973
1067
|
|
|
974
1068
|
try:
|
|
975
1069
|
self.alissa.add_comment(ref, task_comment)
|
|
@@ -978,7 +1072,7 @@ class Orchestrator:
|
|
|
978
1072
|
"could not post %s escalation on %s: %s — not recording; "
|
|
979
1073
|
"retries next poll", kind, ref, exc,
|
|
980
1074
|
)
|
|
981
|
-
return
|
|
1075
|
+
return False
|
|
982
1076
|
|
|
983
1077
|
if issue is not None and issue_comment is not None:
|
|
984
1078
|
try:
|
|
@@ -989,6 +1083,7 @@ class Orchestrator:
|
|
|
989
1083
|
kind, issue.issue_slug, exc,
|
|
990
1084
|
)
|
|
991
1085
|
self.state.record_escalation(task_number, kind)
|
|
1086
|
+
return True
|
|
992
1087
|
|
|
993
1088
|
# ======================================================================
|
|
994
1089
|
# RELEASE EDGE
|
|
@@ -3003,7 +3098,7 @@ class Orchestrator:
|
|
|
3003
3098
|
))
|
|
3004
3099
|
|
|
3005
3100
|
try:
|
|
3006
|
-
self._mutate_close(task_number, ref, task, slug, pr)
|
|
3101
|
+
self._mutate_close(task_number, ref, task, slug, issue, pr)
|
|
3007
3102
|
except AlissaError as exc:
|
|
3008
3103
|
return self._closeloop_failed(task_number, slug, issue, exc, prior)
|
|
3009
3104
|
|
|
@@ -3015,7 +3110,13 @@ class Orchestrator:
|
|
|
3015
3110
|
))
|
|
3016
3111
|
|
|
3017
3112
|
def _mutate_close(
|
|
3018
|
-
self,
|
|
3113
|
+
self,
|
|
3114
|
+
task_number: int,
|
|
3115
|
+
ref: str,
|
|
3116
|
+
task: Task,
|
|
3117
|
+
slug: str,
|
|
3118
|
+
issue: Issue,
|
|
3119
|
+
pr: PullRequest,
|
|
3019
3120
|
) -> None:
|
|
3020
3121
|
"""The close-loop mutation, ordered for idempotency on partial-failure
|
|
3021
3122
|
retry. Two independent idempotency gates, each keyed on what it actually
|
|
@@ -3025,8 +3126,8 @@ class Orchestrator:
|
|
|
3025
3126
|
`committed`, so a retry that re-enters at `in_progress` never re-issues
|
|
3026
3127
|
the (illegal) `in_progress → in_progress` transition;
|
|
3027
3128
|
* the merged-PR evidence write is gated on the `CLOSELOOP_EVIDENCE_DONE`
|
|
3028
|
-
marker, recorded only
|
|
3029
|
-
the `in_progress` step and the evidence write re-enters at
|
|
3129
|
+
marker, recorded only once that step is SETTLED. A retry that died
|
|
3130
|
+
between the `in_progress` step and the evidence write re-enters at
|
|
3030
3131
|
`in_progress` with the marker absent, so it still attaches the evidence
|
|
3031
3132
|
-- where a shared `first_entry` flag would have skipped it forever
|
|
3032
3133
|
(the task would reach `pending_validation` with no merged-PR audit
|
|
@@ -3039,6 +3140,11 @@ class Orchestrator:
|
|
|
3039
3140
|
failure mode to the walk: a PR whose write-up cannot be read still
|
|
3040
3141
|
reaches `pending_validation`, carrying an explicit line saying so.
|
|
3041
3142
|
|
|
3143
|
+
And since O17 (issue #44), a rejected write cannot stop the walk either:
|
|
3144
|
+
`_attach_close_evidence` owns the whole ladder (degrade to the bare
|
|
3145
|
+
line, then give up past the floor), so THIS method's last line is
|
|
3146
|
+
reached on every path that is not still owed a retry.
|
|
3147
|
+
|
|
3042
3148
|
Criteria satisfaction is naturally idempotent (already-satisfied ones are
|
|
3043
3149
|
skipped), so it needs no gate."""
|
|
3044
3150
|
if task.status == TASK_TODO_STATE:
|
|
@@ -3046,14 +3152,173 @@ class Orchestrator:
|
|
|
3046
3152
|
for criterion in task.criteria:
|
|
3047
3153
|
if not criterion.satisfied:
|
|
3048
3154
|
self.alissa.satisfy_criterion(ref, criterion.id)
|
|
3049
|
-
|
|
3155
|
+
self._attach_close_evidence(task_number, ref, slug, issue, pr)
|
|
3156
|
+
self.alissa.set_status(ref, "pending_validation")
|
|
3157
|
+
|
|
3158
|
+
def _attach_close_evidence(
|
|
3159
|
+
self, task_number: int, ref: str, slug: str, issue: Issue, pr: PullRequest
|
|
3160
|
+
) -> None:
|
|
3161
|
+
"""Attach the merged-PR evidence, or settle the step without it. The
|
|
3162
|
+
one rule: never RAISE unless a retry is genuinely owed, because the
|
|
3163
|
+
caller's next line is the `pending_validation` hand-off and an
|
|
3164
|
+
exception here strands the task at `in_progress` (issue #44 -- the
|
|
3165
|
+
strand this method exists to remove).
|
|
3166
|
+
|
|
3167
|
+
The ladder, in the order a failure walks it:
|
|
3168
|
+
|
|
3169
|
+
1. **Transient** (`AlissaTransient`: 408/429/5xx or a transport
|
|
3170
|
+
failure) -- re-raised untouched, at every rung. This is the ONLY
|
|
3171
|
+
path that keeps today's behavior byte-for-byte: `_close_loop` routes
|
|
3172
|
+
it to `_closeloop_failed`, which pages `closeloop-failed:a<j>` and
|
|
3173
|
+
floors the retry. An outage must not spend the degrade.
|
|
3174
|
+
2. **Permanent rejection of the ENRICHED body** -- retried ONCE,
|
|
3175
|
+
immediately, with the bare `CLOSE_EVIDENCE_BODY`. The enriched record
|
|
3176
|
+
is the only *variable* input to this write (a PR body someone else
|
|
3177
|
+
authored, up to `CLOSE_EVIDENCE_RECORD_MAX_CHARS` of it), so it is
|
|
3178
|
+
the first suspect for a deterministic refusal, and dropping it is
|
|
3179
|
+
the same "degrade loudly, never block" trade `render_close_evidence`
|
|
3180
|
+
already makes for an unreadable body. Nothing is added to the bare
|
|
3181
|
+
payload to explain the degradation: the payload is the suspect, and
|
|
3182
|
+
the log line is where the explanation belongs.
|
|
3183
|
+
3. **Permanent rejection of the BARE line too** -- the payload was
|
|
3184
|
+
never the problem (a lost role, an endpoint refusing outright), so
|
|
3185
|
+
`_close_evidence_unwritable` decides between one more retry and
|
|
3186
|
+
giving up.
|
|
3187
|
+
|
|
3188
|
+
Why the split at transient/permanent is `AlissaTransient` and not a
|
|
3189
|
+
status test here: the client's taxonomy already IS that split (it keys
|
|
3190
|
+
on status, which is the contract), and `_dedupe_unreadable`'s lesson is
|
|
3191
|
+
the reason it matters -- a self-clearing rate limit must never spend a
|
|
3192
|
+
one-shot remedy that a real, permanent failure will need."""
|
|
3193
|
+
if self.state.escalated(task_number, CLOSELOOP_EVIDENCE_DONE):
|
|
3194
|
+
return
|
|
3195
|
+
try:
|
|
3050
3196
|
self.alissa.add_evidence(
|
|
3051
3197
|
ref,
|
|
3052
3198
|
CLOSE_EVIDENCE_TITLE,
|
|
3053
3199
|
render_close_evidence(slug, pr),
|
|
3054
3200
|
)
|
|
3055
|
-
|
|
3056
|
-
|
|
3201
|
+
except AlissaTransient:
|
|
3202
|
+
raise
|
|
3203
|
+
except AlissaError as rich_exc:
|
|
3204
|
+
log.warning(
|
|
3205
|
+
"%s: the close evidence was REJECTED permanently on %s (%s) — "
|
|
3206
|
+
"retrying once with the bare merge confirmation, without the "
|
|
3207
|
+
"merged pull request's delivery record",
|
|
3208
|
+
slug, ref, rich_exc,
|
|
3209
|
+
)
|
|
3210
|
+
try:
|
|
3211
|
+
self.alissa.add_evidence(
|
|
3212
|
+
ref,
|
|
3213
|
+
CLOSE_EVIDENCE_TITLE,
|
|
3214
|
+
CLOSE_EVIDENCE_BODY.format(slug=slug, pr_url=pr.url),
|
|
3215
|
+
)
|
|
3216
|
+
except AlissaTransient:
|
|
3217
|
+
raise
|
|
3218
|
+
except AlissaError as bare_exc:
|
|
3219
|
+
self._close_evidence_unwritable(
|
|
3220
|
+
task_number, ref, slug, issue, pr, bare_exc,
|
|
3221
|
+
)
|
|
3222
|
+
else:
|
|
3223
|
+
log.warning(
|
|
3224
|
+
"%s: DEGRADED close evidence attached to %s — the merge "
|
|
3225
|
+
"confirmation only; the delivery record was rejected (%s) "
|
|
3226
|
+
"and is on the pull request %s",
|
|
3227
|
+
slug, ref, rich_exc, pr.url or pr.pr_slug,
|
|
3228
|
+
)
|
|
3229
|
+
self.state.record_escalation(task_number, CLOSELOOP_EVIDENCE_DONE)
|
|
3230
|
+
|
|
3231
|
+
def _close_evidence_unwritable(
|
|
3232
|
+
self,
|
|
3233
|
+
task_number: int,
|
|
3234
|
+
ref: str,
|
|
3235
|
+
slug: str,
|
|
3236
|
+
issue: Issue,
|
|
3237
|
+
pr: PullRequest,
|
|
3238
|
+
exc: AlissaError,
|
|
3239
|
+
) -> None:
|
|
3240
|
+
"""Neither the enriched record nor the bare line could be written, and
|
|
3241
|
+
the refusal was permanent. Either raise (a retry is still owed) or
|
|
3242
|
+
return, having paged terminally -- the caller then settles the step and
|
|
3243
|
+
the hand-off finishes WITHOUT evidence.
|
|
3244
|
+
|
|
3245
|
+
Why give up at all: a permanent refusal does not self-heal, so the
|
|
3246
|
+
alternative is re-attempting the identical write every poll forever
|
|
3247
|
+
while the origin task sits at `in_progress` and the operator is paged
|
|
3248
|
+
on a schedule. The merge already happened; thin evidence on a task that
|
|
3249
|
+
reached `pending_validation` is strictly better than a full record on a
|
|
3250
|
+
task nobody can validate. That is the same trade the close edge already
|
|
3251
|
+
makes for an unreadable PR body, taken one rung further down.
|
|
3252
|
+
|
|
3253
|
+
Why not give up on the FIRST permanent refusal, since a deterministic
|
|
3254
|
+
failure is deterministic by definition: because "permanent" here is a
|
|
3255
|
+
CLASSIFICATION, not an observation. A 403 is permanent as a rule and
|
|
3256
|
+
transient in fact when a role is being re-granted, and this remedy is
|
|
3257
|
+
one-shot -- the evidence is dropped for good. So the first refusal
|
|
3258
|
+
records this clock and re-raises into today's unchanged path (page,
|
|
3259
|
+
floor, retry), and only a refusal that is STILL permanent
|
|
3260
|
+
`CLOSELOOP_RETRY_FLOOR_SECONDS` later spends the remedy. The floor
|
|
3261
|
+
measures THE FAILURE and not the task's age, for the reason
|
|
3262
|
+
`_dedupe_unreadable` gives at length.
|
|
3263
|
+
|
|
3264
|
+
Why the page must have LANDED before the step settles: settling is what
|
|
3265
|
+
stops every future attempt, so a give-up whose page never posted would
|
|
3266
|
+
be silent forever -- the exact failure mode issue #44 is about, moved
|
|
3267
|
+
one layer up. `_escalate` records its dedupe row only after the primary
|
|
3268
|
+
comment posts, so it can say whether the page was delivered and this
|
|
3269
|
+
gates on that; without it, this raises instead and the give-up is
|
|
3270
|
+
re-attempted next poll. (When the page cannot post because the token
|
|
3271
|
+
lost its role on the task, the `pending_validation` write below was
|
|
3272
|
+
going to fail anyway -- so nothing is stranded that was not already.)
|
|
3273
|
+
|
|
3274
|
+
What this page may NOT claim: the hand-off in the past tense, or
|
|
3275
|
+
silence from the close loop. Both were false in round 1 -- see
|
|
3276
|
+
`CLOSE_EVIDENCE_FAILED_TASK_COMMENT`, which now scopes its terminality
|
|
3277
|
+
to the evidence step, the one thing this method really does end."""
|
|
3278
|
+
rows = self.state.escalations_matching(task_number, CLOSE_EVIDENCE_FAILED_SINCE)
|
|
3279
|
+
now = int(self._now())
|
|
3280
|
+
if not rows:
|
|
3281
|
+
self.state.record_escalation(task_number, CLOSE_EVIDENCE_FAILED_SINCE, at=now)
|
|
3282
|
+
log.error(
|
|
3283
|
+
"%s: close evidence rejected permanently on %s, bare line too "
|
|
3284
|
+
"(%s) — first refusal, retrying behind the close-loop floor",
|
|
3285
|
+
slug, ref, exc,
|
|
3286
|
+
)
|
|
3287
|
+
raise exc
|
|
3288
|
+
failing_for = now - int(rows[0]["escalated_at"])
|
|
3289
|
+
if failing_for < CLOSELOOP_RETRY_FLOOR_SECONDS:
|
|
3290
|
+
log.error(
|
|
3291
|
+
"%s: close evidence rejected permanently on %s, bare line too "
|
|
3292
|
+
"(%s) — refused for %ds, within the %ds floor; retrying",
|
|
3293
|
+
slug, ref, exc, failing_for, CLOSELOOP_RETRY_FLOOR_SECONDS,
|
|
3294
|
+
)
|
|
3295
|
+
raise exc
|
|
3296
|
+
|
|
3297
|
+
delivered = self._escalate(
|
|
3298
|
+
task_number,
|
|
3299
|
+
ESCALATION_CLOSE_EVIDENCE_FAILED,
|
|
3300
|
+
CLOSE_EVIDENCE_FAILED_TASK_COMMENT.format(
|
|
3301
|
+
slug=slug,
|
|
3302
|
+
floor_hours=CLOSELOOP_RETRY_FLOOR_SECONDS // 3600,
|
|
3303
|
+
error=exc,
|
|
3304
|
+
pr_url=pr.url or pr.pr_slug,
|
|
3305
|
+
),
|
|
3306
|
+
issue,
|
|
3307
|
+
CLOSE_EVIDENCE_FAILED_ISSUE_COMMENT,
|
|
3308
|
+
)
|
|
3309
|
+
if not delivered:
|
|
3310
|
+
log.error(
|
|
3311
|
+
"%s: giving up on close evidence for %s, but the terminal page "
|
|
3312
|
+
"could not be delivered — NOT settling; retries next poll",
|
|
3313
|
+
slug, ref,
|
|
3314
|
+
)
|
|
3315
|
+
raise exc
|
|
3316
|
+
log.error(
|
|
3317
|
+
"%s: GAVE UP on close evidence for %s after %ds of permanent "
|
|
3318
|
+
"refusals (%s) — completing the hand-off without it; operator "
|
|
3319
|
+
"paged once, no further attempts",
|
|
3320
|
+
slug, ref, failing_for, exc,
|
|
3321
|
+
)
|
|
3057
3322
|
|
|
3058
3323
|
def _closeloop_failed(
|
|
3059
3324
|
self, task_number: int, slug: str, issue: Issue, exc: Exception, prior: list
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.3.11
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.10
|
|
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
|