alissa-tools-github-devloop 0.8.60__py3-none-any.whl → 0.8.61__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2274,7 +2274,9 @@ ESCALATION_LIVE_STALLED = "live-stalled"
2274
2274
  # assignment. Once-ever like ESCALATION_ASSIGNMENT -- the remedy (unassign,
2275
2275
  # or leave it to whoever is working it by hand) does not change per poll.
2276
2276
  # ASSIGNED_OUTSIDE_SEEN is the episode's durable first-seen clock, kept in the
2277
- # activity_log ledger so a daemon restart does not restart the wait.
2277
+ # activity_log ledger so a daemon restart does not restart the wait, and
2278
+ # dropped by the first issue pass that no longer reaches the skip (issue
2279
+ # #204), so the next episode on the same issue starts a clock of its own.
2278
2280
  ESCALATION_ASSIGNED_OUTSIDE = "assigned-outside"
2279
2281
  ASSIGNED_OUTSIDE_SEEN = "assigned-outside-seen"
2280
2282
 
@@ -4737,6 +4739,10 @@ class DevWatcher:
4737
4739
  self._shell_streaks: "dict[str, tuple[int, int]]" = {}
4738
4740
  # The process reaper's own clock (janitor cadence, issue #186).
4739
4741
  self._orphans_at: "float | None" = None
4742
+ # The issues whose assigned-outside episode this issue pass saw
4743
+ # still running (issue #204): `_assigned_outside` adds to it, and the
4744
+ # end of `_poll_issues` ends every other episode's first-seen clock.
4745
+ self._outside_seen: "set[tuple[str, int]]" = set()
4740
4746
 
4741
4747
  # -- per-issue decision ------------------------------------------------
4742
4748
 
@@ -4856,6 +4862,13 @@ class DevWatcher:
4856
4862
  if claim is not None:
4857
4863
  return self._claim_deferral(claim, attempts)
4858
4864
 
4865
+ # What adoption put on the ledger, carried by hand for the reads below
4866
+ # that would otherwise go back to the ledger for it: a dry run writes
4867
+ # no row, so without these the dry pass judged the attempt cap
4868
+ # against 0 billed attempts and liveness against no session name at
4869
+ # all, and could SPAWN where the real pass ESCALATES (issue #204).
4870
+ adopted = 0
4871
+ adopted_session: "str | None" = None
4859
4872
  if login in issue.assignees and age is None:
4860
4873
  # Self-assigned but the ledger never spawned. Issue #200: when the
4861
4874
  # issue's own activity comment carries an EXPIRED spawn line by
@@ -4867,6 +4880,7 @@ class DevWatcher:
4867
4880
  orphans = self._orphaned_spawns(issue, session_name(issue, 1))
4868
4881
  if orphans:
4869
4882
  attempts, age = self._adopt_orphaned_spawns(issue, orphans)
4883
+ adopted, adopted_session = attempts, orphans[-1].session
4870
4884
  else:
4871
4885
  # No spawn line at all: assigned outside this daemon (by
4872
4886
  # hand, or under another workspace's ledger). Not ours to
@@ -4931,8 +4945,9 @@ class DevWatcher:
4931
4945
  )
4932
4946
  linked = [url for url, state in links if state == PR_OPEN]
4933
4947
  if linked:
4934
- row_session = self.state.newest_spawn_session(
4935
- issue.full_name, number
4948
+ row_session = (
4949
+ self.state.newest_spawn_session(issue.full_name, number)
4950
+ or adopted_session
4936
4951
  )
4937
4952
  liveness = self.session_liveness(row_session, roster)
4938
4953
  if liveness is Liveness.ALIVE and row_session:
@@ -5067,9 +5082,11 @@ class DevWatcher:
5067
5082
  # rows resolve INDETERMINATE otherwise, and since they respawn
5068
5083
  # unconditionally TODAY, deferring them forever would be a
5069
5084
  # regression dressed as caution.
5070
- live_session = self.state.newest_spawn_session(
5071
- issue.full_name, number
5072
- ) or session_name(issue, attempts)
5085
+ live_session = (
5086
+ self.state.newest_spawn_session(issue.full_name, number)
5087
+ or adopted_session
5088
+ or session_name(issue, attempts)
5089
+ )
5073
5090
  liveness = self.session_liveness(live_session, roster)
5074
5091
  # Falling through means DEAD, and DEAD here is CONFIRMED (a
5075
5092
  # successful listing without this name) -- so the retry below
@@ -5181,7 +5198,13 @@ class DevWatcher:
5181
5198
  # Billable attempts only (issue #142): an attempt refunded by a
5182
5199
  # usage limit never counts toward the cap; the numbering below still
5183
5200
  # counts it, so session names stay monotonic.
5184
- billed = self.state.attempt_count(issue.full_name, number, billable=True)
5201
+ # An adopted orphan is billable by construction (its row never carries
5202
+ # a limit stamp), and on a dry run it has no row to count: `max` lets
5203
+ # the dry pass reach the cap decision the real one does (issue #204).
5204
+ billed = max(
5205
+ self.state.attempt_count(issue.full_name, number, billable=True),
5206
+ adopted,
5207
+ )
5185
5208
  if billed >= self.config.attempt_cap:
5186
5209
  if self.state.escalated(issue.full_name, number, ESCALATION_CAP):
5187
5210
  return Decision(Action.CAPPED, "already escalated", attempts)
@@ -10263,8 +10286,10 @@ class DevWatcher:
10263
10286
  Rows are numbered 1..k in line order (the ledger has none for this
10264
10287
  issue), keeping the session name the line carries; numbering by the
10265
10288
  name's own suffix could REPLACE a row when a line is missing. Not
10266
- written on a dry run (no ledger write is), but the returned values
10267
- let the dry pass reach the same decision."""
10289
+ written on a dry run (no ledger write is); `evaluate` carries the
10290
+ returned count and the newest line's session name into the reads
10291
+ that would otherwise look for these rows (the attempt cap, the
10292
+ lane's liveness), so the dry pass reaches the same decision."""
10268
10293
  for attempt, orphan in enumerate(orphans, start=1):
10269
10294
  log.warning(
10270
10295
  "%s: adopted orphaned attempt %d from activity log (`%s`, "
@@ -10294,8 +10319,15 @@ class DevWatcher:
10294
10319
  Not silent forever (issue #200): once the skip has persisted for
10295
10320
  STALLED_DEFER_MULTIPLE stale windows (clock: the episode's durable
10296
10321
  first-seen row) it posts ONE operator comment, recorded only after
10297
- it lands so a failed post retries next poll."""
10322
+ it lands so a failed post retries next poll.
10323
+
10324
+ The clock is per EPISODE (issue #204): the issue pass ends it the
10325
+ first time it does not reach this skip for the issue -- unassigned,
10326
+ adopted, spawned, closed or unlabeled -- so a hand assignment weeks
10327
+ later waits its own full window instead of paging on its first
10328
+ poll. The page itself stays once-ever."""
10298
10329
  full_name, number = issue.full_name, issue.number
10330
+ self._outside_seen.add((full_name, number))
10299
10331
  reason = (
10300
10332
  f"assigned to {login} outside this daemon (no spawn on the "
10301
10333
  f"ledger) — unassign the issue to let the loop take it"
@@ -13803,6 +13835,7 @@ class DevWatcher:
13803
13835
  )
13804
13836
 
13805
13837
  results = []
13838
+ self._outside_seen = set()
13806
13839
  for owner, repo, number, assignees in _oldest_first(found):
13807
13840
  slug = f"{owner}/{repo}#{number}"
13808
13841
  # Defense in depth: the search is already repo-scoped, but nothing
@@ -13837,10 +13870,21 @@ class DevWatcher:
13837
13870
  log.error("%s: %s", slug, exc)
13838
13871
  # Owed: an evaluation that failed is unknown, never settled.
13839
13872
  decision = Decision(Action.SKIPPED, str(exc), owed=True)
13873
+ # Unknown is not ended either: a transient failure must
13874
+ # not restart an assigned-outside wait.
13875
+ self._outside_seen.add((f"{owner}/{repo}", number))
13840
13876
 
13841
13877
  level = logging.INFO if decision.action != Action.SKIPPED else logging.DEBUG
13842
13878
  log.log(level, "%s → %s (%s)", slug, decision.action.value, decision.reason)
13843
13879
  results.append((slug, decision))
13880
+ # Issue #204: an assigned-outside episode lasts exactly as long as
13881
+ # the skip keeps answering. Every first-seen clock this pass did not
13882
+ # reach the skip for has ended; drop it, or a later hand assignment
13883
+ # of the same issue inherits a wait that is weeks old and pages on
13884
+ # its first poll. Not reached when the pass raised (RateLimited), and
13885
+ # never on a dry run, which writes nothing.
13886
+ if not self.config.dry_run:
13887
+ self.state.prune_activity(ASSIGNED_OUTSIDE_SEEN, self._outside_seen)
13844
13888
  return results
13845
13889
 
13846
13890
  def _poll_prs(
@@ -1289,6 +1289,54 @@ class State:
1289
1289
  f"record_activity({repo_slug}#{number}, {kind})", write),
1290
1290
  )
1291
1291
 
1292
+ def prune_activity(
1293
+ self, kind: str, keep: "set[tuple[str, int]]"
1294
+ ) -> int:
1295
+ """Drop every activity_log row of exactly this KIND whose
1296
+ (repo_slug, number) is not in `keep`. Returns how many rows were
1297
+ dropped (0 when the store refused the write).
1298
+
1299
+ For a kind whose row is an EPISODE's first-seen clock rather than a
1300
+ posted line's dedupe (issue #204: `assigned-outside-seen`): the
1301
+ caller passes the subjects whose episode it saw still running this
1302
+ pass, and every other clock of the kind ends, so a later episode on
1303
+ the same item starts its own wait instead of inheriting this one's.
1304
+
1305
+ BEST-EFFORT with the SELECT inside the closure, and a pass with
1306
+ nothing to drop reports `False`, for the reasons `prune_escalations`
1307
+ gives. A held (unwritten) row of the kind is released alongside."""
1308
+ dropped: "list[tuple[str, int]]" = []
1309
+
1310
+ def write() -> "bool | None":
1311
+ del dropped[:]
1312
+ rows = self._db.execute(
1313
+ "SELECT repo_slug, number FROM activity_log WHERE kind=?",
1314
+ (kind,),
1315
+ ).fetchall()
1316
+ dropped.extend(
1317
+ (row["repo_slug"], int(row["number"])) for row in rows
1318
+ if (row["repo_slug"], int(row["number"])) not in keep
1319
+ )
1320
+ if not dropped:
1321
+ return False
1322
+ for repo_slug, number in dropped:
1323
+ self._db.execute(
1324
+ "DELETE FROM activity_log WHERE repo_slug=? AND number=? "
1325
+ "AND kind=?",
1326
+ (repo_slug, number, kind),
1327
+ )
1328
+ self._db.commit()
1329
+ return True
1330
+
1331
+ wrote = self._best_effort(f"prune_activity({kind})", write)
1332
+ for key in [
1333
+ key for key in self._pending_gates
1334
+ if key[0] == "activity_log" and key[3] == kind
1335
+ and (key[1], key[2]) not in keep
1336
+ ]:
1337
+ self._pending_gates.pop(key, None)
1338
+ return len(dropped) if wrote else 0
1339
+
1292
1340
  # -- janitor holdouts (issue #99) ---------------------------------------
1293
1341
  #
1294
1342
  # The workspace janitor's once-per-episode memory. Every method here is
@@ -1 +1 @@
1
- 0.8.60
1
+ 0.8.61
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-devloop
3
- Version: 0.8.60
3
+ Version: 0.8.61
4
4
  Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -10,13 +10,13 @@ alissa/tools/github/devloop/hubs.py,sha256=yjnsnSHLG7GNNP8X2hILeAMV6P5n4JGc_IaAx
10
10
  alissa/tools/github/devloop/hungtests.py,sha256=TozIDydUe_Kn1m5EtGeGtYt_BrLqGznrNjikLYhxH-Q,24621
11
11
  alissa/tools/github/devloop/integration_check.py,sha256=JkFwz3JqOQBIvzszmiql-IMVGSH3lDers6mJUHh9zMM,17928
12
12
  alissa/tools/github/devloop/janitor.py,sha256=R8REoHBeY2cfW7fYJVhKr-F5z9Mjlc-vmv-z4KtvBZc,27360
13
- alissa/tools/github/devloop/loop.py,sha256=RA1gBliGUGsmcUZONWg17clzN70szohgD7dSPqumZQs,704494
13
+ alissa/tools/github/devloop/loop.py,sha256=0GhXYt-Ou6b4hCaVlW3MwBZojRpvtxgA7uTkXKMnknU,707186
14
14
  alissa/tools/github/devloop/loop_events.py,sha256=B8wq4_MMXbf1-od3pk9OHVo6F6W2lzzUZmU6GsKcCkM,38113
15
15
  alissa/tools/github/devloop/parking.py,sha256=7h3Gg2XQ0eqlvn0M6K6NeEg9xbqnlUPrZNs9gRofzxc,12410
16
16
  alissa/tools/github/devloop/proc.py,sha256=nzfsxJSHFWRQcrXmfv588KazFysv8dVQrhMPH1q5eRk,3134
17
17
  alissa/tools/github/devloop/prompts.py,sha256=VzN6ec5eF2wwg7QSCRg52WLcxo-tGbGRgQbc4HSahUc,55169
18
- alissa/tools/github/devloop/state.py,sha256=MIlE3siJjUs5YEv1-Xm-Gmur6B-Okq3M_Lu6d_zxtmA,89534
19
- alissa/tools/github/devloop/version,sha256=MQj5gu3ulgK9CxgShUYHkZ5gnQUvytZl9LkGg_h0KPs,7
18
+ alissa/tools/github/devloop/state.py,sha256=mZcKMtWKdwjRk4up74WVI_QZaOJF4JLjGWFMhx1k2FA,91591
19
+ alissa/tools/github/devloop/version,sha256=G3rr6PpxmjA80IOCaXyiYP6ce9bJP9MLMtfPh8hyo00,7
20
20
  alissa/tools/github/devloop/version.py,sha256=znuuOaitXGrBpbskNy78_rlVpFdtmKmk_3k1ACxZrl0,1418
21
21
  alissa/tools/github/devloop/webui/__init__.py,sha256=vdXYoyGYzD7BrJ5lV6XdH8Bpw_pej8-RK0hjfxop7-8,1513
22
22
  alissa/tools/github/devloop/webui/__main__.py,sha256=wORyW5AkgsniK0TZcrOrvm_P7Q4Mr-fH-4S6qj9XcgA,6137
@@ -25,10 +25,10 @@ alissa/tools/github/devloop/webui/page.py,sha256=4OD3e01ByvVM4Wo04AZyFuyOYTFEMT2
25
25
  alissa/tools/github/devloop/webui/server.py,sha256=3aGYGesjUSrPrgcpfy-oYDefBNKnAj3lGfRzwsRkv_I,27031
26
26
  alissa/tools/github/devloop/webui/sources.py,sha256=Uw_2B5QGSmW6f66WfVbQb6yLLYmSv6uuF7HNJ5OSr_I,39314
27
27
  alissa/tools/github/devloop/webui/sysinfo.py,sha256=ptlogH9alFv0vZ-pQfyUaA3w-K7LQS3tQHGv0d8Tdlk,9649
28
- alissa_tools_github_devloop-0.8.60.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
29
- alissa_tools_github_devloop-0.8.60.dist-info/licenses/NOTICE,sha256=FKstNs3uw_tQod4_1XcFLD1FUTJQXf4FC0oXJAOA_HE,260
30
- alissa_tools_github_devloop-0.8.60.dist-info/METADATA,sha256=sXX8hAM08-DQtojqLRYh46EqoEPiPnsvfMzoGc6tVfA,15973
31
- alissa_tools_github_devloop-0.8.60.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
32
- alissa_tools_github_devloop-0.8.60.dist-info/entry_points.txt,sha256=8aC7Ajzx4bu9ZRR37a40IDfuStacvZP5KsAxY1dbFKU,145
33
- alissa_tools_github_devloop-0.8.60.dist-info/top_level.txt,sha256=DodjDg-l-TWQnlxG-Vuc3G5sB4O7cWGQwe6XrVx3u-A,7
34
- alissa_tools_github_devloop-0.8.60.dist-info/RECORD,,
28
+ alissa_tools_github_devloop-0.8.61.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
29
+ alissa_tools_github_devloop-0.8.61.dist-info/licenses/NOTICE,sha256=FKstNs3uw_tQod4_1XcFLD1FUTJQXf4FC0oXJAOA_HE,260
30
+ alissa_tools_github_devloop-0.8.61.dist-info/METADATA,sha256=0xoJnkXt8Xv4qVsf_C7lFVHdGVm05j410cO-eRY29-Q,15973
31
+ alissa_tools_github_devloop-0.8.61.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
32
+ alissa_tools_github_devloop-0.8.61.dist-info/entry_points.txt,sha256=8aC7Ajzx4bu9ZRR37a40IDfuStacvZP5KsAxY1dbFKU,145
33
+ alissa_tools_github_devloop-0.8.61.dist-info/top_level.txt,sha256=DodjDg-l-TWQnlxG-Vuc3G5sB4O7cWGQwe6XrVx3u-A,7
34
+ alissa_tools_github_devloop-0.8.61.dist-info/RECORD,,