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>
65 lines
2.4 KiB
C++
65 lines
2.4 KiB
C++
// clean_history must never hand a provider a tool result whose tool call was not issued: devplace answers
|
|
// that with HTTP 400 "No function call found for function call output with call_id ..." and the whole turn
|
|
// fails (everythingwrong.md; reproduced against the live endpoint with an orphan result).
|
|
#include <cstdio>
|
|
#include "helpers.h"
|
|
#include "json.h"
|
|
using detz::json::Value;
|
|
static int g_fail = 0;
|
|
#define CHECK(cond) do { if (!(cond)) { g_fail++; std::printf("FAIL %s:%d %s\n", __FILE__, __LINE__, #cond); } } while (0)
|
|
|
|
static Value msg(const char* role, const char* content) {
|
|
Value m = Value::object();
|
|
m.set("role", role);
|
|
m.set("content", content);
|
|
return m;
|
|
}
|
|
static Value assistant_calling(const char* id) {
|
|
Value fn = Value::object();
|
|
fn.set("name", "fork");
|
|
fn.set("arguments", "{}");
|
|
Value call = Value::object();
|
|
call.set("id", id);
|
|
call.set("type", "function");
|
|
call.set("function", fn);
|
|
Value calls = Value::array();
|
|
calls.push(call);
|
|
Value m = Value::object();
|
|
m.set("role", "assistant");
|
|
m.set("content", Value());
|
|
m.set("tool_calls", calls);
|
|
return m;
|
|
}
|
|
static Value tool_result(const char* id) {
|
|
Value m = msg("tool", "done");
|
|
m.set("tool_call_id", id);
|
|
return m;
|
|
}
|
|
|
|
int main() {
|
|
// a window that starts on a tool result (compaction cut its assistant message away)
|
|
Value cut = Value::array();
|
|
cut.push(msg("system", "s"));
|
|
cut.push(tool_result("call_orphan"));
|
|
cut.push(msg("user", "go"));
|
|
cut.push(assistant_calling("call_a"));
|
|
cut.push(tool_result("call_a"));
|
|
Value cleaned = detz::llm::drop_orphan_tool_results(detz::llm::clean_history(cut));
|
|
CHECK(cleaned.size() == 4);
|
|
bool orphan_left = false;
|
|
for (const auto& m : cleaned.items()) {
|
|
if (m.get("role").str() == "tool" && m.get("tool_call_id").str() == "call_orphan") orphan_left = true;
|
|
}
|
|
CHECK(!orphan_left);
|
|
// a healthy exchange is untouched
|
|
Value ok = Value::array();
|
|
ok.push(msg("user", "go"));
|
|
ok.push(assistant_calling("call_b"));
|
|
ok.push(tool_result("call_b"));
|
|
CHECK(detz::llm::drop_orphan_tool_results(detz::llm::clean_history(ok)).size() == 3);
|
|
// clean_history itself stays byte-for-byte tai.py behaviour: it keeps the orphan (agent_diff.py compares it)
|
|
CHECK(detz::llm::clean_history(cut).size() == 5);
|
|
std::printf(g_fail ? "llm_history_test: %d FAILED\n" : "llm_history_test: ok\n", g_fail);
|
|
return g_fail ? 1 : 0;
|
|
}
|