@misterhuydo/sentinel 1.5.55 → 1.5.57

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.
package/.cairn/.hint-lock CHANGED
@@ -1 +1 @@
1
- 2026-04-21T06:52:59.423Z
1
+ 2026-04-21T07:25:38.460Z
@@ -1,6 +1,6 @@
1
1
  {
2
- "message": "Auto-checkpoint at 2026-04-21T06:53:13.854Z",
3
- "checkpoint_at": "2026-04-21T06:53:13.855Z",
2
+ "message": "Auto-checkpoint at 2026-04-21T07:32:04.414Z",
3
+ "checkpoint_at": "2026-04-21T07:32:04.416Z",
4
4
  "active_files": [
5
5
  "J:\\Projects\\Sentinel\\cli\\bin\\sentinel.js",
6
6
  "J:\\Projects\\Sentinel\\cli\\lib\\test.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@misterhuydo/sentinel",
3
- "version": "1.5.55",
3
+ "version": "1.5.57",
4
4
  "description": "Sentinel — Autonomous DevOps Agent installer and manager",
5
5
  "bin": {
6
6
  "sentinel": "./bin/sentinel.js"
@@ -1 +1 @@
1
- __version__ = "1.5.55"
1
+ __version__ = "1.5.57"
@@ -377,8 +377,8 @@ reply with a grouped summary like this:
377
377
  Minimum interval: 60 seconds. Allowed tools: fetch_logs, filter_logs, get_status,
378
378
  ask_logs, list_recent_commits, check_health.
379
379
  Always confirm to the user with the monitor ID and stop condition before creating.
380
- • `stop_monitor` — cancel a monitor by ID, or pass "all" to cancel all in this channel
381
- • `list_monitors` — show all active monitors
380
+ • `stop_monitor` — delete a monitor by ID (stops it if active); pass "all" to delete all in this channel
381
+ • `list_monitors` — show active monitors plus completed/cancelled ones from the last 24 hours
382
382
 
383
383
  *File sharing*
384
384
  • `post_file` — upload any output as a Slack file (logs, diffs, reports)
@@ -1350,6 +1350,9 @@ _TOOLS = [
1350
1350
  "Allowed tools: fetch_logs, filter_logs, get_status, ask_logs, list_recent_commits, check_health. "
1351
1351
  "Boss calculates stop_at from phrases like 'within 2 hours' / 'for 30 minutes' using "
1352
1352
  "the current UTC time in the system prompt. "
1353
+ "IMPORTANT: ALWAYS call list_monitors FIRST to get the live DB state before calling "
1354
+ "this tool — never infer active monitors from conversation history, as finished monitors "
1355
+ "are deleted and context window state will be stale. "
1353
1356
  "Examples: 'fetch SSOLWA logs filtered by provision/phone every 5 min for 2 hours', "
1354
1357
  "'check STS health every 10 min until I say stop', 'get status every hour for 3 times'."
1355
1358
  ),
@@ -1391,8 +1394,12 @@ _TOOLS = [
1391
1394
  {
1392
1395
  "name": "stop_monitor",
1393
1396
  "description": (
1394
- "Cancel a running monitor by ID. Pass 'all' to cancel every active monitor in this channel. "
1395
- "Use for: 'stop monitor m-abc123', 'stop all monitors', 'cancel the log watch'."
1397
+ "Delete a monitor by ID removes it from the list immediately. "
1398
+ "Works on any monitor regardless of status (active, done, or cancelled). "
1399
+ "For active monitors this also stops it from running. "
1400
+ "Pass 'all' to delete every monitor in this channel. "
1401
+ "Use for: 'delete monitor m-abc123', 'stop monitor m-abc123', 'delete all monitors', "
1402
+ "'clear monitors', 'cancel the log watch'."
1396
1403
  ),
1397
1404
  "input_schema": {
1398
1405
  "type": "object",
@@ -1408,8 +1415,9 @@ _TOOLS = [
1408
1415
  {
1409
1416
  "name": "list_monitors",
1410
1417
  "description": (
1411
- "List all active scheduled monitors. "
1412
- "Use for: 'what monitors are running?', 'show scheduled tasks', 'list active monitors'."
1418
+ "List all monitors — active ones plus completed/cancelled ones from the last 24 hours. "
1419
+ "Use for: 'what monitors are running?', 'show scheduled tasks', 'list monitors', "
1420
+ "'show recent monitors'. Each entry includes status (active/done/cancelled)."
1413
1421
  ),
1414
1422
  "input_schema": {"type": "object", "properties": {}},
1415
1423
  },
@@ -3586,32 +3594,33 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
3586
3594
  if not mon_id:
3587
3595
  return json.dumps({"error": "monitor_id is required"})
3588
3596
  if mon_id.lower() == "all":
3589
- count = store.cancel_all_monitors(channel=channel)
3590
- return json.dumps({"cancelled": count, "message": f"Cancelled {count} active monitor(s)."})
3591
- ok = store.cancel_monitor(mon_id)
3597
+ count = store.delete_all_monitors(channel=channel)
3598
+ return json.dumps({"deleted": count, "message": f"Deleted {count} monitor(s)."})
3599
+ ok = store.delete_monitor(mon_id)
3592
3600
  if ok:
3593
- return json.dumps({"status": "cancelled", "monitor_id": mon_id})
3594
- return json.dumps({"error": f"Monitor '{mon_id}' not found or already stopped."})
3601
+ return json.dumps({"status": "deleted", "monitor_id": mon_id})
3602
+ return json.dumps({"error": f"Monitor '{mon_id}' not found."})
3595
3603
 
3596
3604
  if name == "list_monitors":
3597
3605
  monitors = store.list_active_monitors()
3598
3606
  if not monitors:
3599
- return json.dumps({"monitors": [], "message": "No active monitors."})
3607
+ return json.dumps({"monitors": [], "message": "No monitors in the last 24 hours."})
3600
3608
  result = []
3601
3609
  for _m in monitors:
3602
3610
  _runs_left = None
3603
3611
  if _m.get("max_runs"):
3604
3612
  _runs_left = _m["max_runs"] - _m["runs_so_far"]
3605
3613
  result.append({
3606
- "id": _m["id"],
3607
- "name": _m.get("name") or "",
3608
- "status": _m["status"],
3609
- "interval": _format_duration(_m["interval_seconds"]),
3610
- "runs_so_far": _m["runs_so_far"],
3611
- "runs_left": _runs_left,
3612
- "next_run_at": _m.get("next_run_at") or "",
3613
- "stop_at": _m.get("stop_at") or "",
3614
- "steps": json.loads(_m.get("steps_json") or "[]"),
3614
+ "id": _m["id"],
3615
+ "name": _m.get("name") or "",
3616
+ "status": _m["status"],
3617
+ "interval": _format_duration(_m["interval_seconds"]),
3618
+ "runs_so_far": _m["runs_so_far"],
3619
+ "runs_left": _runs_left,
3620
+ "next_run_at": _m.get("next_run_at") or "",
3621
+ "stop_at": _m.get("stop_at") or "",
3622
+ "completed_at": _m.get("completed_at") or "",
3623
+ "steps": json.loads(_m.get("steps_json") or "[]"),
3615
3624
  })
3616
3625
  return json.dumps({"monitors": result})
3617
3626
 
@@ -450,8 +450,14 @@ class StateStore:
450
450
  "channel TEXT, "
451
451
  "user_id TEXT, "
452
452
  "status TEXT DEFAULT 'active', "
453
+ "completed_at TEXT, "
453
454
  "created_at TEXT)"
454
455
  )
456
+ # Migration: add completed_at to existing tables
457
+ try:
458
+ conn.execute("ALTER TABLE monitors ADD COLUMN completed_at TEXT")
459
+ except Exception:
460
+ pass
455
461
 
456
462
  def create_monitor(self, id: str, name: str, steps_json: str,
457
463
  interval_seconds: int, stop_at, max_runs,
@@ -486,10 +492,21 @@ class StateStore:
486
492
  return dict(row) if row else None
487
493
 
488
494
  def list_active_monitors(self) -> list[dict]:
495
+ from datetime import datetime, timezone, timedelta
496
+ now = datetime.now(timezone.utc)
497
+ cutoff = (now - timedelta(hours=24)).isoformat()
489
498
  with self._conn() as conn:
490
499
  self._ensure_monitors_table(conn)
500
+ # Auto-clean completed/done monitors older than 24 h
501
+ conn.execute(
502
+ "DELETE FROM monitors WHERE status != 'active' AND completed_at < ?",
503
+ (cutoff,),
504
+ )
491
505
  rows = conn.execute(
492
- "SELECT * FROM monitors WHERE status = 'active' ORDER BY created_at DESC"
506
+ "SELECT * FROM monitors "
507
+ "WHERE status = 'active' OR (status != 'active' AND completed_at >= ?) "
508
+ "ORDER BY created_at DESC",
509
+ (cutoff,),
493
510
  ).fetchall()
494
511
  return [dict(r) for r in rows]
495
512
 
@@ -505,7 +522,12 @@ class StateStore:
505
522
  with self._conn() as conn:
506
523
  self._ensure_monitors_table(conn)
507
524
  if done:
508
- conn.execute("DELETE FROM monitors WHERE id = ?", (id,))
525
+ # Keep done monitors visible for 24 h; user can delete explicitly via stop_monitor
526
+ conn.execute(
527
+ "UPDATE monitors SET runs_so_far = runs_so_far + 1, last_run_at = ?, "
528
+ "status = 'done', completed_at = ? WHERE id = ?",
529
+ (_now(), _now(), id),
530
+ )
509
531
  else:
510
532
  conn.execute(
511
533
  "UPDATE monitors SET runs_so_far = runs_so_far + 1, last_run_at = ?, "
@@ -513,26 +535,31 @@ class StateStore:
513
535
  (_now(), next_run_at, id),
514
536
  )
515
537
 
516
- def cancel_monitor(self, id: str) -> bool:
538
+ def delete_monitor(self, id: str) -> bool:
539
+ """Delete any monitor regardless of status (cancel + remove for active ones)."""
517
540
  with self._conn() as conn:
518
541
  self._ensure_monitors_table(conn)
519
- cur = conn.execute(
520
- "DELETE FROM monitors WHERE id = ? AND status = 'active'", (id,)
521
- )
542
+ cur = conn.execute("DELETE FROM monitors WHERE id = ?", (id,))
522
543
  return cur.rowcount > 0
523
544
 
524
- def cancel_all_monitors(self, channel: str = "") -> int:
545
+ def delete_all_monitors(self, channel: str = "") -> int:
546
+ """Delete all monitors in a channel (or globally if channel is empty)."""
525
547
  with self._conn() as conn:
526
548
  self._ensure_monitors_table(conn)
527
549
  if channel:
528
- cur = conn.execute(
529
- "DELETE FROM monitors WHERE status = 'active' AND channel = ?",
530
- (channel,),
531
- )
550
+ cur = conn.execute("DELETE FROM monitors WHERE channel = ?", (channel,))
532
551
  else:
533
- cur = conn.execute("DELETE FROM monitors WHERE status = 'active'")
552
+ cur = conn.execute("DELETE FROM monitors")
534
553
  return cur.rowcount
535
554
 
555
+ def cancel_monitor(self, id: str) -> bool:
556
+ """Alias for delete_monitor — kept for compatibility."""
557
+ return self.delete_monitor(id)
558
+
559
+ def cancel_all_monitors(self, channel: str = "") -> int:
560
+ """Alias for delete_all_monitors — kept for compatibility."""
561
+ return self.delete_all_monitors(channel)
562
+
536
563
  # ── Pending bot-message routing questions ─────────────────────────────────
537
564
 
538
565
  def _ensure_pending_routings_table(self, conn):