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.