`runtime-options.buildRuntimeConfigOptionPairs` translated
`AcpSessionRuntimeOptions.timeoutSeconds` into a
`session/set_config_option(configId: "timeout")` pair on every turn. Both the
control plane (`AcpSessionManager.applyManagerRuntimeControls`) and the ACPX
wrapper (`AcpxRuntime.setConfigOption`) sit between that pair and the backend:
- The control plane validates pairs against the backend's advertised
config-option keys and throws `ACP_BACKEND_UNSUPPORTED_CONTROL` for any
pair the backend did not advertise. claude-agent-acp does not advertise a
`timeout` alias.
- The wrapper then forwards remaining pairs to the delegate. The Codex ACP
command was already short-circuited there; every other command, including
claude-agent-acp, fell through.
Net effect on the reporter's scenario:
`sessions_spawn({ runtime:"acp", agentId:"claude", timeoutSeconds: 60 })`
failed at the control-plane validation with `ACP_BACKEND_UNSUPPORTED_CONTROL`
(and, had it reached the wire, claude-agent-acp would have answered
`-32603 Internal error / Unknown config option: timeout`, surfacing as
`ACP_TURN_FAILED: Internal error`).
Fix two layers:
1. Control plane (`src/acp/control-plane/runtime-options.ts`): add
`isTimeoutConfigOptionAdvertised(advertisedConfigOptionKeys)` and gate the
timeout pair on it. When advertised keys are unknown (`undefined` or
empty), keep emitting the pair — this preserves current behavior for
backends that have not produced a capability list yet. When advertised
keys are present but exclude every alias in
`RUNTIME_CONFIG_OPTION_ALIASES.timeoutSeconds`, skip the pair. The
per-turn timeout is still enforced in-process via
`AcpSessionManager.resolveTurnTimeoutMs` in `manager.core.ts`.
2. ACPX wrapper (`extensions/acpx/src/runtime.ts`): hoist the Codex
`timeout` / `timeout_seconds` suppression so it also applies to
claude-agent-acp commands. Add `isClaudeAcpCommand` mirroring
`isCodexAcpCommand` (package spec, binary, generated wrapper script).
This layer is defense in depth — relevant when callers reach the wrapper
without going through `applyManagerRuntimeControls`, or when advertised
keys are not yet known.
Coverage:
- `src/acp/control-plane/runtime-options.test.ts` (new) asserts:
- the timeout pair is omitted when advertised keys exclude every alias,
- the pair is kept when `timeout` or `timeout_seconds` is advertised,
- the pair is kept when advertised keys are unknown,
- model/thinking emission is unaffected.
- `extensions/acpx/src/runtime.test.ts` flips the previous
`forwards timeout config controls for non-Codex ACP agents` test, which
codified the buggy behavior, into a suppression assertion. Adds a
positive `still forwards non-timeout config controls for claude-agent-acp`
test and an `isClaudeAcpCommand` detector test.
Closes#81127
Builds on the prior commit by introducing the typed surfaces the rest of
the plugin (and `openclaw doctor`-style consumers) can reuse:
- `inspectTelegramUpdateOffset` returns a discriminated union
(`absent | valid | rotated`) so callers can act on the rotation event
without re-implementing the bot-id / fingerprint comparison.
`readTelegramUpdateOffset` is now a thin adapter over it.
- `TelegramOffsetRotationReason` is exported as a named type alias so
downstream code can switch over it exhaustively.
- New `TelegramOffsetRotationHandler` class encapsulates the
"log warning + delete stale file" side effect that the monitor needs at
startup, plus a `createTelegramOffsetRotationHandler` factory and a
pure `formatTelegramOffsetRotationMessage` helper used to keep the
wording consistent.
- `monitor.ts` now constructs the handler once per polling startup
instead of inlining the closure, and the new surfaces are re-exported
through `monitor-polling.runtime.ts`.
Unit coverage:
pnpm test extensions/telegram/src/update-offset-store.test.ts \
extensions/telegram/src/offset-rotation-handler.test.ts \
extensions/telegram/src/monitor.test.ts
Closes#80653.
Persist a non-reversible SHA-256 fingerprint of the bot token alongside the
bot id in the long-poll update offset store (version 3). On read, treat the
persisted offset as stale when the fingerprint diverges from the current
token, even when the bot id still matches. This covers the BotFather
`/revoke` case where the bot id is unchanged but the secret rotates -- the
in-process update tracker would otherwise silently skip any new updates
whose `update_id` is `<=` the restored watermark.
The legacy v2 (bot-id-only) layout still parses, and offsets are preserved
when the bot id matches so existing installs don't lose a watermark on
upgrade; the next persistence upgrades the file to v3 and enables rotation
detection going forward.
`readTelegramUpdateOffset` now reports each rotation through a new
`onRotationDetected` callback. `monitor.ts` uses it to log a clear warning
naming the previous/new bot id and the discarded offset, and to delete the
stale file rather than waiting for the first update to overwrite it.
Acceptance suites pass:
pnpm test extensions/telegram/src/update-offset-store.test.ts \
extensions/telegram/src/bot-update-tracker.test.ts \
extensions/telegram/src/monitor.test.ts \
extensions/telegram/src/bot.create-telegram-bot.test.ts \
extensions/telegram/src/token.test.ts \
extensions/telegram/src/polling-lease.test.ts