personal memory agent
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add next_run, duration_seconds, and max_age_seconds to status events

Enrich supervisor.status with next_run (epoch ms) on each schedule entry,
and observe.status with running.duration_seconds and per-handler
max_age_seconds for the health dashboard.

+82
+10
observe/sense.py
··· 685 685 "file": rel_file, 686 686 "ref": handler_proc.managed.ref, 687 687 } 688 + handler_status["running"]["duration_seconds"] = int( 689 + now - handler_proc.started_at 690 + ) 688 691 689 692 # Queued items with age 690 693 if handler_queue.queue_size() > 0: ··· 699 702 {"file": rel_file, "age_seconds": int(now - item.queued_at)} 700 703 ) 701 704 handler_status["queued"] = queued_list 705 + 706 + if handler_queue.queue_size() > 0: 707 + handler_status["max_age_seconds"] = int( 708 + now - min(item.queued_at for item in handler_queue.queue) 709 + ) 710 + elif handler_status: 711 + handler_status["max_age_seconds"] = 0 702 712 703 713 # Add section if any activity for this handler 704 714 if handler_status:
+51
tests/test_scheduler.py
··· 950 950 assert "last_run" in status[0] 951 951 assert "due" in status[0] 952 952 953 + def test_next_run_hourly(self, journal_path): 954 + import think.scheduler as mod 955 + 956 + mod._entries = {"a": {"cmd": ["sol", "x"], "every": "hourly"}} 957 + mod._state = {"a": {"last_run": datetime(2026, 2, 17, 14, 5).timestamp()}} 958 + 959 + with _fake_now(datetime(2026, 2, 17, 14, 30)): 960 + status = mod.collect_status() 961 + 962 + expected = int(datetime(2026, 2, 17, 15, 0).timestamp() * 1000) 963 + assert status[0]["next_run"] == expected 964 + 965 + def test_next_run_daily(self, journal_path): 966 + import think.scheduler as mod 967 + 968 + mod._daily_time = "03:00" 969 + mod._entries = {"a": {"cmd": ["sol", "x"], "every": "daily"}} 970 + mod._state = {"a": {"last_run": datetime(2026, 2, 17, 3, 30).timestamp()}} 971 + 972 + with _fake_now(datetime(2026, 2, 17, 4, 0)): 973 + status = mod.collect_status() 974 + 975 + expected = int(datetime(2026, 2, 18, 3, 0).timestamp() * 1000) 976 + assert status[0]["next_run"] == expected 977 + 978 + def test_next_run_weekly(self, journal_path): 979 + import think.scheduler as mod 980 + 981 + mod._weekly_day = "sunday" 982 + mod._weekly_time = "03:00" 983 + mod._entries = {"a": {"cmd": ["sol", "x"], "every": "weekly"}} 984 + mod._state = {"a": {"last_run": datetime(2026, 3, 22, 3, 30).timestamp()}} 985 + 986 + with _fake_now(datetime(2026, 3, 22, 4, 0)): 987 + status = mod.collect_status() 988 + 989 + expected = int(datetime(2026, 3, 29, 3, 0).timestamp() * 1000) 990 + assert status[0]["next_run"] == expected 991 + 992 + def test_next_run_when_due(self, journal_path): 993 + import think.scheduler as mod 994 + 995 + mod._entries = {"a": {"cmd": ["sol", "x"], "every": "hourly"}} 996 + mod._state = {} 997 + 998 + with _fake_now(datetime(2026, 2, 17, 14, 30)): 999 + status = mod.collect_status() 1000 + 1001 + expected = int(datetime(2026, 2, 17, 14, 0).timestamp() * 1000) 1002 + assert status[0]["next_run"] == expected 1003 + 953 1004 954 1005 class TestHeartbeatSchedule: 955 1006 """Tests for heartbeat schedule registration and daily firing."""
+21
think/scheduler.py
··· 473 473 "last_run": last_run, 474 474 "due": _is_due(entry, state_entry, now), 475 475 } 476 + entry_status["next_run"] = _compute_next_run(entry, state_entry, now) 476 477 if entry["every"] == "daily" and _daily_time: 477 478 entry_status["daily_time"] = _daily_time 478 479 if entry["every"] == "weekly": ··· 482 483 entry_status["weekly_time"] = _weekly_time 483 484 result.append(entry_status) 484 485 return result 486 + 487 + 488 + def _compute_next_run(entry: dict, state_entry: dict | None, now: datetime) -> int: 489 + """Compute next run time as epoch milliseconds.""" 490 + every = entry["every"] 491 + if every == "hourly": 492 + mark = _hour_mark(now) 493 + nxt = mark if _is_due(entry, state_entry, now) else mark + timedelta(hours=1) 494 + elif every == "daily": 495 + mark = _compute_daily_mark(now, _daily_time) 496 + nxt = mark if _is_due(entry, state_entry, now) else mark + timedelta(days=1) 497 + elif every == "weekly": 498 + weekly_day_val = _parse_weekly_day(_weekly_day) 499 + if weekly_day_val is None: 500 + weekly_day_val = 6 501 + mark = _compute_weekly_mark(now, weekly_day_val, _weekly_time) 502 + nxt = mark if _is_due(entry, state_entry, now) else mark + timedelta(weeks=1) 503 + else: 504 + return int(now.timestamp() * 1000) 505 + return int(nxt.timestamp() * 1000) 485 506 486 507 487 508 # ---------------------------------------------------------------------------