Skip to content

fix: gaps whatsapp-cli hit — providerRef leak, store-history sync, MCP delete for everyone, stored lookup in connect - #1423

Merged
leemour merged 10 commits into
mainfrom
fix/store-history-messengers
Oct 17, 2026
Merged

leemour merged 10 commits into
mainfrom
fix/store-history-messengers

Conversation

@leemour

@leemour leemour commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

Task

Fix the cli-core gaps whatsapp-cli (wa) hit, so wa can drop its workarounds.

Why

wa is the first messenger with history: "store" (WhatsApp has no server history) and the first whose attachments carry decryption keys in providerRef. Several shared paths assumed a Telegram-shaped messenger.

What

  • SEC-54 · providerRef never reaches output. watch --jsonl, watch --events --jsonl and the bot watch --jsonl printed the adapter's live event with providerRef (WhatsApp: media path + decryption keys; MAX bot: token). Now every answer envelope (CLI JSON, JSON lines, pretty, MCP) and the watch lines drop providerRef/provider_ref at any depth (src/cli/provider-refs.ts, applied in sealed()/renderAnswer).
  • BUG-271 · store keeps attachment keys. A later copy with no or empty providerRef (edit event, re-push) keeps the stored value.
  • BUG-272 · download resolves the chat before calling the adapter, like forward does.
  • BUG-273 · no sync-first for store-history messengers. --sync-first and its bounds, MCP sync_first, the messages sync --all --background help, and the messages sync next steps in contacts and chats documents are not offered when history: "store".
  • BUG-274 · MCP messages_delete takes for_everyone, under the same service and guard as --for-everyone.
  • DEBT-126 · ConnectOptions.stored: a read-only StoredMessages lookup (message(chatId, id), newestIncoming(chatId, before?)) over the command's own store handle, given on every shared connect (withServices, withMessenger, MCP session, and so serve's relay).
  • BUG-275 · ReadState is markRead alone; markTopicRead moves to the new optional TopicReadState. MCP chats_mark_read hides topic where topics: false.
  • UX-58 · the "nothing recorded yet" error names <tool> <profile> setup first when the CLI has a setup command.
  • Test for the serve relay's version-skew warning (NEED-1406). The warning already existed (src/cli/program.ts, lock version); nothing tested it.

Changelog under Unreleased. "Changed — may break callers" covers ReadState, syncOptions(command, messenger) and download's resolved chat id.

Downstream

tg-cli, max-cli, cli-memo: nothing breaks. They have server history, so their sync options and steps stay. Their adapters still compile: tg's class has both read methods, and max uses Pick<ReadState, "markRead">. max's download resolves an id too. No downstream code reads provider_ref from output.

Testing

  • vitest run: 4925 passed. pnpm ci:quick and the lint are green.
  • Each fix has a test that fails without it: SEC-54 uses a sentinel value through watch, --events, stored reads and run logs, and also through CLI and MCP envelopes. BUG-271 has a store round trip. BUG-272, BUG-273 (CLI help and MCP schema and description), BUG-274, BUG-275 and UX-58 each have their own test. DEBT-126 runs a lookup through a real send.

🤖 Generated with Claude Code

leemour and others added 10 commits October 17, 2026 04:00
watch --jsonl, bot watch --jsonl and every answer envelope now drop
providerRef/provider_ref at any depth. An adapter keeps there what it
needs to fetch a file again: WhatsApp's media path and decryption keys,
a MAX bot's token. Every stored read already dropped it through
messageItem; live events and raw adapter answers did not.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ne (BUG-271)

The attachment upsert took provider_ref from the newer copy, so an edit
event or a re-push without keys erased the ones that fetch the file. A
null or empty providerRef now keeps the stored value; a new one still
replaces it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…BUG-272)

services.messages.download passed the chat as typed, a name or an id,
straight to the adapter; forward, edit and the other writes resolve it
first. It now resolves it the same way, and download --all hands each
message's own chat id.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ry (BUG-273)

A messenger with history: "store" (WhatsApp) has no server history:
messages sync refuses there. Its search messages, stats messages show,
rankings top, search conversations and contacts context no longer mount
--sync-first, the search help says serve keeps the archive instead of
messages sync --all --background, and contacts and chats documents
suggest no messages sync step. Over MCP the sync_first arguments are
hidden and the search, stats and conversations descriptions drop them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The MCP delete tool only deleted the owner's copy, so a messenger that
deletes only for everyone (WhatsApp refuses delete-for-me) could never
delete through MCP. for_everyone passes through to the same service and
send guard the CLI's --for-everyone uses: the delete permission, the
hourly limit and the journal.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…G-275)

ReadState now holds markRead alone, so a messenger without forum topics
(MAX, WhatsApp) implements ReadState in full instead of
Pick<ReadState, "markRead">. markTopicRead is TopicReadState, optional
on MessengerAdapter like every other group, and capability() refuses it
where absent. chats_mark_read over MCP no longer offers topic where
Messenger.topics is false, as the CLI's --topic already did.

BREAKING CHANGE: code that reads markTopicRead through the ReadState
type must use TopicReadState; implements ReadState and Pick usages
compile unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tg, max and wa all have `<tool> <profile> setup`. When the CLI mounts a
setup command, a profile with no recorded account is told to link it
with setup and then run serve or watch (a store-history messenger), or
to log in with setup or run without --offline (one with server history).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…BT-126)

ConnectOptions.stored hands an adapter what the local store holds of
its own account: message(chatId, id) and newestIncoming(chatId, before?).
WhatsApp addresses a message by its sender and whether it is the
owner's, so edits, reactions and read receipts look it up first; wa
opened a second store handle in its connect to do it. Every connect the
shared commands make now passes it over the store the command already
has: withServices' own store, and for withMessenger and the MCP session
one lazily opened handle shared with what the reads save (connectedFor).
A command run through serve borrows serve's connection and its lookup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… version

NEED-1406's skew warning was already there: serve writes its version
into the lock beside relay, and the client compares it before relaying
(src/cli/program.ts). Nothing tested it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…f fixes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant