@misterhuydo/sentinel 1.6.14 → 1.6.15

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-04-28T09:29:24.499Z",
3
- "checkpoint_at": "2026-04-28T09:29:24.501Z",
2
+ "message": "Auto-checkpoint at 2026-04-28T09:57:09.547Z",
3
+ "checkpoint_at": "2026-04-28T09:57:09.549Z",
4
4
  "active_files": [
5
5
  "J:\\Projects\\Sentinel\\cli\\bin\\sentinel.js",
6
6
  "J:\\Projects\\Sentinel\\cli\\lib\\test.js"
package/lib/generate.js CHANGED
@@ -45,8 +45,15 @@ if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
45
45
  exit 0
46
46
  fi
47
47
 
48
- # Kill any orphaned sentinel processes for this project (stale PIDs not in PID file)
49
- pkill -f "sentinel.main --config $DIR/config" 2>/dev/null || true
48
+ # Kill any orphaned sentinel.main processes whose cwd is this project dir.
49
+ # Match by /proc/PID/cwd (immune to relative-vs-absolute --config arg) so we
50
+ # don't leak duplicate workers when watchdog spawns over a still-alive worker.
51
+ for _pid in $(pgrep -f 'sentinel\\.main' 2>/dev/null); do
52
+ _pcwd=$(readlink -f "/proc/$_pid/cwd" 2>/dev/null) || continue
53
+ if [[ "$_pcwd" == "$DIR" ]]; then
54
+ kill "$_pid" 2>/dev/null && echo "[sentinel] killed orphaned sentinel-main PID $_pid (cwd=$DIR)"
55
+ fi
56
+ done
50
57
  rm -f "$PID_FILE"
51
58
 
52
59
  WORKSPACE="$(dirname "$DIR")"
@@ -200,7 +207,12 @@ if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE")" 2>/dev/null; then
200
207
  echo "[sentinel] __NAME__ already running (PID $(cat "$PID_FILE"))"
201
208
  exit 0
202
209
  fi
203
- pkill -f "sentinel.main --config $DIR/config" 2>/dev/null || true
210
+ for _pid in $(pgrep -f 'sentinel\.main' 2>/dev/null); do
211
+ _pcwd=$(readlink -f "/proc/$_pid/cwd" 2>/dev/null) || continue
212
+ if [[ "$_pcwd" == "$DIR" ]]; then
213
+ kill "$_pid" 2>/dev/null && echo "[sentinel] killed orphaned sentinel-main PID $_pid (cwd=$DIR)"
214
+ fi
215
+ done
204
216
  rm -f "$PID_FILE"
205
217
  WORKSPACE="$(dirname "$DIR")"
206
218
  _claude_pro=true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.6.14",
3
+ "version": "1.6.15",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -1 +1 @@
1
- __version__ = "1.6.14"
1
+ __version__ = "1.6.15"
@@ -61,6 +61,20 @@ def _project_lock(project_name: str) -> "asyncio.Lock":
61
61
  return _project_locks[project_name]
62
62
 
63
63
 
64
+ def _restart_via_execv() -> None:
65
+ """Re-exec the current process, preserving the original `python -m sentinel.main` invocation.
66
+
67
+ sys.argv[0] under -m mode is the absolute path to main.py — running that path
68
+ directly makes Python treat the file as a script, which breaks the relative
69
+ `from .cairn_client import ...` imports. Detect -m mode and re-launch with
70
+ `[python, '-m', 'sentinel.main', *original_args]` instead.
71
+ """
72
+ if __package__ and sys.argv and sys.argv[0].endswith(("main.py", "main")):
73
+ os.execv(sys.executable, [sys.executable, "-m", f"{__package__}.main", *sys.argv[1:]])
74
+ else:
75
+ os.execv(sys.executable, [sys.executable, *sys.argv])
76
+
77
+
64
78
  def _on_sigusr1(*_):
65
79
  global _report_requested
66
80
  _report_requested = True
@@ -619,6 +633,19 @@ async def _handle_issue(event: IssueEvent, cfg_loader: ConfigLoader, store: Stat
619
633
  async def _handle_issue_locked(event, repo, cfg_loader, store):
620
634
  """Heavy work portion of _handle_issue — serialised per project via _project_lock."""
621
635
  sentinel = cfg_loader.sentinel
636
+
637
+ # Re-check dedupe AFTER acquiring the lock — the pre-lock check in
638
+ # _handle_issue is racy when two poll cycles (or two worker processes
639
+ # sharing the same sentinel.db) both see the issue file before either has
640
+ # recorded an attempt. The lock then merely serialises duplicate work.
641
+ if store.fix_attempted_recently(event.fingerprint, hours=24):
642
+ logger.info(
643
+ "Issue %s skipped (post-lock recheck) — fingerprint %s already attempted",
644
+ event.source, event.fingerprint,
645
+ )
646
+ mark_done(event.issue_file)
647
+ return None
648
+
622
649
  auto_commit = resolve_auto_commit(repo, sentinel)
623
650
  auto_release = resolve_auto_release(repo, sentinel)
624
651
 
@@ -945,7 +972,17 @@ async def _handle_issue_locked(event, repo, cfg_loader, store):
945
972
 
946
973
  except Exception:
947
974
  logger.exception("Unexpected error processing issue %s — archiving to prevent retry loop", event.source)
948
- store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
975
+ # Don't insert a 'failed' row if the apply/publish flow already recorded
976
+ # an outcome (success or otherwise) for this fingerprint — otherwise a
977
+ # post-success exception (e.g. mark_done TOCTOU) would overwrite the
978
+ # success in Boss DM aggregation that picks the most recent row.
979
+ if not store.fix_attempted_recently(event.fingerprint, hours=24):
980
+ store.record_fix(event.fingerprint, "failed", repo_name=repo.repo_name)
981
+ else:
982
+ logger.info(
983
+ "Catch-all skipped recording 'failed' for %s — fixes table already has a recent row for this fingerprint",
984
+ event.fingerprint,
985
+ )
949
986
  mark_done(event.issue_file)
950
987
  return {"submitter": getattr(event, "submitter_user_id", ""),
951
988
  "repo_name": repo.repo_name if repo else event.target_repo,
@@ -1481,7 +1518,7 @@ def _check_and_upgrade(cfg: SentinelConfig) -> bool:
1481
1518
  except Exception:
1482
1519
  pass
1483
1520
 
1484
- os.execv(sys.executable, [sys.executable] + sys.argv)
1521
+ _restart_via_execv()
1485
1522
  return True # unreachable after execv
1486
1523
 
1487
1524
 
@@ -1603,7 +1640,7 @@ async def _handle_dev_task(task, cfg_loader: ConfigLoader, store: StateStore):
1603
1640
  f"{mentions}:white_check_mark: *Patch finished* — running tests before restart...",
1604
1641
  )
1605
1642
  # Run test suite before restarting — revert if tests fail.
1606
- import os as _os, sys as _sys, subprocess as _sp
1643
+ import subprocess as _sp
1607
1644
  _loop2 = asyncio.get_event_loop()
1608
1645
  _code_dir = Path(sentinel.sentinel_dev_repo_path or ".")
1609
1646
  _venv_pytest = _code_dir / ".venv" / "bin" / "pytest"
@@ -1666,7 +1703,7 @@ async def _handle_dev_task(task, cfg_loader: ConfigLoader, store: StateStore):
1666
1703
  f"SOAK_MINUTES={_soak_mins}\n"
1667
1704
  )
1668
1705
  await asyncio.sleep(1) # let the Slack message flush
1669
- _os.execv(_sys.executable, [_sys.executable] + _sys.argv)
1706
+ _restart_via_execv()
1670
1707
  elif status == "needs_human":
1671
1708
  # Boss qualifies the raw Patch explanation before surfacing to users
1672
1709
  qualified = _boss_qualify_dev_reason(detail, sentinel)
@@ -2006,9 +2043,8 @@ async def _patch_soak_monitor(cfg_loader: ConfigLoader) -> None:
2006
2043
  f":x: *Patch soak failed* — new errors detected after patch `{patch_hash[:8]}`. "
2007
2044
  f"Reverted {'✓' if revert_ok else '(failed — check manually)'}. Restarting...")
2008
2045
  if revert_ok:
2009
- import os as _os, sys as _sys
2010
2046
  await asyncio.sleep(2)
2011
- _os.execv(_sys.executable, [_sys.executable] + _sys.argv)
2047
+ _restart_via_execv()
2012
2048
  return
2013
2049
 
2014
2050
  # Clean soak — notify