Repository navigation
fix(whatsapp): continuous sessions β two-sided history, stable ids, offline catch-up, visible errors - #781
Merged
Merged
Conversation
β¦ds, visible errors The WhatsApp frontend's persistent history held only inbound messages, its message-id counter reset every boot, a failed turn died silently in the log, and /reset destroyed the only record of the conversation. Each of these breaks the same promise from a different angle: to the person on the phone this is one long conversation, and the bot should be able to reconstruct it whenever its own context starts fresh. Outbound recording. sendContent β the single funnel every new-content send passes through (chat sends, action tools, scheduled sends, heartbeat deliveries) β now persists the bot's row alongside the inbound ones: sender id 0 (the cross-frontend assistant convention), the configured botDisplayName, media type and caption for media sends. Until now read_chat_history showed a one-sided chat and search_chat_history could never find anything the bot itself had said β the flight details it sent yesterday were unrecoverable after a reset. Id seeding. The in-memory counter restarts at 1,000,000 every boot, but the ids it hands out are also history's msg_ids, where INSERT OR IGNORE + UNIQUE(chat_id, msg_id) dedupes. On the live deployment history already holds 1000000β1000012, so the very next restart would have the first thirteen messages silently vanish from history β and a reaction aimed at an old id from history hit whatever new message reused the number. The counter is now seeded past MAX(msg_id) over wa_* chats at frontend start (LIKE with an escaped underscore β unescaped, "wa_" matches "waX..." too and seeds from foreign chats). Paging. read_chat_history advertises `before` and `offset_id`, and the core fallback handler ignored both β "go further back" handed back the same newest 30 messages no matter what the model asked, on every frontend without a platform history override (WhatsApp, native). Both cursors now work, via the existing recentBefore query and a new recentBeforeTime. Reset. performSessionReset cleared local history unconditionally. For Telegram/Discord that's a mirror wipe β the platform still has everything. For WhatsApp the local store IS the chat record, so /reset destroyed exactly what the continuity tools exist to recover. WhatsApp now passes keepHistory: a reset clears the model's session, not the conversation log, and /help says so. Errors. A failed turn logged and settled the watchdog β and sent nothing. The message was marked read (blue ticks) and then silence, which reads as being ignored. Turn failures now mirror the Telegram queue's semantics via a shared runTurnWithRecovery: a user stop settles silently, a brief transient failure gets one retry, everything else delivers friendlyMessage() into the chat. Extracted into turn-recovery.ts so the policy is unit-testable without a Baileys socket. prompts/whatsapp.md gains a Continuity section: the model's context and the chat are different things; when the sender references something not in context, search the history before asking them to repeat themselves. 14 new tests cover outbound recording (text, media caption/type, no empty rows), restart seeding (including the LIKE-escape case and monotonicity), both paging cursors plus the no-cursor and bad-date paths, and all four recovery outcomes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161F7hgHnXqHeg1fgxMe7ym
Baileys delivers messages queued while the device was offline as messages.upsert type "append" (messages-recv.js marks offline nodes so), and the handler dropped every append wholesale β anything sent to the bot during a restart simply vanished: never recorded, never answered. The comment believed append was only history sync. Appends now run through the same handleInbound gates as live messages β fromMe drops our own sends echoing back, the allowlists drop newsletter posts and strangers β and are processed as catch-up: always recorded to history (with the message's own platform timestamp, so late-recorded rows read in true order), but given a reply turn only while fresh. Past the 15-minute window the message is history-only and the next live turn reads it from there β replying to a whole night's backlog hours later reads worse than picking up naturally. The freshness policy lives in turn-recovery.ts (shouldReplyToCatchUp) with boundary tests; a missing timestamp counts as fresh, since one odd reply beats a silently dropped question. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0161F7hgHnXqHeg1fgxMe7ym
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Log audit + code trace of WhatsApp session management turned up five defects that all break the same promise from different angles: to the person on the phone this is one long conversation, and the bot should be able to reconstruct it whenever its own context starts fresh (reset, restart, overflow).
1. History only held the user's half
The single
pushMessagecall was on the inbound path.sendContentβ the funnel every outbound send passes through β remembered messages for reply-addressing but never persisted them. Soread_chat_historyshowed a one-sided chat, andsearch_chat_historycould never find anything the bot itself said: the flight details it sent yesterday were unrecoverable after a reset.Telegram/Discord don't have this problem because their
read_history/search_historyoverrides query the platform's history. WhatsApp has no platform history API β the local store is the record, and it was half-empty.Outbound sends now persist with sender id 0 (the cross-frontend assistant convention), the configured
botDisplayName, and media type/caption for media sends.2. Message ids collide after every restart β imminent silent data loss
The in-memory id counter restarts at 1,000,000 every boot; history persists;
INSERT OR IGNORE+UNIQUE(chat_id, msg_id)dedupes. The live deployment's history already holds ids 1000000β1000012 forwa_dm_353834733284β the very next restart would have the first thirteen messages silently vanish from history, and a reaction aimed at an old id from history would hit whichever new message reused the number.The counter is now seeded past
MAX(msg_id)overwa_*chats at frontend start. The LIKE prefix escapes its underscore β unescaped,wa_matcheswaXβ¦too and would seed from foreign chats (tested).3. History paging was a silent no-op
read_chat_historyadvertisesbefore(ISO date) andoffset_idβ and the core fallback handler ignored both, returning the same newest-30 window no matter what the model asked. "Go further back" did nothing on every frontend without a platform override (WhatsApp, native). Both cursors now work.4.
/resetdestroyed the only record of the conversationperformSessionResetcleared local history unconditionally β a harmless mirror-wipe on Telegram, total record destruction on WhatsApp. Reset now takeskeepHistory; WhatsApp passes it: a reset clears the model's session, not the chat log./helptext updated to match.5. Messages sent during a restart vanished; failures were invisible
append; the handler dropped every append (the comment believed it was only history sync β the Baileys source marks offline nodes as append,messages-recv.js:1432). Appends now run through the same gates as live messages (fromMe/allowlists drop echoes and newsletters) and are processed as catch-up: always recorded with the message's own timestamp, replied to only within a 15-minute freshness window β a night's backlog gets recorded, not serially answered.runTurnWithRecovery, extracted for unit testing): user stop β silent, brief transient β one retry, everything else βfriendlyMessage()in the chat.Prompt
prompts/whatsapp.mdgains a Continuity section: your context and the chat are different things; when the sender references something not in context ("the flights", "what you said yesterday"), search the history before asking them to repeat themselves.Verification
statements.generated.tsregenerated vianpm run build:sql(drift gate).sqlite3 talon.dbshowswa_dm_353834733284occupying 1000000β1000012 with the daemon un-restarted since β this PR lands before the collision does.π€ Generated with Claude Code
https://claude.ai/code/session_0161F7hgHnXqHeg1fgxMe7ym