alissa-tools-github-devloop 0.8.59__tar.gz → 0.8.61__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.
Files changed (41) hide show
  1. {alissa_tools_github_devloop-0.8.59/src/main/alissa_tools_github_devloop.egg-info → alissa_tools_github_devloop-0.8.61}/PKG-INFO +1 -1
  2. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/loop.py +63 -14
  3. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/state.py +48 -0
  4. alissa_tools_github_devloop-0.8.61/src/main/alissa/tools/github/devloop/version +1 -0
  5. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61/src/main/alissa_tools_github_devloop.egg-info}/PKG-INFO +1 -1
  6. alissa_tools_github_devloop-0.8.59/src/main/alissa/tools/github/devloop/version +0 -1
  7. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/LICENSE +0 -0
  8. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/MANIFEST.in +0 -0
  9. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/NOTICE +0 -0
  10. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/README.md +0 -0
  11. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/requirements.txt +0 -0
  12. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/setup.cfg +0 -0
  13. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/setup.py +0 -0
  14. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/__init__.py +0 -0
  15. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/__main__.py +0 -0
  16. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/alissa.py +0 -0
  17. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/alissa_client.py +0 -0
  18. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/bows.py +0 -0
  19. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/config.py +0 -0
  20. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/fleet_vitals.py +0 -0
  21. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/ghclient.py +0 -0
  22. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/hubs.py +0 -0
  23. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/hungtests.py +0 -0
  24. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/integration_check.py +0 -0
  25. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/janitor.py +0 -0
  26. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/loop_events.py +0 -0
  27. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/parking.py +0 -0
  28. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/proc.py +0 -0
  29. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/prompts.py +0 -0
  30. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/version.py +0 -0
  31. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/webui/__init__.py +0 -0
  32. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/webui/__main__.py +0 -0
  33. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/webui/auth.py +0 -0
  34. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/webui/page.py +0 -0
  35. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/webui/server.py +0 -0
  36. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/webui/sources.py +0 -0
  37. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa/tools/github/devloop/webui/sysinfo.py +0 -0
  38. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa_tools_github_devloop.egg-info/SOURCES.txt +0 -0
  39. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa_tools_github_devloop.egg-info/dependency_links.txt +0 -0
  40. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa_tools_github_devloop.egg-info/entry_points.txt +0 -0
  41. {alissa_tools_github_devloop-0.8.59 → alissa_tools_github_devloop-0.8.61}/src/main/alissa_tools_github_devloop.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-devloop
3
- Version: 0.8.59
3
+ Version: 0.8.61
4
4
  Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera
@@ -547,11 +547,16 @@ _CRITERIA_RULE = (
547
547
  )
548
548
 
549
549
  # The adopt half: a resumed or fix-round session inherits a task some other
550
- # session typed, possibly before this rule existed.
550
+ # session typed, possibly before this rule existed. No worker surface can
551
+ # edit, retype or add a criterion on an existing task (the REST API only
552
+ # creates tasks with criteria and satisfies them; the CLI has neither), so
553
+ # the clause asks for the one thing a worker can do: put each breach in front
554
+ # of the validator, who reads the delivery note.
551
555
  _CRITERIA_ADOPT = (
552
- " On a task you ADOPT, retype any criterion that breaks this rule (a fact "
553
- "typed `human` becomes `evidence`), and type any criterion you add the "
554
- "same way."
556
+ " On a task you ADOPT, leave its criteria as they are — no tool you have "
557
+ "can edit, retype or add one — and list every criterion that breaks this "
558
+ "rule, with the type it should carry (a fact typed `human` should be "
559
+ "`evidence`), under the delivery note's Judgment calls."
555
560
  )
556
561
 
557
562
  # The origin-task instruction is split in two because the downstream rule is
@@ -2269,7 +2274,9 @@ ESCALATION_LIVE_STALLED = "live-stalled"
2269
2274
  # assignment. Once-ever like ESCALATION_ASSIGNMENT -- the remedy (unassign,
2270
2275
  # or leave it to whoever is working it by hand) does not change per poll.
2271
2276
  # ASSIGNED_OUTSIDE_SEEN is the episode's durable first-seen clock, kept in the
2272
- # 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.
2273
2280
  ESCALATION_ASSIGNED_OUTSIDE = "assigned-outside"
2274
2281
  ASSIGNED_OUTSIDE_SEEN = "assigned-outside-seen"
2275
2282
 
@@ -4732,6 +4739,10 @@ class DevWatcher:
4732
4739
  self._shell_streaks: "dict[str, tuple[int, int]]" = {}
4733
4740
  # The process reaper's own clock (janitor cadence, issue #186).
4734
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()
4735
4746
 
4736
4747
  # -- per-issue decision ------------------------------------------------
4737
4748
 
@@ -4851,6 +4862,13 @@ class DevWatcher:
4851
4862
  if claim is not None:
4852
4863
  return self._claim_deferral(claim, attempts)
4853
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
4854
4872
  if login in issue.assignees and age is None:
4855
4873
  # Self-assigned but the ledger never spawned. Issue #200: when the
4856
4874
  # issue's own activity comment carries an EXPIRED spawn line by
@@ -4862,6 +4880,7 @@ class DevWatcher:
4862
4880
  orphans = self._orphaned_spawns(issue, session_name(issue, 1))
4863
4881
  if orphans:
4864
4882
  attempts, age = self._adopt_orphaned_spawns(issue, orphans)
4883
+ adopted, adopted_session = attempts, orphans[-1].session
4865
4884
  else:
4866
4885
  # No spawn line at all: assigned outside this daemon (by
4867
4886
  # hand, or under another workspace's ledger). Not ours to
@@ -4926,8 +4945,9 @@ class DevWatcher:
4926
4945
  )
4927
4946
  linked = [url for url, state in links if state == PR_OPEN]
4928
4947
  if linked:
4929
- row_session = self.state.newest_spawn_session(
4930
- issue.full_name, number
4948
+ row_session = (
4949
+ self.state.newest_spawn_session(issue.full_name, number)
4950
+ or adopted_session
4931
4951
  )
4932
4952
  liveness = self.session_liveness(row_session, roster)
4933
4953
  if liveness is Liveness.ALIVE and row_session:
@@ -5062,9 +5082,11 @@ class DevWatcher:
5062
5082
  # rows resolve INDETERMINATE otherwise, and since they respawn
5063
5083
  # unconditionally TODAY, deferring them forever would be a
5064
5084
  # regression dressed as caution.
5065
- live_session = self.state.newest_spawn_session(
5066
- issue.full_name, number
5067
- ) 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
+ )
5068
5090
  liveness = self.session_liveness(live_session, roster)
5069
5091
  # Falling through means DEAD, and DEAD here is CONFIRMED (a
5070
5092
  # successful listing without this name) -- so the retry below
@@ -5176,7 +5198,13 @@ class DevWatcher:
5176
5198
  # Billable attempts only (issue #142): an attempt refunded by a
5177
5199
  # usage limit never counts toward the cap; the numbering below still
5178
5200
  # counts it, so session names stay monotonic.
5179
- 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
+ )
5180
5208
  if billed >= self.config.attempt_cap:
5181
5209
  if self.state.escalated(issue.full_name, number, ESCALATION_CAP):
5182
5210
  return Decision(Action.CAPPED, "already escalated", attempts)
@@ -10258,8 +10286,10 @@ class DevWatcher:
10258
10286
  Rows are numbered 1..k in line order (the ledger has none for this
10259
10287
  issue), keeping the session name the line carries; numbering by the
10260
10288
  name's own suffix could REPLACE a row when a line is missing. Not
10261
- written on a dry run (no ledger write is), but the returned values
10262
- 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."""
10263
10293
  for attempt, orphan in enumerate(orphans, start=1):
10264
10294
  log.warning(
10265
10295
  "%s: adopted orphaned attempt %d from activity log (`%s`, "
@@ -10289,8 +10319,15 @@ class DevWatcher:
10289
10319
  Not silent forever (issue #200): once the skip has persisted for
10290
10320
  STALLED_DEFER_MULTIPLE stale windows (clock: the episode's durable
10291
10321
  first-seen row) it posts ONE operator comment, recorded only after
10292
- 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."""
10293
10329
  full_name, number = issue.full_name, issue.number
10330
+ self._outside_seen.add((full_name, number))
10294
10331
  reason = (
10295
10332
  f"assigned to {login} outside this daemon (no spawn on the "
10296
10333
  f"ledger) — unassign the issue to let the loop take it"
@@ -13798,6 +13835,7 @@ class DevWatcher:
13798
13835
  )
13799
13836
 
13800
13837
  results = []
13838
+ self._outside_seen = set()
13801
13839
  for owner, repo, number, assignees in _oldest_first(found):
13802
13840
  slug = f"{owner}/{repo}#{number}"
13803
13841
  # Defense in depth: the search is already repo-scoped, but nothing
@@ -13832,10 +13870,21 @@ class DevWatcher:
13832
13870
  log.error("%s: %s", slug, exc)
13833
13871
  # Owed: an evaluation that failed is unknown, never settled.
13834
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))
13835
13876
 
13836
13877
  level = logging.INFO if decision.action != Action.SKIPPED else logging.DEBUG
13837
13878
  log.log(level, "%s → %s (%s)", slug, decision.action.value, decision.reason)
13838
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)
13839
13888
  return results
13840
13889
 
13841
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alissa-tools-github-devloop
3
- Version: 0.8.59
3
+ Version: 0.8.61
4
4
  Summary: ALISSA-TOOLS-GITHUB-DEVLOOP
5
5
  Home-page: https://alissa.app
6
6
  Author: Fahera