Add topic-specific session file isolation to fix root cause of Gemini turn validation errors.
Each Telegram topic now maintains its own conversation history file, eliminating race
conditions and message corruption during concurrent topic processing.
Changes:
1. Enhanced resolveSessionTranscriptPath() to support optional topicId parameter
- Topic ID (Telegram messageThreadId) now incorporated into session filename
- Format: sessionId.jsonl (direct chats) vs sessionId-topic-{topicId}.jsonl (topics)
- Backward compatible: topicId is optional
2. Updated reply.ts to pass MessageThreadId to session file resolution
- ctx.MessageThreadId now flows through to resolveSessionTranscriptPath()
- Automatically provides topic context for each incoming message
3. Automatic propagation through entire system
- sessionFile parameter automatically carries topic-specific path through:
- FollowupRun object (queued runs)
- runEmbeddedPiAgent() calls
- compactEmbeddedPiSession() calls
- SessionManager lifecycle (load, read, write operations)
Benefits:
✓ Complete elimination of shared .jsonl race conditions
✓ Each topic's conversation history independently cached
✓ SessionManager instances operate on isolated files
✓ No concurrent mutations of the same message history
✓ Maintains full Phase 1 turn validation as safety layer
Testing:
✓ Build succeeds with no TypeScript errors
✓ Backward compatible with non-topic sessions (direct messages)
✓ Topic ID properly extracted from Telegram messageThreadId
Expected impact:
- Gemini "function call turn" errors eliminated (root cause fixed)
- Message history corruption prevented across all topics
- Improved stability in multi-topic scenarios
- Each topic maintains independent conversation state
This completes the two-phase fix:
- Phase 1 (previous): Turn validation to suppress errors
- Phase 2 (current): Topic isolation to fix root cause
🤖 Generated with Claude Code
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
When queued messages come from different providers (Slack + Telegram),
process them individually instead of collecting into a single prompt.
This ensures each reply routes back to its originating provider.
- Add hasCrossProviderItems() to detect multi-provider queues
- Skip collect mode when cross-provider detected
- Preserve originatingChannel/originatingTo when collecting same-provider
When OriginatingChannel matches Surface (same provider), use normal
dispatcher. Only route via routeReply() when they differ, ensuring
cross-provider messages (e.g., Telegram queued while Slack active)
get routed back to their origin.
Implement reply routing based on OriginatingChannel/OriginatingTo fields.
This ensures replies go back to the provider where the message originated
instead of using the session's lastChannel.
Changes:
- Add OriginatingChannel/OriginatingTo fields to MsgContext (templating.ts)
- Add originatingChannel/originatingTo fields to FollowupRun (queue.ts)
- Create route-reply.ts with provider-agnostic router
- Update all providers (Telegram, Slack, Discord, Signal, iMessage)
to pass originating channel info
- Update reply.ts to pass originating channel to followupRun
- Update followup-runner.ts to use route-reply for originating channels
This addresses the issue where messages from one provider (e.g., Slack)
would receive replies on a different provider (e.g., Telegram) because
the queue used the last active dispatcher instead of the originating one.
- Added `isError` property to `EmbeddedPiRunResult` and reply items to indicate error states.
- Updated error handling in `runReplyAgent` to provide more informative messages for specific socket connection errors.