New sessions/session_export tools plus a sessions_block vault map in the system prompt (tai.py, ported to detz); recall/search carry a session footer instead of letting the model grep sealed bytes. write_file refuses new markdown at working/repo roots, and the blanket *.md gitignore is gone so docs stay visible. One LineInput owns fd 0 for every prompt (ask, approvals, shell_ask, secrets), ending phantom-empty answers; Ctrl-C re-prompts (double exits), Ctrl-D exits, Ctrl-Z suspends; mid-turn Ctrl-C announces itself and kills foreground shell children via RunInterrupted. Covered by SessionsTests, repl_keys.py pty checks, agent_diff sessions_block cases.
clear_device deleted the device rows inside an implicit transaction and never committed,
so the delete was rolled back at process exit and --whatsapp-link answered "Already
linked". Test reopens the database and checks the device is gone and contacts are kept.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A message to the user's own number listed the phone twice (as recipient device and as
own device), so the server accepted it but nothing arrived. The device list is now
deduplicated; devices of the sender's user get the DeviceSentMessage wrapper, all
others the plain message (whatsmeow encryptMessageForDevices). The participant hash is
computed and compared with the server's (they match: 2:TKwG4B1n), and an ack carrying
error= is reported as a failed send instead of success.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
docs/cpp-safety.md (the four-layer safe-C++ playbook and design decisions) and
the AGENTS.md standing rule were skipped by taicpp/.gitignore's *.md pattern in
79ae6f2; force-add them so the policy is actually in the tree, matching how the
other docs are tracked.
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>
A protobuf/binary length field could be read as a huge value, and the
`pos + len > size` bounds check overflowed size_t and slipped past, so
vector::assign threw std::length_error. Node dispatch (pairing, etc.) was
not wrapped in a catch, so this aborted the whole process (core dump) during
--whatsapp-link after the QR refreshed.
- ProtoReader::next and the binary Decoder now use overflow-safe length
checks (len > remaining) and the decoder reads 4-byte lengths unsigned and
rejects anything larger than the frame; read_bytes rejects negatives.
- WaClient::dispatch wraps all node routing in try/catch: a bad node is
logged and skipped instead of crashing the client.
Verified: build/whatsapp_test 16/16, make lint clean, and a live
--whatsapp-link run survives the QR-refresh window with no abort.
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>
Feed panes in the multiplexer (vpane_* control commands, main-vertical layout, header row),
src/orch (board, panel, formatter, transport, CAP adapter), Agent observer and cancel flag,
supervision (idle warn/cancel, user close cancels, soft then hard stop), CAP answers collected
through the spill path. Research archived under background-work-limits.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
make test builds and runs 32 steps; not green: misc.diff and mplex.e2e fail on real module bugs.
test_main.cpp replaced by util_config_test.cpp; stale *.mk and store_test rule removed.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Port of the grr client of tai.py: client, SSE stream, session, store, router, shell, views,
chat, panes, file viewer and the unix control socket; GrrTui::run is the --tui entry.
Verified against tai.py --tui on a real grr server over a pty: 236 steps, screen text and
per-cell styles identical except clocks, spinner frames and server-made ids.
docs/tui.md holds the symbol table and gaps, docs/wiring/tui.md the integrator hooks.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>