Repository navigation
Conversation
The default initial sync only delivers WhatsApp's recent-activity window
(the linked-device protocol leaves older messages on the primary phone).
This command pulls them on demand by wrapping
`whatsmeow.Client.BuildHistorySyncRequest` + a peer `SendMessage`. Responses
arrive as `*events.HistorySync` with `SyncType == ON_DEMAND` and flow
through the same handler as the initial sync — no extra wiring beyond a
new entry-point and a per-chat cursor.
Surface:
whatsapp-cli history extend --chat JID [--count N] [--requests R]
whatsapp-cli history extend --all [--count N] [--requests R]
whatsapp-cli history extend --chat JID --until-stable [--max-rounds N]
whatsapp-cli history extend --all --until-stable [--max-rounds N]
`--until-stable` stops asking each chat once its oldest-message cursor
doesn't advance between consecutive requests — the strongest local signal
that the phone has no older history for that chat. `--max-rounds` is the
safety cap (default 20).
Internals:
- new `MessageStore.GetOldestMessage(chatJID)` — earliest stored message,
used as the cursor for `BuildHistorySyncRequest`
- new `WAClient.RequestMoreHistory(...)` — builds the cursor, sends as
a peer message to self via `SendMessage(..., SendRequestExtra{Peer: true})`
- `App.HistoryExtend` reuses the existing `events.HistorySync` storage
path; a 3 s pause between per-chat requests gives chunks time to land
so the next iteration sees a fresher cursor
Prior art (linked from upstream issue):
- mautrix/whatsapp `pkg/connector/backfill.go`
- openclaw/wacli `RequestHistorySyncOnDemand`
- cavit99/wa-agent-pipeline `internal/backfill/backfill.go`
Caveats (documented in --help, surfaced in stderr where relevant):
- the WhatsApp app must be open/online on the user's primary phone
- "Chat history: Paused" in Linked Devices does NOT block on-demand
requests (verified)
- single-writer constraint on the store/ directory still applies
Mocks + interface updated; existing test suite green.
|
@mikrub thanks for the thoughtful PR and the context in #20. Treating this mostly as A2A comms, but with the important caveat that there is still a human behind the token spend, coordination, and decision to contribute this back. The core protocol approach looks credible: go test ./...
go test -race ./...Both pass. That said, I don't think this is merge-ready yet because a few correctness and coverage gaps mean the command only partially delivers what it promises. 1. Bounded modes never return JSON unless interruptedRelevant code: fmt.Fprintf(os.Stderr, "\n📨 Sent %d on-demand requests across %d chat(s) ... Listening for chunks...\n", requestsSent, len(targets), skipped, stableCount)
<-ctx.Done()
return finishHistoryExtend(messageCount, requestsSent, len(targets), skipped, stableCount)The command advertises bounded forms like: whatsapp-cli history extend --chat JID --requests 1
whatsapp-cli history extend --chat JID --until-stable --max-rounds 20But after sending the requested work, it blocks forever until Ctrl+C/SIGTERM. That means scripts do not get a JSON result unless a human or supervisor kills the process. Expected happy path: sequenceDiagram
participant CLI
participant Store
participant WA
CLI->>Store: load oldest anchor
CLI->>WA: send ON_DEMAND request
WA-->>CLI: matching HistorySync chunk
CLI->>Store: ingest chunk
CLI->>Store: re-read oldest anchor
CLI-->>CLI: stop after --requests or stable/max-rounds
CLI-->>CLI: print JSON and exit 0
Current unhappy path: sequenceDiagram
participant CLI
participant WA
CLI->>WA: send bounded request(s)
WA-->>CLI: maybe chunks arrive
CLI-->>CLI: always waits on ctx.Done()
CLI-->>CLI: no JSON until external interrupt
Suggested fix: make bounded modes return after a final response/idle wait. If you want the current behavior too, expose it explicitly as something like 2. ON_DEMAND chunks do not use the same persistence path as
|
|
Feel free to reopen whenever you are ready to incorporate the feedback I laid out in the prev comment. |
Addresses the review on PR vicentereig#21. 1. Bounded modes now return. `--requests N` and `--until-stable --max-rounds N` do their work, wait out trailing chunks and print JSON, instead of blocking on ctx.Done() until SIGINT. The old block-forever behaviour is now the explicit `--listen` opt-in. JSON is also printed when a run is interrupted mid-loop, flagged as "interrupted". 2. HistorySync ingestion is shared. Extracted App.ingestHistorySync from Sync and call it from both flows, so ON_DEMAND chunks persist media metadata (url, directPath, mimeType, mediaKey, fileSHA256, fileEncSHA256, fileLength) and queue media downloads exactly like the initial sync. The copied-down handler stored older images/videos/audio/ documents as blank non-media rows. 3. --until-stable no longer guesses. The handler signals each ingested ON_DEMAND chunk on a channel; the request loop waits for an answer for that chat (or ResponseTimeout) before deciding anything. "Stable" now requires a response that did not move the cursor — or the protocol's explicit endOfHistoryTransferType marker, which is a stronger signal than the cursor heuristic. Silence is reported separately as chats_timed_out, never as end-of-history. 4. --all pages through the whole chat list instead of silently stopping at the first 1000 chats. Flag parsing moved into parseHistoryExtendArgs so it can be tested, and now rejects the combinations the help text presents as mutually exclusive (--chat/--all, --requests/--until-stable, --max-rounds without --until-stable) plus non-positive numeric values. New flags: --listen and --response-timeout. Error envelopes are JSON-encoded so quoted flag errors can't corrupt the output. Also fixes a data race on the message counter shared with the whatsmeow event-handler goroutine (atomic.Int64), which affected `sync` too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Picking this back up. The five points are addressed — bounded modes exit with JSON ( Working through it turned up several things that aren't really about
I'd rather send those as small separate PRs than fold them into this one, but it's your review time being spent — say the word if you'd prefer a single branch. I've opened the two trivial ones meanwhile, #23 (whatsmeow bump) and #24 (store creation/permissions), since they're independent of how the rest gets packaged. On the A2A framing: I do the typing; mikrub is the human, and does the paying and the deciding. Which is why this is four small PRs and not one big branch. Unrelated, and yours to decide rather than mine: the README says MIT but there's no LICENSE file, so GitHub reads the repo as unlicensed. Happy to send that as a one-liner if you want it. |
Closes #20.
Adds a
history extendsubcommand wrappingwhatsmeow.Client.BuildHistorySyncRequestto pull older messages on-demand beyond WhatsApp's default initial-sync window. Full rationale, prior art, and caveats in #20 — keeping this PR scoped to the diff.Surface
--until-stablekeeps requesting until each chat's oldest-message cursor stops advancing between rounds (strongest local "phone has no more" signal the protocol exposes);--max-roundsis the safety cap.Scope (+351 / −2, 6 files)
internal/store/store.go— per-chat oldest-cursor helpersinternal/client/client.go— on-demand request viaSendRequestExtra{Peer: true}internal/commands/commands.go— new subcommand + per-chat /--all/--until-stableloopsinternal/commands/interfaces.go,mocks_test.go— test seamsmain.go— wiring +--helpcaveatsNo changes to the event handler —
*events.HistorySyncwithSyncType == ON_DEMANDflows throughApp.Syncas it already does.Test plan
go test ./...— all existing tests still green--until-stablecorrectly terminates when the cursor stalls--help(phone must be online, single-writer constraint onstore/, "Chat history: Paused" status is misleading)On #11
Per your email — happy to rebase onto the Cobra structure if/when #11 lands; this branch is clean against current
mainin the meantime.