Root causes, each reproduced against the live server or endpoint:
- Server-only JIDs decoded as "@s.whatsapp.net" and pairing replies copied them
into `to`, so the device was never registered and every login returned 401.
Decoder/encoder fixed, replies use s.whatsapp.net, ADV hosted prefixes follow
whatsmeow. The link wizard now waits for the restart, logs in, uploads prekeys
and retries once with a fresh device on 401/403.
- Closed sockets made every receive loop spin until its deadline (the 7 minute
CPU burn). Loops stop on a closed stream.
- whatsapp_send had no image argument and send_image was a stub. Added WaMedia
(byte-exact against 8 wa-go golden vectors), media_conn upload, image send and
the DeviceSentMessage copy for the user's other devices.
- Phone contacts never reached the client: added app-state sync (WaAppState,
WaReceiver, LTHash and MAC checks verified against a whatsmeow-built oracle)
and the historySyncConfig declaration at registration.
- Contact lookup: FTS5 trigram plus fuzzy ranking, ambiguity is reported and
never guessed, global search lists contacts. Send caps (10/min, 200/day) are
enforced and survive unlink.
Failover and agent:
- Every failed backend candidate is printed the moment it fails.
- muse runs with low reasoning effort, an empty preset and one stable workspace
(36.5s down to 2.8s on the real prompt); Ctrl-C no longer counts as a backend
failure; CLI backends use the circuit breaker; opencode runs are capped at 30s.
- Orphan tool results are dropped before sending (devplace HTTP 400, reproduced);
compaction no longer starts its window on a tool result.
- Terminal art pasted as a prompt is refused before any model call.
Tests: whatsapp_test, muse_flags_test, llm_history_test; full suite 40 steps
(agent.e2e reads live tmux panes and is flaky, green on re-run).
Research archived under research/whatsapp-automation.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replaying the captured failing frame proved the std::vector max_size() abort was
never a parser bug. It was an object-lifetime bug in WaDevice::serialize():
put_blob(b, Bytes((jid ? *jid : "").begin(), (jid ? *jid : "").end()));
(jid ? *jid : "") builds a fresh temporary std::string for each evaluation, so
begin() and end() came from two different, already-destroyed objects; the range
constructor computed a garbage length and vector threw. It only misfired once
jid was set (after the phone scanned), which is why every parser test and the
initial unpaired save passed. Fixed by binding the value to a named const
std::string before taking iterators.
- src/whatsapp/wa_device.cpp: bind jid/lid before iterating; audited the tree
for the same two-temporaries pattern (no other instances).
- tests/whatsapp_test.cpp: test_device round-trips a PAIRED device (jid+lid set,
synthetic identifiers), the populated case that triggers the bug; runs under
tools/san-whatsapp.sh.
- docs/whatsapp.md 13.7: records the real root cause and closes the open item.
- docs/cpp-safety.md: adds the object-lifetime rule (never take iterators from a
temporary you rebuild) as a distinct bug class from size overflow.
Verified: replaying the captured pair-success frame now serializes, saves, and
encodes the response with no throw; build, whatsapp_test, make lint, and
tools/san-whatsapp.sh all green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Response to incident WA-2026-10-27 (the std::vector max_size() crash). Deep
research archived under research/safe-cpp-practices (8 Tier 1, 37 Tier 2, 2
conclusions) on production-safe C++ that keeps development speed: OpenSSF
compiler hardening, the C++ Core Guidelines bounds profile, integer-overflow
practice, libFuzzer/sanitizer CI, the sanitizer overhead ladder, clang-tidy
ratchets.
The research prescribed an ASan+UBSan+_GLIBCXX_ASSERTIONS build of the parsers.
Standing it up immediately caught a second, independent undefined-behavior bug
that two manual reviews had missed: a protobuf varint loop that shifted by up
to 70 bits (shift >= 64 is UB) on malformed input. Fixed and guarded by a
deterministic test and the sanitizer net.
- src/whatsapp/wa_proto.cpp: guard the wire-0 varint shift (the UB).
- tests/whatsapp_test.cpp: add an overlong-varint hostile case.
- tools/san-whatsapp.sh: the standing sanitizer net for detz::wa parsers.
- WaClient frame capture: a frame that fails to decode/dispatch has its exact
bytes recorded to <home>/whatsapp/last-error.log (bounded), so the still
unreproduced live <iq> failure becomes a one-paste deterministic fix. Wired
from the wizard and the whatsapp_send tool.
- docs/cpp-safety.md: the project-wide playbook and design decisions (four
layers: overflow-safe checked-reader parsing, cheap release hardening, the
sanitizer test net, capture). docs/whatsapp.md section 13.6 records the
follow-up and the honest open item. AGENTS.md makes it a standing rule;
cap.md and mcp.md inherit it for their wire parsers.
Verified: build, whatsapp_test (incl. hostile+fuzz), make lint, and
tools/san-whatsapp.sh all green; research audit PASS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record incident WA-2026-10-27 in docs/whatsapp.md section 13: the symptom (a
std::length_error core dump during --whatsapp-link after the QR refreshed), the
root cause (an overflowable `pos + len > size` bounds check plus an unguarded
dispatch loop), the fix (commit f16c5c6), and the standing rule for all code
that parses untrusted bytes: overflow-safe bounds checks, no unchecked
allocation from an attacker-controlled count, and no exception escaping a
top-level loop. The goal is C++ that a hostile peer can never crash.
Prevention is enforced, not just written: tests/whatsapp_test.cpp gains a
test_hostile section that feeds the parsers the exact shapes that crashed (a
4GB length field, an oversized protobuf varint, truncated frames, 2000 random
inputs) and fails if std::length_error is ever thrown again. Runs under
build/whatsapp_test / whatsapp.unit / make test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Native WhatsApp Web multi-device client in C++ (src/whatsapp/, namespace
detz::wa), built on OpenSSL plus a vendored public-domain ed25519 ref10
(third_party/ed25519, zlib) for XEdDSA and the nayuki QR generator
(third_party/qrcodegen, MIT); no new runtime dependency.
- WaCrypto, WaBinary, WaProto, WaNoise, WaSignal, WaDevice, WaSocket,
WaClient, WaStore, WaQr, WaWizard.
- Verified offline against golden vectors captured from the real Baileys
client (tests/whatsapp_data, from jfelipesjc/wa-go, MIT): HKDF/PBKDF2 RFC
vectors, XEdDSA roundtrips, codec battery 19/19, ClientPayload byte-exact,
the Noise handshake, and a 4-exchange Signal session byte-exact. Unit test
build/whatsapp_test (16/16), run_all step whatsapp.unit.
- Verified live against web.whatsapp.com: `detz --whatsapp-link` connects,
completes the Noise handshake, receives pair-device, renders a scannable QR
in the terminal, and persists the device to the vault (whatsapp/<profile>.db).
Also --whatsapp-status / --whatsapp-unlink.
- Agent tools whatsapp_send and whatsapp_contacts (detz-only, in DetzTools).
- Pairing completion after the phone scans, and the message send path
(usync + prekey fetch + Signal encrypt + send node), are ported faithfully
from whatsmeow but unverified in-tree (no linked account in development).
send_image and own-device fan-out are not wired yet. See docs/whatsapp.md.
Also in this snapshot: the terminal-screenshot tool (src/termshot/,
terminal_screenshot, docs/termshot.md, research/terminal-screenshot-rendering)
and a concurrent session's in-progress llm/muse failover changes carried along
in the shared files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No native Python WhatsApp library exists (neonize is whatsmeow over
ctypes, yowsup is dead) and no browser is needed. Spec for detz::wa:
native C++ transpiled from wa-go (MIT, golden vectors) with whatsmeow as
referee, libcurl WebSocket plus OpenSSL, no new dependencies, build vs
deep-research table, caps, tests, phases, and a whatsapp-mcp shortcut.
Research archive: research/whatsapp-automation (70 sources, 3
conclusions). Nothing built.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>