Skip to content

feat: Add heartbeat control panel to console#381

Merged
xieyxclack merged 3 commits intoagentscope-ai:mainfrom
rayrayraykk:weirui/dev/console_heartbeat
Mar 2, 2026
Merged

feat: Add heartbeat control panel to console#381
xieyxclack merged 3 commits intoagentscope-ai:mainfrom
rayrayraykk:weirui/dev/console_heartbeat

Conversation

@rayrayraykk
Copy link
Copy Markdown
Member

Description

[Describe what this PR does and why]

Related Issue: Fixes #(issue_number) or Relates to #(issue_number)

Security Considerations: [If applicable, e.g. channel auth, env/config handling]

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Refactoring

Component(s) Affected

  • Core / Backend (app, agents, config, providers, utils, local_models)
  • Console (frontend web UI)
  • Channels (DingTalk, Feishu, QQ, Discord, iMessage, etc.)
  • Skills
  • CLI
  • Documentation (website)
  • Tests
  • CI/CD
  • Scripts / Deploy

Checklist

  • Pre-commit hooks pass (pre-commit run --all-files or CI)
  • Tests pass locally (pytest or as relevant)
  • Documentation updated (if needed)
  • Ready for review

Testing

[How to test these changes]

Additional Notes

[Optional: any other context]

Copilot AI review requested due to automatic review settings March 2, 2026 12:51
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Copy link
Copy Markdown
Member

@xieyxclack xieyxclack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@xieyxclack xieyxclack merged commit 0f9032f into agentscope-ai:main Mar 2, 2026
3 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Heartbeat control panel to the web console and wires it to backend config + scheduler so heartbeat can be viewed/updated and hot-reloaded without restarting the service.

Changes:

  • Add console Heartbeat page, navigation entry, and i18n strings.
  • Add backend /config/heartbeat GET/PUT endpoints and request schema.
  • Add heartbeat enable flag + hot-reload support (CronManager reschedule + ConfigWatcher detection) and adjust default heartbeat interval.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/copaw/constant.py Changes default heartbeat interval constant to 6h.
src/copaw/config/watcher.py Extends config watcher to detect heartbeat changes and trigger cron reschedule; refactors channel reload logic.
src/copaw/config/config.py Adds enabled flag to HeartbeatConfig.
src/copaw/config/init.py Re-exports HeartbeatConfig.
src/copaw/app/routers/schemas_config.py Adds request body schema for heartbeat updates.
src/copaw/app/routers/config.py Adds /config/heartbeat GET/PUT endpoints and hot-reschedule hook.
src/copaw/app/crons/manager.py Schedules heartbeat only when enabled; adds reschedule_heartbeat().
src/copaw/app/_app.py Wires CronManager into ConfigWatcher for heartbeat hot-reload.
console/src/pages/Control/Heartbeat/index.tsx Adds Heartbeat settings form and API integration.
console/src/pages/Control/Heartbeat/index.module.less Styles for Heartbeat page layout.
console/src/locales/zh.json Adds nav + heartbeat translations (ZH).
console/src/locales/en.json Adds nav + heartbeat translations (EN).
console/src/layouts/Sidebar.tsx Adds Heartbeat menu item and route mapping.
console/src/layouts/MainLayout/index.tsx Adds /heartbeat route to render the new page.
console/src/layouts/Header.tsx Adds header label mapping for Heartbeat page.
console/src/api/types/index.ts Exports new heartbeat API types.
console/src/api/types/heartbeat.ts Adds HeartbeatConfig + ActiveHoursConfig TS types.
console/src/api/modules/heartbeat.ts Adds heartbeat API module (GET/PUT).
console/src/api/index.ts Aggregates heartbeat API into the main api export.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

"ConfigWatcher: failed to reload channel '%s'",
name,
)
setattr(new_channels, name, old_ch if old_ch else new_ch)
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On channel reload failure, setattr(new_channels, name, old_ch if old_ch else new_ch) does not reliably revert to the previous value: it treats empty dicts/falsey configs as missing and it keeps new_ch when old_ch is None, so the snapshot can record a config that failed to apply. Prefer checking old_ch is not None (and possibly deleting the attr / setting to None when it truly didn’t exist) so the watcher will retry correctly on the next change.

Suggested change
setattr(new_channels, name, old_ch if old_ch else new_ch)
if old_ch is not None:
# Restore the previous channel config in the snapshot
setattr(new_channels, name, old_ch)
else:
# No previous config existed: remove the failed config
extra = getattr(new_channels, "__pydantic_extra__", None)
if isinstance(extra, dict) and name in extra:
del extra[name]
elif hasattr(new_channels, name):
delattr(new_channels, name)

Copilot uses AI. Check for mistakes.
Comment on lines +178 to 189
if (
self._cron_manager is not None
and new_hb_hash != self._last_heartbeat_hash
):
self._last_heartbeat_hash = new_hb_hash
try:
old_channel = await self._channel_manager.get_channel(name)
if old_channel is None:
logger.warning(
f"ConfigWatcher: channel '{name}' not found, skip",
)
continue
new_channel = old_channel.clone(new_ch)
await self._channel_manager.replace_channel(new_channel)
logger.info(f"ConfigWatcher: channel '{name}' reloaded")
await self._cron_manager.reschedule_heartbeat()
logger.info("ConfigWatcher: heartbeat rescheduled")
except Exception:
# Reload failed — keep old snapshot for this channel so
# the next config change will retry the reload.
logger.exception(
f"ConfigWatcher: failed to reload channel '{name}'",
"ConfigWatcher: failed to reschedule heartbeat",
)
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_last_heartbeat_hash is updated before reschedule_heartbeat() runs. If rescheduling fails, the hash stays “new” and the watcher won’t retry on subsequent polls unless the config changes again. Update the stored hash only after a successful reschedule (or restore the old hash in the exception path) to keep the system eventually consistent.

Copilot uses AI. Check for mistakes.

model_config = {"populate_by_name": True}

enabled: bool = Field(default=False, description="Whether heartbeat is on")
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding enabled with a default of False changes the effective default behavior: when agents.defaults.heartbeat is missing, get_heartbeat_config() returns HeartbeatConfig() and the scheduler will treat heartbeat as disabled. If the previous behavior was “heartbeat on by default”, this is a breaking behavioral change; consider defaulting to True (or making get_heartbeat_config() return enabled=True unless explicitly disabled) and documenting the migration.

Suggested change
enabled: bool = Field(default=False, description="Whether heartbeat is on")
enabled: bool = Field(default=True, description="Whether heartbeat is on")

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +74
# Heartbeat: one interval job when enabled in config
hb = get_heartbeat_config()
interval_seconds = parse_heartbeat_every(hb.every)
self._scheduler.add_job(
self._heartbeat_callback,
trigger=IntervalTrigger(seconds=interval_seconds),
id=HEARTBEAT_JOB_ID,
replace_existing=True,
)
if getattr(hb, "enabled", True):
interval_seconds = parse_heartbeat_every(hb.every)
self._scheduler.add_job(
self._heartbeat_callback,
trigger=IntervalTrigger(seconds=interval_seconds),
id=HEARTBEAT_JOB_ID,
replace_existing=True,
)
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heartbeat interval parsing falls back to a hard-coded 30m for empty/invalid hb.every (see parse_heartbeat_every), but this PR changes HEARTBEAT_DEFAULT_EVERY to 6h. This can lead to surprising behavior where invalid input schedules far more frequently than the new default; consider aligning the fallback with HEARTBEAT_DEFAULT_EVERY (or validating every at write-time).

Copilot uses AI. Check for mistakes.
Comment thread src/copaw/app/_app.py
Comment on lines +99 to +103
# --- config file watcher (channels + heartbeat hot-reload on change) ---
config_watcher = ConfigWatcher(
channel_manager=channel_manager,
cron_manager=cron_manager,
)
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR metadata marks only the Console component as affected, but this change also modifies backend defaults/scheduling (CronManager, ConfigWatcher, config models, and new /config/heartbeat endpoints). Please update the PR description/checklist to reflect the backend impact so reviewers know to validate server-side behavior and rollout considerations.

Copilot uses AI. Check for mistakes.
Comment on lines +15 to +18
import api from "../../../api";
import type { HeartbeatConfig } from "../../../api/types/heartbeat";
import { parseEvery, serializeEvery, type EveryUnit } from "./parseEvery";
import styles from "./index.module.less";
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

./parseEvery is imported but there is no parseEvery.ts/parseEvery.tsx in this directory (current folder only contains index.tsx and index.module.less). This will break the console build; add the missing module or update the import to the correct existing path.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants