Allow allowlisted remote channels to list, get, and update their own
command cron jobs while preserving exact channel/chat ownership checks.
Apply the same access control to remove, enable, and disable so remote
callers cannot mutate jobs outside their channel/chat scope. Keep wildcard
remote allowlist behavior constrained by exact job ownership.
Add regression tests for allowlisted, non-allowlisted, wildcard, internal,
and non-command reminder access paths.
Add tools.cron.command_allowed_remotes so remote channels can be
explicitly allowlisted for command-executing cron jobs. Entries support
channel names, channel:chat_id pairs, and a literal * to allow every
non-empty channel while preserving the default deny posture.
The existing allow_command and command_confirm checks still apply after the
channel gate. Update tests, config examples, and cron documentation for the
new option.
When handling inbound Telegram messages from forum topics, InboundContext.ChatID was set to the plain chat ID (without thread). This caused outbound messages to lose the thread routing, sending replies to the General topic instead of the specific forum topic — even though typing indicators correctly targeted the right thread because StartTyping received the compositeChatID.
The fix sets InboundContext.ChatID to compositeChatID (chatID/threadID format) for forum messages, matching the format already used for session routing and typing indicators. TopicID is still preserved as a redundant fallback for any path that strips the composite format.
Closes#3110
Add defer-recover to 11 goroutines across 4 files to prevent
ungoroutine panics from crashing the entire process:
- pkg/tools/toolloop.go: parallel tool execution
- pkg/channels/manager.go: HTTP server (x2), channel registration
- pkg/events/subscription.go: concurrent dispatch, timeout handler,
watchContext
- pkg/tools/shell.go: cmd.Wait, PTY cmd.Wait, PTY read, pipe read
Key design decisions:
- Recover handlers send fallback values to channels (shell done,
subscription done) to prevent deadlocks when the producer panics
- PTY cmd.Wait sets session.Status='error' on panic for consistency
- toolloop sets ErrorResult on panic so the LLM gets a meaningful
response instead of a nil result
- subscription.go uses log.Printf to match existing invokeHandler style
- Other files use project logger (ErrorCF) with stack traces
Refs: FIX-PLAN-0.3.0 #2
Two concurrency bugs identified in PR #2904 review:
1. Replace sync.WaitGroup with sync.Cond-based activeReqCount to avoid
the "WaitGroup is reused before previous Wait has returned" panic that
occurs when Add(1) races with a goroutine-launched Wait().
2. Make panic cleanup conditional: when runTurn panics, only delete the
session's activeTurnStates entry if it still points to our placeholder.
Previously, an unconditional delete could wipe a new message's slot
claimed between the panic and the deferred cleanup.
Unit tests prove an out-of-tree channel type becomes valid and decodable after
RegisterChannelSettings, including the full InitChannelList -> GetDecoded path that
previously failed with "unknown type".
Expose a public registration hook so external packages that register a channel
factory via channels.RegisterFactory can also register the channel's settings
struct prototype. Without this, InitChannelList rejects any channel type absent
from the private channelSettingsFactory map, making out-of-tree channels
impossible without forking. The map access is now guarded by a RWMutex.
This is additive and changes no existing behavior.
Replace silently discarded json.Marshal and json.Unmarshal errors with
explicit checks. If serialization fails, log a warning and either
return early (for the config-level marshal/unmarshal) or skip the
channel (for per-channel marshal). This prevents silent data loss
when channel configuration contains unexpected types.
The reviewer identified two bugs in the original PR:
1. PATCH /api/config leaves session.dimensions stale: LoadConfig()
derives dimensions from the old dm_scope, and the merge carries
those stale dimensions forward. ApplyDmScope() then exits early
because dimensions is already populated, causing a mismatch between
dm_scope (new) and dimensions (old).
2. Legacy/default configs omit dm_scope in GET response: configs with
explicit dimensions but no dm_scope (including DefaultConfig) return
no dm_scope field, causing the frontend to fall back to its default
('per-channel-peer'), which may not match the actual dimensions.
Fix:
- Add DeriveDmScope() to reverse-map known dimensions arrays to
dm_scope when dm_scope is empty.
- Call it in LoadConfig(), PUT handler, PATCH handler, and
ResetToDefaults() for consistent normalization.
- In PATCH handler, clear stale dimensions from the merge result when
the patch contains session.dm_scope but not session.dimensions,
allowing ApplyDmScope() to re-derive from the new scope.
- Add comprehensive unit tests for DeriveDmScope() and scope
transition scenarios.
short_retrieval.go: Check Atoi error even though regex ensures numeric input. gateway.go: Log warning when gateway config JSON is malformed instead of silently using defaults.