Skip to content

fix(respond): keep an unusable segmented-reply log base from dropping the reply - #10231

Merged
Soulter merged 3 commits into
AstrBotDevs:masterfrom
Lesereingrape:fix/segmented-reply-log-base-domain
Sep 27, 2026
Merged

Soulter merged 3 commits into
AstrBotDevs:masterfrom
Lesereingrape:fix/segmented-reply-log-base-domain

Conversation

@Lesereingrape

@Lesereingrape Lesereingrape commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

platform_settings.segmented_reply.log_base is a dashboard float field (it shows up when 间隔方法 / interval_method is log), and its own hint advertises the range 1.0-10.0 — astrbot/core/config/default.py:4551-4556, copied into every locale, e.g. dashboard/src/i18n/locales/en-US/features/config-metadata.json:1120 ("Base for logarithmic intervals, defaults to 2.6. Value range: 1.0-10.0.").

RespondStage.initialize read it with a bare float() and no domain check, and _calc_comp_interval computes math.log(wc + 1, self.log_base) (astrbot/core/pipeline/respond/stage.py:103). A base of exactly 1 divides by log(1) == 0, so the lower bound the UI recommends raises:

ZeroDivisionError: float division by zero
astrbot/core/pipeline/respond/stage.py:277: in process
astrbot/core/pipeline/respond/stage.py:103: ZeroDivisionError

process() awaits _calc_comp_interval at :277, outside the per-segment try at :279-290, so the exception escapes the send loop and no segment is ever sent — measured on master with a two-Plain-component chain and interval_method: log:

log_base segments delivered
2.6 (default) 2
1.0 (hint's own lower bound) 0, ZeroDivisionError
0 (cleared numeric field) 0, ValueError: math domain error

The user sees a Prepare to send ... log line and then nothing at all. A base of 0 or a negative number hits the same line through math.log's domain error, and a cleared/hand-edited non-numeric field raises ValueError: could not convert string to float: '' from initialize itself, which breaks pipeline setup for the whole instance.

Modifications / 改动点

  • Parse the log base through a new _resolve_log_base, which keeps the documented default 2.6 when the value cannot be a logarithm base (<= 0, == 1, non-finite, or unparsable) and logs why, instead of letting the send loop die. This is the same doctrine the interval field eight lines below already uses (Failed to parse the segmented-reply interval: ... → keep the default). Valid values, including the whole rest of the advertised range, are unchanged.

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

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

New tests/test_respond_stage_log_base.py (15 cases, repo style with @pytest.mark.asyncio and a SimpleNamespace context, following tests/test_rate_limit_stage.py and tests/test_qqofficial_group_message_create.py): the unusable bases fall back, the usable ones are kept, an unparsable field keeps the default instead of raising, _calc_comp_interval returns a delay at the advertised bound, and process() still delivers both segments there.

On master @ 69ae35a3 (before the change):

FAILED tests/test_respond_stage_log_base.py::test_an_unusable_log_base_falls_back_to_the_default[1]
FAILED tests/test_respond_stage_log_base.py::test_an_unusable_log_base_falls_back_to_the_default[1.0]
FAILED tests/test_respond_stage_log_base.py::test_an_unusable_log_base_falls_back_to_the_default[0]
FAILED tests/test_respond_stage_log_base.py::test_an_unusable_log_base_falls_back_to_the_default[0.0]
FAILED tests/test_respond_stage_log_base.py::test_an_unusable_log_base_falls_back_to_the_default[-2.0]
FAILED tests/test_respond_stage_log_base.py::test_an_unusable_log_base_falls_back_to_the_default[nan]
FAILED tests/test_respond_stage_log_base.py::test_an_unparsable_log_base_keeps_the_default[]
FAILED tests/test_respond_stage_log_base.py::test_an_unparsable_log_base_keeps_the_default[None]
FAILED tests/test_respond_stage_log_base.py::test_an_unparsable_log_base_keeps_the_default[abc]
FAILED tests/test_respond_stage_log_base.py::test_an_unparsable_log_base_keeps_the_default[2,6]
FAILED tests/test_respond_stage_log_base.py::test_the_interval_calculation_survives_the_advertised_lower_bound
FAILED tests/test_respond_stage_log_base.py::test_segmented_reply_is_still_delivered
12 failed, 3 passed in 6.93s

The 3 passes are the usable-base cases, which the change deliberately does not alter. After the change:

15 passed in 5.87s

Regression, Windows / Python 3.12, TESTING=true:

tests/test_respond_stage_log_base.py tests/test_qqofficial_group_message_create.py tests/test_rate_limit_stage.py tests/test_smoke.py
49 passed in 21.56s

pytest tests -q
3534 passed, 82 skipped in 356.93s (0:05:56)

Lint gate (repo-wide job, pinned version):

ruff 0.15.22 check astrbot/core/pipeline/respond/stage.py tests/test_respond_stage_log_base.py -> All checks passed!
ruff 0.15.22 format --check ... -> 2 files already formatted

Docs

No docs/ change: the field keeps its meaning, default and unit, and 1.0 now behaves like "unset" (with a log line) rather than killing delivery. If maintainers would rather reject 1.0 in the dashboard instead, the condition in _resolve_log_base is the one line to move.

Checklist / 检查清单

The boxes below are left unticked on purpose: this patch was prepared by an automated agent session working for the account owner, so the first-person statements are the owner's to confirm, not the agent's. The measured evidence each item would attest to is inline above.


📋 查看中文说明

platform_settings.segmented_reply.log_base 是 Dashboard 里「间隔方法」选 log 时出现的浮点数配置项,其提示文案(astrbot/core/config/default.py:4551-4556,四种语言均已同步)明确写着「取值范围为 1.0-10.0」。但 RespondStage.initialize 只用裸 float() 读取、未校验取值域,而 _calc_comp_interval 在 stage.py:103 计算 math.log(字数 + 1, self.log_base)。底数为 1 时 log(1) == 0,正好触发 ZeroDivisionError——也就是说提示文案推荐的范围下界会让程序崩溃。更糟的是 process() 在 :277 调用它,位置在 :279-290 的逐段 try 之外,异常直接跳出发送循环:实测两段消息链在默认底数 2.6 下发送 2 段,底数为 1.0 或 0 时发送 0 段,用户只看到一行「Prepare to send」此后再无回复。若该字段被清空或手工写成非数字,float('') 会在 initialize 阶段抛错,导致整条 pipeline 初始化失败。本 PR 新增 _resolve_log_base:当底数不可用(≤0、等于 1、非有限值或无法解析)时记录日志并回落到文档默认值 2.6,做法与其下方八行的 interval 字段完全一致;其余合法取值行为不变。新增 15 条用例,修改前 12 failed, 3 passed,修改后 15 passed;相关四文件 49 passed;Windows 全量 3534 passed, 82 skipped;ruff 0.15.22 check/format 均通过。检查清单未勾选是因为该补丁由自动化 agent 会话代为准备,第一人称声明需由账号本人确认,实测证据已附于上文。

Summary by Sourcery

Keep segmented replies deliverable by safely resolving invalid logarithmic interval bases to the default value.

Bug Fixes:

  • Prevent segmented replies from being dropped when the configured logarithmic interval base is invalid or unparsable by falling back to the documented default.

Tests:

  • Add regression coverage for invalid and valid log bases, interval calculation at the documented boundary, and delivery of all reply segments.

@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/pipeline/respond/stage.py" line_range="27" />
<code_context>
+    except (TypeError, ValueError) as e:
+        logger.error(f"Failed to parse the segmented-reply log base: {e}")
+        return DEFAULT_LOG_BASE
+    if not math.isfinite(log_base) or log_base <= 0 or log_base == 1:
+        # math.log(words, 1) raises ZeroDivisionError and a non-positive base
+        # raises ValueError; both escape from _calc_comp_interval before the
</code_context>
<issue_to_address>
**issue (bug_risk):** A finite logarithm base between 0 and 1 is accepted, but `_calc_comp_interval` returns a negative value for any nonempty plain message because `log_base(x)` is negative when `x > 1`; `asyncio.sleep` then treats the negative delay as an immediate yield, so segmented replies lose their configured delay.

**Triggers:** When a hand-edited configuration uses a base such as `0.5` and the reply contains more than zero counted words.

**Suggested fix:** Treat `log_base <= 1` as unusable and fall back to `DEFAULT_LOG_BASE`, since the documented interval range starts at 1.0 and delays must be non-negative.

```suggestion
    if not math.isfinite(log_base) or log_base <= 1:
```
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 1 finding to address first, and for an invalid configured base, this changes behavior from dropping the reply to sending segmented messages using the default interval. If that fallback is wrong, reverting prevents future sends but cannot undo messages already delivered externally.

Blocking findings: astrbot/core/pipeline/respond/stage.py:27


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

Comment thread astrbot/core/pipeline/respond/stage.py Outdated

@kilisamemarisaaa kilisamemarisaaa 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.

I independently checked the new validation boundary in _resolve_log_base.

A finite base between 0 and 1 is still accepted, for example log_base=0.5. For any non-empty plain component, _calc_comp_interval computes math.log(words + 1, 0.5), which is negative. process() passes that value to asyncio.sleep(); asyncio treats the negative delay as an immediate yield, so segmented replies lose the configured pacing while the setting is silently accepted.

Please classify 0 < base < 1 as unusable and fall back to DEFAULT_LOG_BASE (the proposed condition can be not math.isfinite(log_base) or log_base <= 1). A focused regression should assert the fallback and a non-negative interval for log_base=0.5.

Soulter and others added 2 commits September 27, 2026 11:10
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>

@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.

Sourcery assessment

Approved.

@Soulter
Soulter merged commit 14835b5 into AstrBotDevs:master Sep 27, 2026
22 checks passed
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