alissa-tools-github-devloop 0.6.0__tar.gz → 0.6.2__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.6.0/src/main/alissa_tools_github_devloop.egg-info → alissa_tools_github_devloop-0.6.2}/PKG-INFO +1 -1
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/__main__.py +22 -1
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/config.py +18 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/ghclient.py +23 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/loop.py +397 -63
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/state.py +25 -0
- alissa_tools_github_devloop-0.6.2/src/main/alissa/tools/github/devloop/version +1 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/webui/sources.py +1 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2/src/main/alissa_tools_github_devloop.egg-info}/PKG-INFO +1 -1
- alissa_tools_github_devloop-0.6.0/src/main/alissa/tools/github/devloop/version +0 -1
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/MANIFEST.in +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/README.md +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/requirements.txt +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/setup.cfg +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/setup.py +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/alissa.py +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/proc.py +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/version.py +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/webui/page.py +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa_tools_github_devloop.egg-info/SOURCES.txt +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa_tools_github_devloop.egg-info/dependency_links.txt +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa_tools_github_devloop.egg-info/entry_points.txt +0 -0
- {alissa_tools_github_devloop-0.6.0 → alissa_tools_github_devloop-0.6.2}/src/main/alissa_tools_github_devloop.egg-info/top_level.txt +0 -0
|
@@ -165,6 +165,24 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
165
165
|
"never auto-resume — a dead-with-PR lane waits for an operator",
|
|
166
166
|
)
|
|
167
167
|
|
|
168
|
+
claims = over.add_mutually_exclusive_group()
|
|
169
|
+
claims.add_argument(
|
|
170
|
+
"--activity-claims",
|
|
171
|
+
dest="activity_claims_enabled",
|
|
172
|
+
action="store_true",
|
|
173
|
+
default=None,
|
|
174
|
+
help="read the trigger artifact's activity comment for another "
|
|
175
|
+
"instance's fresh spawn line and defer on it, so spawn dedupe "
|
|
176
|
+
"survives a ledger the local instance never wrote (the default)",
|
|
177
|
+
)
|
|
178
|
+
claims.add_argument(
|
|
179
|
+
"--no-activity-claims",
|
|
180
|
+
dest="activity_claims_enabled",
|
|
181
|
+
action="store_false",
|
|
182
|
+
help="ledger-only spawn dedupe: never read external claim evidence "
|
|
183
|
+
"(two overlapping instances can then both spawn on one item)",
|
|
184
|
+
)
|
|
185
|
+
|
|
168
186
|
dry = over.add_mutually_exclusive_group()
|
|
169
187
|
dry.add_argument(
|
|
170
188
|
"--dry-run",
|
|
@@ -203,6 +221,7 @@ def overrides_from(args: argparse.Namespace) -> dict:
|
|
|
203
221
|
"maintain_label": args.maintain_label,
|
|
204
222
|
"maintain_enabled": args.maintain_enabled,
|
|
205
223
|
"resume_enabled": args.resume_enabled,
|
|
224
|
+
"activity_claims_enabled": args.activity_claims_enabled,
|
|
206
225
|
"dry_run": args.dry_run,
|
|
207
226
|
}
|
|
208
227
|
|
|
@@ -234,13 +253,15 @@ def log_effective_config(config: Config, login: str) -> None:
|
|
|
234
253
|
log.info("reviewers: %s", ", ".join(config.reviewers) or "none")
|
|
235
254
|
log.info(
|
|
236
255
|
"poll every %ss; dry_run=%s; attempt_cap=%s; stale after %s min; "
|
|
237
|
-
"max_sessions=%s; fix_rounds=%s; maintain=%s (label %r); resume=%s"
|
|
256
|
+
"max_sessions=%s; fix_rounds=%s; maintain=%s (label %r); resume=%s; "
|
|
257
|
+
"activity_claims=%s",
|
|
238
258
|
config.poll_interval, config.dry_run, config.attempt_cap,
|
|
239
259
|
config.stale_minutes,
|
|
240
260
|
config.max_sessions or "unlimited",
|
|
241
261
|
config.fix_rounds_enabled,
|
|
242
262
|
config.maintain_enabled, config.maintain_label,
|
|
243
263
|
config.resume_enabled,
|
|
264
|
+
config.activity_claims_enabled,
|
|
244
265
|
)
|
|
245
266
|
log.debug("hub_template: %s", config.hub_template)
|
|
246
267
|
log.debug("agent_profile: %s", config.agent_profile)
|
|
@@ -63,6 +63,7 @@ CONFIG_KEYS = (
|
|
|
63
63
|
"maintain_label",
|
|
64
64
|
"maintain_enabled",
|
|
65
65
|
"resume_enabled",
|
|
66
|
+
"activity_claims_enabled",
|
|
66
67
|
"dry_run",
|
|
67
68
|
)
|
|
68
69
|
|
|
@@ -180,6 +181,20 @@ class Config:
|
|
|
180
181
|
# operator who wants a dead-with-PR lane to wait for a human.
|
|
181
182
|
resume_enabled: bool = True
|
|
182
183
|
|
|
184
|
+
# Cross-instance spawn dedupe (issue #53): before spawning on any edge,
|
|
185
|
+
# read the trigger artifact's mechanical activity comment and treat a
|
|
186
|
+
# spawn line younger than stale_minutes as an in-flight claim EVEN when
|
|
187
|
+
# the local ledger has no row. The ledger is instance-local and its volume
|
|
188
|
+
# follows the active deployment, so during a deploy overlap or a
|
|
189
|
+
# crashloop two daemons run with one GitHub identity and two ledgers and
|
|
190
|
+
# both spawn; the activity comment is the one claim record either can
|
|
191
|
+
# read. Default ON -- it closes a thrice-observed duplicate-PR incident,
|
|
192
|
+
# it can only ever DEFER a spawn, and a claim expires with the same
|
|
193
|
+
# stale_minutes window a ledger row does. false restores the
|
|
194
|
+
# ledger-only dedupe: the escape hatch for a single-instance deployment
|
|
195
|
+
# that would rather not pay the extra comments fetch.
|
|
196
|
+
activity_claims_enabled: bool = True
|
|
197
|
+
|
|
183
198
|
dry_run: bool = False
|
|
184
199
|
|
|
185
200
|
# Derived at build time: `repos` casefolded for matching. GitHub names are
|
|
@@ -338,6 +353,9 @@ class Config:
|
|
|
338
353
|
resume_enabled=bool(
|
|
339
354
|
raw.get("resume_enabled", cls.resume_enabled)
|
|
340
355
|
),
|
|
356
|
+
activity_claims_enabled=bool(
|
|
357
|
+
raw.get("activity_claims_enabled", cls.activity_claims_enabled)
|
|
358
|
+
),
|
|
341
359
|
dry_run=bool(raw.get("dry_run", cls.dry_run)),
|
|
342
360
|
)
|
|
343
361
|
|
|
@@ -821,6 +821,29 @@ class GitHub:
|
|
|
821
821
|
if isinstance(c, dict)
|
|
822
822
|
]
|
|
823
823
|
|
|
824
|
+
def marked_comment(
|
|
825
|
+
self, owner: str, repo: str, number: int, marker: str
|
|
826
|
+
) -> "IssueComment | None":
|
|
827
|
+
"""THE own-authored comment carrying `marker` on this issue/PR, or None.
|
|
828
|
+
|
|
829
|
+
The find half of the activity comment's find-or-create, lifted out of
|
|
830
|
+
the writer so the READER shares one definition of "the daemon's
|
|
831
|
+
activity comment" with it -- a marker/author filter that drifted
|
|
832
|
+
between the two would let a spawn append to one comment while the
|
|
833
|
+
cross-instance claim check read another.
|
|
834
|
+
|
|
835
|
+
Both halves of the filter are load-bearing. The marker names the
|
|
836
|
+
mechanical log (an operator page on the same artifact must never be
|
|
837
|
+
mistaken for it), and OWN AUTHORSHIP is what keeps a marker anyone can
|
|
838
|
+
paste from being PATCHed by the writer -- or, for the reader, from
|
|
839
|
+
being able to park a lane by spoofing a claim line. First match wins,
|
|
840
|
+
matching the writer's `mine[0]`.
|
|
841
|
+
"""
|
|
842
|
+
for comment in self.issue_comments(owner, repo, number):
|
|
843
|
+
if comment.author == self.login and marker in comment.body:
|
|
844
|
+
return comment
|
|
845
|
+
return None
|
|
846
|
+
|
|
824
847
|
def update_comment(
|
|
825
848
|
self, owner: str, repo: str, comment_id: int, body: str
|
|
826
849
|
) -> None:
|
|
@@ -161,6 +161,35 @@ spams the comment; spawn lines self-dedupe on their own spawn ledger row. Being
|
|
|
161
161
|
plain issue comments, they create no review records, never move the fix edge's
|
|
162
162
|
author-activity liveness signal, and never register as operator escalations.
|
|
163
163
|
|
|
164
|
+
That comment is also the loop's only CROSS-INSTANCE claim record, and since
|
|
165
|
+
`activity_claims_enabled` (default true) it is READ as well as written. Every
|
|
166
|
+
dedupe signal above this one is local (the ledger) or identity-scoped (the
|
|
167
|
+
assignment), and neither can see the failure that produced three
|
|
168
|
+
closed-as-duplicate PRs: during a deploy overlap or a crashloop window two
|
|
169
|
+
daemon instances are briefly alive sharing ONE GitHub identity but not one
|
|
170
|
+
ledger -- the state volume follows the active deployment -- so each reads an
|
|
171
|
+
empty ledger, each believes it holds the only claim, and both spawn. The
|
|
172
|
+
activity comment escapes both limits: it lives on the trigger artifact, so any
|
|
173
|
+
instance can read any other's spawn line from the one place both already
|
|
174
|
+
write. Before spawning on ANY edge, once the local ledger stops vouching for
|
|
175
|
+
the lane, the daemon reads it: a spawn line younger than stale_minutes defers
|
|
176
|
+
exactly like a live local claim, EVEN with no ledger row at all. A claim is
|
|
177
|
+
matched by LANE (the session name minus its attempt segment, so the fix edge's
|
|
178
|
+
round stays in and two instances numbering attempts differently still agree
|
|
179
|
+
what is claimed) and tested on FRESHNESS ALONE -- never liveness, because a
|
|
180
|
+
peer's sessions are not in this container's tmux and would read DEAD, which
|
|
181
|
+
would defeat the entire signal. Past the window the claim expires and the
|
|
182
|
+
stale/liveness matrix above governs untouched. The new signal is fail-open by
|
|
183
|
+
construction: no comment, an unreadable one, a foreign author, a missing
|
|
184
|
+
marker, a fetch that raises -- every one of them reads as "no claim" and
|
|
185
|
+
restores the ledger-only behavior, because a gate that could latch a lane
|
|
186
|
+
closed on a bad read would be worse than the double-spawn it prevents. This is
|
|
187
|
+
also why the activity line is now written BEFORE the enqueue on all three
|
|
188
|
+
edges rather than after: a claim published after the session exists does not
|
|
189
|
+
cover the window it is for. The cost is a crash between the line and the
|
|
190
|
+
enqueue leaving a claim with no session -- one stale window of deferral, then
|
|
191
|
+
the line ages out and the next poll spawns normally.
|
|
192
|
+
|
|
164
193
|
Finally, every pass runs the SESSION REAPER: finished `develop-*`/`fix-*`
|
|
165
194
|
worker sessions otherwise idle in tmux forever (observed live on the reviewer
|
|
166
195
|
daemon, 2026-07-22 -- and dev workers are worse, because they block through
|
|
@@ -184,6 +213,7 @@ spared, dry-run only logs, and kills are best-effort: a CLI without
|
|
|
184
213
|
|
|
185
214
|
from __future__ import annotations
|
|
186
215
|
|
|
216
|
+
import calendar
|
|
187
217
|
import logging
|
|
188
218
|
import re
|
|
189
219
|
import time
|
|
@@ -520,6 +550,37 @@ ACTIVITY_HEADER = (
|
|
|
520
550
|
# line is deduped per episode -- see deferral_activity_kind.
|
|
521
551
|
ACTIVITY_DEFERRED = "deferred"
|
|
522
552
|
|
|
553
|
+
# The reader's grammar for a line `_activity_line` wrote: bullet, UTC
|
|
554
|
+
# timestamp, backticked session name, edge, context. The edge field is
|
|
555
|
+
# non-greedy and em-dash-free so the CONTEXT keeps every em-dash it contains
|
|
556
|
+
# ("deferred — at capacity", "re-enqueued — previous session presumed dead");
|
|
557
|
+
# a line that does not match this shape is simply not read, which is the
|
|
558
|
+
# fail-open direction (see external_claim).
|
|
559
|
+
ACTIVITY_LINE_RE = re.compile(
|
|
560
|
+
r"^-\s+(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) UTC\s+—\s+"
|
|
561
|
+
r"`(?P<session>[^`]+)`\s+—\s+(?P<edge>[^—]*?)\s+—\s+(?P<context>.*)$"
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# The trailing attempt segment every session name ends in. Stripping it turns
|
|
565
|
+
# a session name into its LANE -- the (edge, artifact, and on the fix edge
|
|
566
|
+
# round) identity that a claim is made against. Attempt-independent on
|
|
567
|
+
# purpose: two instances with different ledgers disagree about the attempt
|
|
568
|
+
# NUMBER while working the very same lane, and the lane is what must not be
|
|
569
|
+
# double-claimed.
|
|
570
|
+
ATTEMPT_SUFFIX_RE = re.compile(r"-a\d+$")
|
|
571
|
+
|
|
572
|
+
# How far a claim line's timestamp may sit in the FUTURE and still be read.
|
|
573
|
+
# The line is stamped by whichever instance wrote it, off ITS clock, and read
|
|
574
|
+
# against ours, so a small forward skew is ordinary and must not make a real
|
|
575
|
+
# claim unreadable -- inside the tolerance the age simply floors at 0 (the
|
|
576
|
+
# line reads as brand new, the deferring direction). Beyond it the clock is
|
|
577
|
+
# not plausibly a peer's: honouring such a line would park the lane for as
|
|
578
|
+
# long as the bogus timestamp stays in the future, so it is dropped -- the
|
|
579
|
+
# fail-open direction this whole signal is held to. A backward-skewed clock
|
|
580
|
+
# needs no tolerance: its line simply looks older, and past stale_minutes it
|
|
581
|
+
# stops claiming anything, exactly as an old line should.
|
|
582
|
+
ACTIVITY_SKEW_TOLERANCE = 300
|
|
583
|
+
|
|
523
584
|
|
|
524
585
|
# Escalation-ledger kinds. Cap-out, assignment-rejection, and a stalled
|
|
525
586
|
# linked-PR deferral have different operator remedies (raise attempt_cap vs
|
|
@@ -920,6 +981,86 @@ def maintain_session_name(pr: PullRequest, attempt: int) -> str:
|
|
|
920
981
|
return f"maintain-{owner}-{repo}-pr{pr.number}-a{attempt}"
|
|
921
982
|
|
|
922
983
|
|
|
984
|
+
def activity_lane(session: str) -> str:
|
|
985
|
+
"""A session name reduced to its LANE by dropping the attempt segment.
|
|
986
|
+
|
|
987
|
+
`develop-acme-widgets-i7-a3` -> `develop-acme-widgets-i7`;
|
|
988
|
+
`fix-acme-widgets-pr16-r2-a1` -> `fix-acme-widgets-pr16-r2` (the ROUND
|
|
989
|
+
stays: the fix ledger dedupes per round, so a claim must too);
|
|
990
|
+
`maintain-acme-widgets-pr16-a1` -> `maintain-acme-widgets-pr16`.
|
|
991
|
+
|
|
992
|
+
The lane is what a spawn claims. Attempt numbers come from a LOCAL ledger,
|
|
993
|
+
and the whole point of reading another instance's spawn line is that its
|
|
994
|
+
ledger is not ours -- it can be on attempt 1 of a lane we would number 3.
|
|
995
|
+
Comparing lanes rather than names is what lets the two agree on what is
|
|
996
|
+
already claimed. A name without an attempt segment is returned unchanged
|
|
997
|
+
and so simply matches nothing (no lane the daemon ever spawns lacks one).
|
|
998
|
+
"""
|
|
999
|
+
return ATTEMPT_SUFFIX_RE.sub("", session)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
def parse_activity_timestamp(ts: str) -> "int | None":
|
|
1003
|
+
"""An activity line's `%Y-%m-%d %H:%M:%S` UTC stamp as unix seconds, or
|
|
1004
|
+
None when it is not a real instant.
|
|
1005
|
+
|
|
1006
|
+
`_activity_line` writes UTC via `time.gmtime`, so the read is `timegm`,
|
|
1007
|
+
never `mktime` -- reading a UTC stamp through the host's local timezone
|
|
1008
|
+
would shift every claim by the offset and, east of Greenwich, make fresh
|
|
1009
|
+
lines look future-dated. The mirror of ghclient.parse_github_timestamp
|
|
1010
|
+
(different format, same contract: unreadable is None, never an error).
|
|
1011
|
+
"""
|
|
1012
|
+
try:
|
|
1013
|
+
return calendar.timegm(time.strptime(ts, "%Y-%m-%d %H:%M:%S"))
|
|
1014
|
+
except (ValueError, TypeError):
|
|
1015
|
+
return None
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
@dataclass(frozen=True, slots=True)
|
|
1019
|
+
class ActivityClaim:
|
|
1020
|
+
"""One readable spawn record from the activity comment: a session name, the
|
|
1021
|
+
instant its line was written, and how old that is against OUR clock (never
|
|
1022
|
+
negative -- see ACTIVITY_SKEW_TOLERANCE)."""
|
|
1023
|
+
|
|
1024
|
+
session: str
|
|
1025
|
+
at: int
|
|
1026
|
+
age: float
|
|
1027
|
+
|
|
1028
|
+
|
|
1029
|
+
def activity_claims(body: str, now: "float | None" = None) -> "list[ActivityClaim]":
|
|
1030
|
+
"""Every readable SPAWN record in an activity comment's body.
|
|
1031
|
+
|
|
1032
|
+
Spawn records only: a capacity-deferral line explicitly claims nothing (no
|
|
1033
|
+
self-assign, no ledger row, no session -- see _spawn's gate), so reading
|
|
1034
|
+
one as a claim would let a deferring instance park the lane against
|
|
1035
|
+
everybody including itself. The context field is what tells them apart,
|
|
1036
|
+
and `deferred` is the only context that is not a spawn.
|
|
1037
|
+
|
|
1038
|
+
Everything else fails open, one line at a time: a line that does not match
|
|
1039
|
+
the grammar, or whose stamp is not an instant, or whose stamp is
|
|
1040
|
+
implausibly far in the future is dropped and the remaining lines are still
|
|
1041
|
+
read. A comment that is entirely unreadable therefore yields no claims,
|
|
1042
|
+
which is exactly "behave as before" on the new signal.
|
|
1043
|
+
"""
|
|
1044
|
+
now = time.time() if now is None else now
|
|
1045
|
+
claims: "list[ActivityClaim]" = []
|
|
1046
|
+
for raw in (body or "").splitlines():
|
|
1047
|
+
match = ACTIVITY_LINE_RE.match(raw.strip())
|
|
1048
|
+
if match is None:
|
|
1049
|
+
continue
|
|
1050
|
+
if match.group("context").strip().lower().startswith(ACTIVITY_DEFERRED):
|
|
1051
|
+
continue
|
|
1052
|
+
at = parse_activity_timestamp(match.group("ts"))
|
|
1053
|
+
if at is None:
|
|
1054
|
+
continue
|
|
1055
|
+
age = now - at
|
|
1056
|
+
if age < -ACTIVITY_SKEW_TOLERANCE:
|
|
1057
|
+
continue
|
|
1058
|
+
claims.append(
|
|
1059
|
+
ActivityClaim(session=match.group("session"), at=at, age=max(age, 0.0))
|
|
1060
|
+
)
|
|
1061
|
+
return claims
|
|
1062
|
+
|
|
1063
|
+
|
|
923
1064
|
def no_envelope_verdict(owner: str, repo: str, number: int) -> "str | None":
|
|
924
1065
|
"""The default (stub) envelope tie-breaker: no verdict is ever readable.
|
|
925
1066
|
|
|
@@ -1026,6 +1167,29 @@ class DevWatcher:
|
|
|
1026
1167
|
session=session_name(issue, attempts),
|
|
1027
1168
|
)
|
|
1028
1169
|
|
|
1170
|
+
# The one dedupe signal that survives an instance change, consulted
|
|
1171
|
+
# the moment the LOCAL ledger stops vouching for the lane (no row, or
|
|
1172
|
+
# a row already past the window): another instance's fresh spawn line
|
|
1173
|
+
# on this issue's own activity comment. It sits ABOVE the
|
|
1174
|
+
# self-assigned-outside guard on purpose. Both branches describe an
|
|
1175
|
+
# issue marked in flight with nothing local to show for it, but their
|
|
1176
|
+
# operator remedies are OPPOSITE -- that guard's message asks the
|
|
1177
|
+
# operator to UNASSIGN, which against a peer instance's live claim
|
|
1178
|
+
# would clear the only other marker and invite the very double-spawn
|
|
1179
|
+
# this closes. The claim is the more specific diagnosis, so it answers
|
|
1180
|
+
# first. The cost is one comments fetch per poll on an issue nothing
|
|
1181
|
+
# local claims; the fresh-in-flight return above keeps it off the
|
|
1182
|
+
# common case, exactly as the linked-PR probe is kept off it.
|
|
1183
|
+
claim = self.external_claim(
|
|
1184
|
+
issue.owner,
|
|
1185
|
+
issue.repo,
|
|
1186
|
+
issue.number,
|
|
1187
|
+
session_name(issue, attempts + 1),
|
|
1188
|
+
issue.issue_slug,
|
|
1189
|
+
)
|
|
1190
|
+
if claim is not None:
|
|
1191
|
+
return self._claim_deferral(claim, attempts)
|
|
1192
|
+
|
|
1029
1193
|
if login in issue.assignees and age is None:
|
|
1030
1194
|
# Self-assigned but the ledger never spawned: assigned outside this
|
|
1031
1195
|
# daemon (by hand, or under another workspace's ledger). Not a
|
|
@@ -1305,27 +1469,29 @@ class DevWatcher:
|
|
|
1305
1469
|
except AssignmentRejected as exc:
|
|
1306
1470
|
return self._escalate_rejection(issue, exc)
|
|
1307
1471
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
#
|
|
1325
|
-
#
|
|
1326
|
-
#
|
|
1327
|
-
#
|
|
1328
|
-
#
|
|
1472
|
+
# BEFORE the enqueue, not after (issue #53). The line stopped being
|
|
1473
|
+
# only telemetry the moment `external_claim` started reading it: it is
|
|
1474
|
+
# now this spawn's CROSS-INSTANCE claim, the one marker a peer daemon
|
|
1475
|
+
# with its own ledger can see, and a claim published after the session
|
|
1476
|
+
# exists leaves the window it is meant to close wide open -- the whole
|
|
1477
|
+
# gap is the seconds between deciding to spawn and the record of it
|
|
1478
|
+
# becoming visible. Publish first, then enqueue.
|
|
1479
|
+
#
|
|
1480
|
+
# The crash story is why this is safe to reorder. A crash between the
|
|
1481
|
+
# line and the enqueue leaves a claim with no session behind it: this
|
|
1482
|
+
# lane then defers for ONE stale window, after which the line ages out
|
|
1483
|
+
# of the freshness test and the next poll spawns normally. That is
|
|
1484
|
+
# bounded, self-healing, and strictly cheaper than the failure the
|
|
1485
|
+
# other order permits (two live sessions, two PRs, two review loops,
|
|
1486
|
+
# one closed as a duplicate). Nothing else moved: the assignment still
|
|
1487
|
+
# lands first (it is the issue edge's server-side marker), the line is
|
|
1488
|
+
# still best-effort -- `_append_activity` swallows its failures and a
|
|
1489
|
+
# failed line never gates the spawn -- and still fully dry-run gated.
|
|
1490
|
+
#
|
|
1491
|
+
# A resume names the confirmation that unblocked it (an operator
|
|
1492
|
+
# reading the issue must be able to tell a proven-dead resume from a
|
|
1493
|
+
# timer-only retry); a plain retry says presumed; attempt 1 is a plain
|
|
1494
|
+
# spawn.
|
|
1329
1495
|
if resume_pr is not None:
|
|
1330
1496
|
context = (
|
|
1331
1497
|
f"resumed — previous session confirmed dead (absent from the "
|
|
@@ -1344,6 +1510,22 @@ class DevWatcher:
|
|
|
1344
1510
|
issue.issue_slug,
|
|
1345
1511
|
)
|
|
1346
1512
|
|
|
1513
|
+
self.alissa.enqueue_developer(
|
|
1514
|
+
session=name,
|
|
1515
|
+
directive=directive,
|
|
1516
|
+
cwd=hub,
|
|
1517
|
+
agent=self.config.agent_profile,
|
|
1518
|
+
dry_run=self.config.dry_run,
|
|
1519
|
+
)
|
|
1520
|
+
|
|
1521
|
+
if not self.config.dry_run:
|
|
1522
|
+
self.state.record_spawn(
|
|
1523
|
+
repo_slug=issue.full_name,
|
|
1524
|
+
issue=issue.number,
|
|
1525
|
+
attempt=attempt,
|
|
1526
|
+
session=name,
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1347
1529
|
resumed = (
|
|
1348
1530
|
""
|
|
1349
1531
|
if resume_pr is None
|
|
@@ -1743,6 +1925,26 @@ class DevWatcher:
|
|
|
1743
1925
|
round=round_,
|
|
1744
1926
|
)
|
|
1745
1927
|
|
|
1928
|
+
# The cross-instance claim check, the issue edge's placement
|
|
1929
|
+
# transposed: past the fresh-in-flight return, the local ledger no
|
|
1930
|
+
# longer vouches for this round, and a peer instance's fresh spawn
|
|
1931
|
+
# line on THIS PR's activity comment is the only thing that can. The
|
|
1932
|
+
# lane is round-scoped (activity_lane keeps the `-r<k>` segment), so a
|
|
1933
|
+
# claim on round k never suppresses round k+1 -- the fix ledger
|
|
1934
|
+
# dedupes per round and the external evidence must match it. This edge
|
|
1935
|
+
# needs the signal most: it has no assignment analogue at all (PR
|
|
1936
|
+
# authorship is the marker, and both instances share the identity that
|
|
1937
|
+
# authored it), so before this the ledger was the ONLY dedupe it had.
|
|
1938
|
+
claim = self.external_claim(
|
|
1939
|
+
pr.owner,
|
|
1940
|
+
pr.repo,
|
|
1941
|
+
pr.number,
|
|
1942
|
+
fix_session_name(pr, round_, attempts + 1),
|
|
1943
|
+
pr.pr_slug,
|
|
1944
|
+
)
|
|
1945
|
+
if claim is not None:
|
|
1946
|
+
return self._claim_deferral(claim, attempts, round_=round_)
|
|
1947
|
+
|
|
1746
1948
|
# Fix-retry staleness needs TWO signals, the issue edge's doctrine
|
|
1747
1949
|
# verbatim (see the linked-PR probe in evaluate()): the ledger timer
|
|
1748
1950
|
# says the newest fix spawn is old, but elapsed time alone cannot
|
|
@@ -2093,6 +2295,36 @@ class DevWatcher:
|
|
|
2093
2295
|
dead_session=dead_session,
|
|
2094
2296
|
)
|
|
2095
2297
|
|
|
2298
|
+
# BEFORE the enqueue (issue #53), for the reason _spawn spells out:
|
|
2299
|
+
# the line is this spawn's cross-instance claim, and a claim published
|
|
2300
|
+
# after the session exists does not close the window it is for. On
|
|
2301
|
+
# this edge the argument is sharper still -- there is no
|
|
2302
|
+
# self-assignment to fall back on (the PR's authorship IS the marker,
|
|
2303
|
+
# and both instances share the identity that authored it), so this
|
|
2304
|
+
# line is the ONLY thing a peer daemon can read. Same crash story: a
|
|
2305
|
+
# crash between the line and the enqueue costs one stale window of
|
|
2306
|
+
# deferral on this round, then the line ages out and the retry
|
|
2307
|
+
# proceeds. The line stays best-effort and dry-run gated.
|
|
2308
|
+
#
|
|
2309
|
+
# The session name carries the round and attempt; the context marks a
|
|
2310
|
+
# retry, and names the confirmation when a resume is what unblocked it.
|
|
2311
|
+
if dead_session is not None:
|
|
2312
|
+
context = (
|
|
2313
|
+
"resumed — previous session confirmed dead (absent from the "
|
|
2314
|
+
"tmux listing); finishing the round on the same branch"
|
|
2315
|
+
)
|
|
2316
|
+
elif reenqueued:
|
|
2317
|
+
context = "re-enqueued — previous session presumed dead"
|
|
2318
|
+
else:
|
|
2319
|
+
context = "spawned"
|
|
2320
|
+
self._append_activity(
|
|
2321
|
+
pr.owner,
|
|
2322
|
+
pr.repo,
|
|
2323
|
+
pr.number,
|
|
2324
|
+
self._activity_line(name, "fix edge", context),
|
|
2325
|
+
pr.pr_slug,
|
|
2326
|
+
)
|
|
2327
|
+
|
|
2096
2328
|
# No self-assignment here: the PR's authorship IS the server-side
|
|
2097
2329
|
# marker (the token already owns it), and the in-flight signal is the
|
|
2098
2330
|
# fix ledger row plus, once the session re-requests, the pending
|
|
@@ -2114,26 +2346,6 @@ class DevWatcher:
|
|
|
2114
2346
|
session=name,
|
|
2115
2347
|
)
|
|
2116
2348
|
|
|
2117
|
-
# AFTER the spawn side effects: telemetry, never a gate. The session
|
|
2118
|
-
# name carries the round and attempt; the context marks a retry, and
|
|
2119
|
-
# names the confirmation when a resume is what unblocked it.
|
|
2120
|
-
if dead_session is not None:
|
|
2121
|
-
context = (
|
|
2122
|
-
"resumed — previous session confirmed dead (absent from the "
|
|
2123
|
-
"tmux listing); finishing the round on the same branch"
|
|
2124
|
-
)
|
|
2125
|
-
elif reenqueued:
|
|
2126
|
-
context = "re-enqueued — previous session presumed dead"
|
|
2127
|
-
else:
|
|
2128
|
-
context = "spawned"
|
|
2129
|
-
self._append_activity(
|
|
2130
|
-
pr.owner,
|
|
2131
|
-
pr.repo,
|
|
2132
|
-
pr.number,
|
|
2133
|
-
self._activity_line(name, "fix edge", context),
|
|
2134
|
-
pr.pr_slug,
|
|
2135
|
-
)
|
|
2136
|
-
|
|
2137
2349
|
resumed = (
|
|
2138
2350
|
f" — RESUMING the round after {dead_session} was confirmed dead"
|
|
2139
2351
|
if dead_session is not None
|
|
@@ -2205,6 +2417,23 @@ class DevWatcher:
|
|
|
2205
2417
|
session=maintain_session_name(pr, attempts),
|
|
2206
2418
|
)
|
|
2207
2419
|
|
|
2420
|
+
# The cross-instance claim check, before the epoch re-basing: whether
|
|
2421
|
+
# this request's budget is fresh or re-based, a peer instance that
|
|
2422
|
+
# already spawned against the SAME standing label must not be spawned
|
|
2423
|
+
# over. Like the fix edge, this edge has no assignment analogue (the
|
|
2424
|
+
# label is the operator's ask, not a per-instance claim, and it stays
|
|
2425
|
+
# put until the maintenance session removes it), so the activity
|
|
2426
|
+
# comment is its only cross-instance evidence.
|
|
2427
|
+
claim = self.external_claim(
|
|
2428
|
+
pr.owner,
|
|
2429
|
+
pr.repo,
|
|
2430
|
+
pr.number,
|
|
2431
|
+
maintain_session_name(pr, attempts + 1),
|
|
2432
|
+
pr.pr_slug,
|
|
2433
|
+
)
|
|
2434
|
+
if claim is not None:
|
|
2435
|
+
return self._claim_deferral(claim, attempts)
|
|
2436
|
+
|
|
2208
2437
|
# Stale path with history: re-base the budget on the current
|
|
2209
2438
|
# request. An unreadable boundary (no visible labeled event, or an
|
|
2210
2439
|
# unparseable timestamp) conservatively leaves the lifetime budget
|
|
@@ -2321,6 +2550,27 @@ class DevWatcher:
|
|
|
2321
2550
|
session=name,
|
|
2322
2551
|
)
|
|
2323
2552
|
|
|
2553
|
+
# BEFORE the enqueue (issue #53), for the reason _spawn spells out:
|
|
2554
|
+
# the line is this spawn's cross-instance claim, and this edge, like
|
|
2555
|
+
# the fix edge, has no per-instance server-side marker to fall back on
|
|
2556
|
+
# -- the label is the OPERATOR's standing ask and stays put until the
|
|
2557
|
+
# maintenance session removes it, so it says nothing about which
|
|
2558
|
+
# instance already took it. Same crash story, same one-stale-window
|
|
2559
|
+
# cost; the line stays best-effort and dry-run gated. The session name
|
|
2560
|
+
# carries the attempt; the context marks a retry.
|
|
2561
|
+
context = (
|
|
2562
|
+
"re-enqueued — previous session presumed dead"
|
|
2563
|
+
if reenqueued
|
|
2564
|
+
else "spawned"
|
|
2565
|
+
)
|
|
2566
|
+
self._append_activity(
|
|
2567
|
+
pr.owner,
|
|
2568
|
+
pr.repo,
|
|
2569
|
+
pr.number,
|
|
2570
|
+
self._activity_line(name, "maintain edge", context),
|
|
2571
|
+
pr.pr_slug,
|
|
2572
|
+
)
|
|
2573
|
+
|
|
2324
2574
|
# No self-assignment (authorship is the marker, like the fix edge)
|
|
2325
2575
|
# and NO label removal: remove-on-completion is the design decision.
|
|
2326
2576
|
# The label stays as the visible in-flight state; the ledger row
|
|
@@ -2341,21 +2591,6 @@ class DevWatcher:
|
|
|
2341
2591
|
session=name,
|
|
2342
2592
|
)
|
|
2343
2593
|
|
|
2344
|
-
# AFTER the spawn side effects: telemetry, never a gate. The session
|
|
2345
|
-
# name carries the attempt; the context marks a retry.
|
|
2346
|
-
context = (
|
|
2347
|
-
"re-enqueued — previous session presumed dead"
|
|
2348
|
-
if reenqueued
|
|
2349
|
-
else "spawned"
|
|
2350
|
-
)
|
|
2351
|
-
self._append_activity(
|
|
2352
|
-
pr.owner,
|
|
2353
|
-
pr.repo,
|
|
2354
|
-
pr.number,
|
|
2355
|
-
self._activity_line(name, "maintain edge", context),
|
|
2356
|
-
pr.pr_slug,
|
|
2357
|
-
)
|
|
2358
|
-
|
|
2359
2594
|
return Decision(
|
|
2360
2595
|
Action.SPAWNED,
|
|
2361
2596
|
f"maintenance session {name} (label {self.config.maintain_label!r} "
|
|
@@ -2431,14 +2666,10 @@ class DevWatcher:
|
|
|
2431
2666
|
log.info("[dry-run] would append activity line on %s: %s", slug, line)
|
|
2432
2667
|
return False
|
|
2433
2668
|
try:
|
|
2434
|
-
mine =
|
|
2435
|
-
|
|
2436
|
-
for c in self.github.issue_comments(owner, repo, number)
|
|
2437
|
-
if c.author == self.github.login and ACTIVITY_MARKER in c.body
|
|
2438
|
-
]
|
|
2439
|
-
if mine:
|
|
2669
|
+
mine = self.github.marked_comment(owner, repo, number, ACTIVITY_MARKER)
|
|
2670
|
+
if mine is not None:
|
|
2440
2671
|
self.github.update_comment(
|
|
2441
|
-
owner, repo, mine
|
|
2672
|
+
owner, repo, mine.id, mine.body + "\n" + line
|
|
2442
2673
|
)
|
|
2443
2674
|
else:
|
|
2444
2675
|
self.github.comment(
|
|
@@ -2449,6 +2680,109 @@ class DevWatcher:
|
|
|
2449
2680
|
return False
|
|
2450
2681
|
return True
|
|
2451
2682
|
|
|
2683
|
+
def external_claim(
|
|
2684
|
+
self, owner: str, repo: str, number: int, session: str, slug: str
|
|
2685
|
+
) -> "ActivityClaim | None":
|
|
2686
|
+
"""Has ANOTHER instance already claimed this lane? The newest fresh
|
|
2687
|
+
spawn line on the trigger artifact's activity comment, or None.
|
|
2688
|
+
|
|
2689
|
+
The local ledger is an instance-local file on a volume that follows
|
|
2690
|
+
the active deployment, so during a deploy overlap or a crashloop two
|
|
2691
|
+
daemons are briefly alive sharing one GitHub identity and NOT one
|
|
2692
|
+
ledger: each reads an empty ledger, each believes it holds the only
|
|
2693
|
+
claim, and both spawn (observed three times -- orc #7, #13 and #16 all
|
|
2694
|
+
produced closed-as-duplicate PRs). Nothing local can see that, and
|
|
2695
|
+
GitHub assignment cannot dedupe it either, because the two instances
|
|
2696
|
+
ARE the same login. The activity comment can: it is written
|
|
2697
|
+
mechanically at every spawn, on the trigger artifact itself, so any
|
|
2698
|
+
instance can read any other instance's claim from the one place both
|
|
2699
|
+
of them already write. External truth for claims is what makes spawn
|
|
2700
|
+
dedupe survive a volume handoff, an overlapping deploy, or ledger loss
|
|
2701
|
+
outright.
|
|
2702
|
+
|
|
2703
|
+
FRESH is `stale_minutes` measured from the line's own timestamp -- the
|
|
2704
|
+
same window the ledger's rows are aged by, so a claim expires exactly
|
|
2705
|
+
as a local spawn row does and a dead foreign session cannot park the
|
|
2706
|
+
lane forever. Past it this returns None and the existing stale /
|
|
2707
|
+
liveness matrix (resume path included) governs untouched.
|
|
2708
|
+
|
|
2709
|
+
Liveness is deliberately NOT consulted for the session on the line.
|
|
2710
|
+
The tmux roster is this container's; a peer instance's sessions are
|
|
2711
|
+
not in it and would read DEAD -- taking that as permission to respawn
|
|
2712
|
+
would defeat the entire signal. Freshness is the only test a claim
|
|
2713
|
+
gets.
|
|
2714
|
+
|
|
2715
|
+
Fail-open, always, on this signal alone: no marker comment, an
|
|
2716
|
+
unreadable one, a comment fetch that raises (the broad except mirrors
|
|
2717
|
+
`_append_activity` -- `_api` raises RateLimited, not CommandError) all
|
|
2718
|
+
return None, which is precisely today's behavior. This gate can only
|
|
2719
|
+
ever DEFER a spawn, so a false positive parks a lane for one stale
|
|
2720
|
+
window while a false negative is the double-spawn it exists to
|
|
2721
|
+
prevent -- but a signal that could latch a lane closed on a bad read
|
|
2722
|
+
would be worse than the bug, so it never fails closed.
|
|
2723
|
+
|
|
2724
|
+
NOT dry-run gated: reading is free of side effects, and a dry pass has
|
|
2725
|
+
to reach the same decisions the real one would.
|
|
2726
|
+
"""
|
|
2727
|
+
if not self.config.activity_claims_enabled:
|
|
2728
|
+
return None
|
|
2729
|
+
lane = activity_lane(session)
|
|
2730
|
+
try:
|
|
2731
|
+
comment = self.github.marked_comment(
|
|
2732
|
+
owner, repo, number, ACTIVITY_MARKER
|
|
2733
|
+
)
|
|
2734
|
+
except Exception as exc:
|
|
2735
|
+
log.warning(
|
|
2736
|
+
"could not read the activity comment on %s (%s) — no external "
|
|
2737
|
+
"claim evidence this poll",
|
|
2738
|
+
slug,
|
|
2739
|
+
exc,
|
|
2740
|
+
)
|
|
2741
|
+
return None
|
|
2742
|
+
if comment is None:
|
|
2743
|
+
return None
|
|
2744
|
+
|
|
2745
|
+
window = self.config.stale_minutes * 60
|
|
2746
|
+
newest: "ActivityClaim | None" = None
|
|
2747
|
+
for claim in activity_claims(comment.body):
|
|
2748
|
+
if activity_lane(claim.session) != lane:
|
|
2749
|
+
continue
|
|
2750
|
+
if claim.age >= window:
|
|
2751
|
+
continue
|
|
2752
|
+
if self.state.knows_session(claim.session):
|
|
2753
|
+
# This daemon's OWN past spawn, read back off the shared
|
|
2754
|
+
# record. Its ledger row is the authoritative clock for it and
|
|
2755
|
+
# the matrix above has already weighed that row; treating the
|
|
2756
|
+
# line as foreign evidence would let the daemon defer behind
|
|
2757
|
+
# itself -- and, because the line never ages differently from
|
|
2758
|
+
# the row it was written beside, it could only ever say what
|
|
2759
|
+
# the ledger just said. EXTERNAL means "a spawn this ledger has
|
|
2760
|
+
# no memory of", which is exactly the ledger-loss, volume-
|
|
2761
|
+
# handoff and overlapping-deploy cases this exists for.
|
|
2762
|
+
continue
|
|
2763
|
+
if newest is None or claim.at > newest.at:
|
|
2764
|
+
newest = claim
|
|
2765
|
+
return newest
|
|
2766
|
+
|
|
2767
|
+
def _claim_deferral(
|
|
2768
|
+
self, claim: "ActivityClaim", attempts: int, round_: "int | None" = None
|
|
2769
|
+
) -> Decision:
|
|
2770
|
+
"""The IN_FLIGHT decision an external claim produces. IN_FLIGHT, not
|
|
2771
|
+
SKIPPED or DEFERRED: the work IS in flight, just under another
|
|
2772
|
+
instance's ledger, and like every other in-flight return this one
|
|
2773
|
+
claims nothing locally -- no assignment, no ledger row -- so the item
|
|
2774
|
+
stays a clean candidate the moment the claim ages out."""
|
|
2775
|
+
return Decision(
|
|
2776
|
+
Action.IN_FLIGHT,
|
|
2777
|
+
f"another daemon instance recorded a spawn on this lane "
|
|
2778
|
+
f"{int(claim.age / 60)} min ago (`{claim.session}`, on the "
|
|
2779
|
+
f"mechanical activity comment) and the local ledger has no fresh "
|
|
2780
|
+
f"row — external claim evidence; not double-spawning",
|
|
2781
|
+
attempts,
|
|
2782
|
+
session=claim.session,
|
|
2783
|
+
round=round_,
|
|
2784
|
+
)
|
|
2785
|
+
|
|
2452
2786
|
def _log_deferral(
|
|
2453
2787
|
self,
|
|
2454
2788
|
owner: str,
|
|
@@ -236,6 +236,31 @@ class State:
|
|
|
236
236
|
).fetchone()
|
|
237
237
|
return None if row is None else row["session"]
|
|
238
238
|
|
|
239
|
+
def knows_session(self, session: str) -> bool:
|
|
240
|
+
"""Has THIS ledger ever recorded a spawn under this session name?
|
|
241
|
+
|
|
242
|
+
The question the cross-instance claim check asks of every spawn line it
|
|
243
|
+
reads on an activity comment (loop.DevWatcher.external_claim). The
|
|
244
|
+
comment is a shared record: a spawn line is only EXTERNAL evidence when
|
|
245
|
+
this instance has no memory of writing it -- otherwise it is this
|
|
246
|
+
daemon's own past spawn, whose ledger row already carries the
|
|
247
|
+
authoritative clock, and reading it back as a foreign claim would let
|
|
248
|
+
the daemon defer behind itself.
|
|
249
|
+
|
|
250
|
+
Deliberately NOT edge- or artifact-scoped: session names are globally
|
|
251
|
+
unique by construction (each edge's builder folds in its own prefix,
|
|
252
|
+
the owner/repo, the number, and the attempt), so one lookup across the
|
|
253
|
+
three spawn ledgers answers it for every edge without a caller having
|
|
254
|
+
to say which one it is asking about.
|
|
255
|
+
"""
|
|
256
|
+
row = self._db.execute(
|
|
257
|
+
"SELECT 1 AS hit FROM spawns WHERE session=? "
|
|
258
|
+
"UNION ALL SELECT 1 FROM fix_spawns WHERE session=? "
|
|
259
|
+
"UNION ALL SELECT 1 FROM maintain_spawns WHERE session=? LIMIT 1",
|
|
260
|
+
(session, session, session),
|
|
261
|
+
).fetchone()
|
|
262
|
+
return row is not None
|
|
263
|
+
|
|
239
264
|
def record_fix_spawn(
|
|
240
265
|
self, *, repo_slug: str, number: int, round_: int, attempt: int, session: str
|
|
241
266
|
) -> None:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.6.2
|
|
@@ -324,6 +324,7 @@ class Sources:
|
|
|
324
324
|
"fix_rounds_enabled": c.fix_rounds_enabled,
|
|
325
325
|
"maintain_enabled": c.maintain_enabled,
|
|
326
326
|
"resume_enabled": c.resume_enabled,
|
|
327
|
+
"activity_claims_enabled": c.activity_claims_enabled,
|
|
327
328
|
"reviewers": list(c.reviewers),
|
|
328
329
|
"agent_profile": c.agent_profile,
|
|
329
330
|
"state_db": str(c.state_db),
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.6.0
|
|
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
|