Skip to content

fix(platform): keep the Satori heartbeat and reconnect delays from being disabled by a cleared field - #10248

Merged
Soulter merged 2 commits into
AstrBotDevs:masterfrom
Lesereingrape:fix/satori-heartbeat-floor
Sep 27, 2026
Merged

Soulter merged 2 commits into
AstrBotDevs:masterfrom
Lesereingrape:fix/satori-heartbeat-floor

Conversation

@Lesereingrape

@Lesereingrape Lesereingrape commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

satori_heartbeat_interval and satori_reconnect_delay are declared "type": "int" with no minimum in astrbot/core/config/default.py:743-752, with template defaults 10 and 5 (:533-534), and both are handed straight to asyncio.sleep — heartbeat_loop sleeps self.heartbeat_interval between two PING frames (satori_adapter.py:221), and the reconnect loop uses self.reconnect_delay as the base of its exponential backoff (:135):

self.heartbeat_interval = self.config.get("satori_heartbeat_interval", 10)
self.reconnect_delay = self.config.get("satori_reconnect_delay", 5)

dict.get only applies the default when the key is absent. A 0 reaches the constructor from the dashboard as soon as the box is cleared — toNumber() maps parseFloat('') to 0 (dashboard/src/components/shared/ConfigItemRenderer.vue:361-364) — and a string reaches it from a hand-edited data/config.json ("satori_heartbeat_interval": "30"). Both shapes are accepted today.

Measured against the real heartbeat_loop on master @ 42ad2774, with a stub WebSocket that counts frames, over a 0.2 s window:

satori_heartbeat_interval PING frames in 0.2 s heartbeat task
unset (default 10) 0 alive
0 (cleared field) 32010 alive, spinning
'10' (quoted config value) 0 dead

The 0 row is a busy loop against the upstream Satori gateway — the interval is a seconds value, so removing the pause removes the rate limit on its own protocol keep-alive. The '10' row is worse than an error: asyncio.sleep('10') raises TypeError: '<=' not supported between instances of 'str' and 'int', heartbeat_loop's own except Exception at :240-241 logs 心跳任务异常: ... and returns, so the connection stays open with no heartbeat at all and the platform only notices when the gateway drops it. A 0 satori_reconnect_delay removes the pause between reconnect attempts in the same way.

Modifications / 改动点

  • Route both fields through coerce_int_config (astrbot/core/utils/config_number.py), which is already the repo's answer for exactly this class of field: it handles bool/int/str/other, keeps a valid value, and clamps below min_value. With min_value=1 a cleared box or a negative number falls back to one second — the smallest value that is still a pause — and a quoted number like "30" is honored instead of killing the task. Valid positive values are unchanged. This mirrors what tg_adapter.py already does inline for its own restart delay (telegram_polling_restart_delay), whose guard logs the same reason: "enforcing minimum of 0.1s to avoid tight restart loops".

  • This is NOT a breaking change. / 这不是一个破坏性变更。

Screenshots or Test Results / 运行截图或测试结果

New tests/test_satori_adapter_heartbeat_floor.py (7 cases, following tests/test_weixin_oc_adapter_timeout_floor.py): the cleared, negative and fractional fields fall back, a non-numeric one keeps the documented default instead of reaching asyncio.sleep, a numeric string is honored, and valid tuned values plus the unset defaults pass through untouched.

On master @ 42ad2774 (before the change):

FAILED tests/test_satori_adapter_heartbeat_floor.py::test_cleared_fields_fall_back_to_a_positive_floor
FAILED tests/test_satori_adapter_heartbeat_floor.py::test_negative_fields_are_clamped
FAILED tests/test_satori_adapter_heartbeat_floor.py::test_a_fractional_interval_cannot_lose_the_pause
FAILED tests/test_satori_adapter_heartbeat_floor.py::test_non_numeric_fields_do_not_kill_the_platform_adapter
FAILED tests/test_satori_adapter_heartbeat_floor.py::test_a_numeric_string_is_respected
5 failed, 2 passed

With the change: 7 passed. The two that already pass are the controls (a tuned 60/20, and unset fields).

The gate is the one CI runs for these paths:

ruff format --check astrbot/core/platform/sources/satori/satori_adapter.py tests/test_satori_adapter_heartbeat_floor.py   # 2 files already formatted
ruff check astrbot/core/platform/sources/satori/satori_adapter.py tests/test_satori_adapter_heartbeat_floor.py           # All checks passed!

Two notes for the reviewer:

  • .github/copilot-instructions.md:27 says "Do not generate test files for now." I kept the test here because it is the measurement behind the table above, and tests/test_weixin_oc_adapter_timeout_floor.py shows this kind of guard test is wanted in the adapter layer — but say the word and I will drop the file and leave the two-line fix.
  • I noticed feat: 优化 satori 适配器 #6095 rewrites this adapter on the satori-python SDK. It is dirty against current master and has not been updated since 2026-04-28, so I did not build on it; if it lands, the same coerce_int_config call should be kept at whichever line ends up reading those two fields.

Disclosure: this PR was prepared, tested and submitted by an AI agent working on behalf of the account owner.

Summary by Sourcery

Ensure Satori heartbeat and reconnect timing always use safe positive integer delays.

Bug Fixes:

  • Prevent Satori heartbeat and reconnect loops from spinning or failing when configuration values are cleared, negative, fractional, or provided as strings.

Enhancements:

  • Normalize Satori timing configuration values while preserving valid positive settings and documented defaults.

Tests:

  • Add coverage for Satori timing configuration fallbacks, clamping, string values, invalid input, and valid defaults.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/platform/sources/satori/satori_adapter.py" line_range="61-66" />
<code_context>
         self.auto_reconnect = self.config.get("satori_auto_reconnect", True)
-        self.heartbeat_interval = self.config.get("satori_heartbeat_interval", 10)
-        self.reconnect_delay = self.config.get("satori_reconnect_delay", 5)
+        self.heartbeat_interval = coerce_int_config(
+            self.config.get("satori_heartbeat_interval", 10),
+            default=10,
+            min_value=1,
+            field_name="satori_heartbeat_interval",
+            source="Satori config",
+        )
+        self.reconnect_delay = coerce_int_config(
</code_context>
<issue_to_address>
**issue (bug_risk):** `coerce_int_config` raises `OverflowError` when the configuration value is positive or negative infinity, because it calls `int(value)` without catching `OverflowError`; the Satori adapter then fails during construction instead of falling back to its default.

**Triggers:** When a hand-edited JSON configuration contains `Infinity`, `-Infinity`, or a numeric value large enough for Python's JSON parser to produce an infinite float.

**Suggested fix:** Catch `OverflowError` alongside `TypeError` and `ValueError` in `coerce_int_config`, or reject non-finite numeric values before conversion.
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: astrbot/core/platform/sources/satori/satori_adapter.py:66


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +61 to +66
self.heartbeat_interval = coerce_int_config(
self.config.get("satori_heartbeat_interval", 10),
default=10,
min_value=1,
field_name="satori_heartbeat_interval",
source="Satori config",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): coerce_int_config raises OverflowError when the configuration value is positive or negative infinity, because it calls int(value) without catching OverflowError; the Satori adapter then fails during construction instead of falling back to its default.

Triggers: When a hand-edited JSON configuration contains Infinity, -Infinity, or a numeric value large enough for Python's JSON parser to produce an infinite float.

Suggested fix: Catch OverflowError alongside TypeError and ValueError in coerce_int_config, or reject non-finite numeric values before conversion.

@Soulter
Soulter merged commit c8a07f7 into AstrBotDevs:master Sep 27, 2026
22 checks passed
elecvoid243 referenced this pull request in elecvoid243/AstrBot_for_spzx Oct 11, 2026
Port 81 compatible upstream patches through fb49dfa.
Preserve local chat scrolling, agent extensions, API behavior, and release versions.
Adapt queued-request tests to local stage fields and resolve duplicate context arguments.

Defer 63aa363 (#10319): plugin Views tests/docs require the excluded Pages-to-Views migration.

Validation:
- Ruff checks, dashboard typecheck, and production build passed.
- Frontend: 1453 passed; 10 failures reproduced on the pre-integration baseline.
- Backend: 1091 passed; 2 queue/OpenAPI failures reproduced on the pre-integration baseline.
- Backend tests used SQLModel 0.0.31 and SQLAlchemy 2.0.44 in an isolated temporary environment.

Upstream commits:
5950d7c chore(deps): bump the github-actions group with 2 updates (#10088)
553096b docs: sync guides with current WebUI navigation (#10093)
763a5ee fix(cron): say who owns a task instead of returning an empty list (#10104)
e0aa8d3 fix: clarify local file access menu descriptions
2704b4c fix: tell the model when an animation becomes a frame montage (#10119)
bb79c08 fix(dashboard): drop stale exclude_ids hardcode hiding conversations (#10129)
f097600 fix(gemini): keep narration streamed before a tool call in history (#10132)
8492df4 fix: serialize Azure TTS backend client lifetimes (#10101)
0812616 fix: propagate Xinference rerank failures instead of returning empty results (#10051)
9c73295 fix: preserve scheduled reminders when an edit is invalid (#10075)
2782454 fix: use GitHub HEAD when repository branch is unspecified (#10080)
dae7c24 fix(provider): recognize thinking tags in compatible completions (#10116)
b13338d fix(dashboard): restore readable text and switch contrast in fullscreen console (#10135)
774cc94 docs: name the dashboard host/port environment variables and their precedence (#10095)
54c98e8 fix: resolve each OneBot file segment by its own ID (#10098)
b065505 fix: clean up invalid materialized images (#10090)
96d9504 docs: explain HTTP proxy address when AstrBot runs in Docker (#10162)
8b5e24b fix: respect session plugin filters in cron agents (#10163)
67c74b7 feat: gzip dashboard static js, css, and html (#10187)
2b6b31a fix: refresh queued conversation history under the session lock (#10238)
fbf5286 feat: forward extra_user_content_parts through Context.tool_loop_agent (#10237)
559b171 fix(weixin_oc): keep a cleared timeout field from disabling the HTTP timeout (#10229)
14835b5 fix(respond): keep an unusable segmented-reply log base from dropping the reply (#10231)
a426808 fix: skip queued LLM requests after disabling (#10234)
61ba3a2 fix: estimate emoji token cost above the plain text rate (#10216)
0531575 fix: increase cron misfire grace time to 300 seconds (#10183)
42ad277 chore(deps): bump the github-actions group with 4 updates (#10184)
1a59156 feat(dashboard): add search to Skills and MCP pages (#10217)
5fcaf69 fix: compare platform message history cutoff in UTC (#10246)
c8a07f7 fix(platform): keep the Satori heartbeat and reconnect delays from being disabled by a cleared field (#10248)
ae5a155 fix(dashboard): fix dark mode icons and platform template order (#10061)
e99432c fix(qqofficial): restore @ mentions in group messages (#9705)
5b04f13 perf: skip state allocation when rate limiting is disabled (#10270)
e19954b fix(stats): prevent timers from starting after unmount (#10272)
1c5dd91 perf(session): skip unused alias lookup when listing rules (#10273)
9c6b603 perf(trace): sort records only for updated spans (#10274)
b53999e fix: support Windows long paths during plugin archive extraction (#10193)
5a4745f docs: rename dev QQ group to chit-chat group and add community email (#10282)
fe29538 fix: update remaining legacy AstrBot icons (#10278)
6d50e1e chore(deps): bump github/codeql-action in the github-actions group (#10265)
ac515d9 docs: add slider key to plugin dev docs (#10289)
73b4fc9 fix: correct shields.io badge links in README files (#10290)
6a2186b fix: replace DeepWiki badge with shields.io in README files (#10299)
553b10f fix(dashboard): use the primary color for the config test-chat button (#10309)
9d4f523 feat(dashboard): edit config profiles from a side drawer in the bot editor (#10310)
9f65a01 fix: reuse logo tokens and refresh expired platform cache (#10294)
ab7de72 feat(dashboard): add log search and keyword highlighting to console (#10327)
d399090 fix(webui): use colored DeepSeek provider icon (#10332)
a6265fa fix: degrade unresolvable voice attachments instead of failing the turn (#10337)
1245979 fix(dashboard): enforce session ownership on chat send path (#10346)
44b5c1b feat(dashboard): show source badge for skills in skills list (#10339)
95af435 fix(dashboard): restore plugin README anchor navigation (#10341)
3d80dd3 fix: clean up session waiter registrations only once (#10344) (#10347)
ac4df99 fix(dashboard): align fullscreen log search with dark theme (#10351)
6a4decd fix: handle UTF-16 offsets in Telegram mentions (#10367)
f7f3c71 fix: leave Telegram bot token empty by default (#10365)
2f889d2 fix: preserve child folders when deleting persona folders (#10361)
de31bbe fix: close FishAudio TTS HTTP client after requests (#10358)
4c037c4 fix: resolve remote Slack file URLs before upload (#10355)
dfaaca1 docs: update Slack setup steps and screenshot (#10353)
3861730 fix(telegram): match bot mentions case-insensitively (#10399)
57ac1d8 fix: preserve Slack code block and quote text (#10380)
64430fa docs: sync Lark/Feishu guide with current capabilities and permissions (#10400)
c1356bb fix(dify): handle CRLF event separators in streaming responses (#10392)
900a700 fix: match command group names as whole words (#10372)
55c2dd3 feat: remember the last opened tab on the Data & Logs page (#10251)
e1d30f3 fix(provider): replace retired gemini-embedding-exp-03-07 default with gemini-embedding-001 (#10393)
2de9059 fix: avoid blocking audio format probes (#10055)
65cbe98 feat: ship astrbot CLI in Docker image for in-container password reset (#10442)
cae8f1d feat(dashboard): add distinct hints for local permission access modes (#10444)
30820a4 fix(provider): keep Anthropic tool calls whose streamed input is empty (#10432)
6b2e417 fix(provider): merge custom server tools with registered tools in Anthropic requests (#10437)
d8ef9dd fix: send Discord video components as attachments (#10423)
5994167 fix: preserve links and mentions in Slack lists (#10447)
03ce652 fix: preserve outbound Slack user mentions (#10448)
d2fb912 fix: prevent Discord self-reply loops (#10425)
b01285a chore(github): add plugin submission links to issue template config (#10455)
c6f4150 fix: avoid recording cron sends without a matching platform (#10468)
28a10c1 fix: warn when a background task result is not sent to the user (#10398)
845517e chore(github): update plugin submission links in issue template config (#10472)
fb49dfa fix(dashboard): update plugin market submit link and label (#10471)
@Soulter Soulter moved this to Done in AstrBot Roadmap Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants