6f207d2e28
Found by driving the core against a real local server rather than an injected delivery stub, which is exactly where it could hide: fetch only rejects on a network-level failure, so a 500, a 503 or a 429 resolved normally and the batch was counted as delivered and dropped. The unit tests could not catch it because their stub throws, and real fetch does not. That is the likelier outage than a refused connection, so the retry added in the previous commit was covering the rarer half of the problem. Any non-2xx now throws and takes the retry path. Matching the Python core, which retries every HTTP error rather than classifying them: the backoff and the queue bound contain a payload that will never be accepted, because the re-queued batch sits at the front and is the first thing evicted. Two tests against the real default delivery path, stubbing fetch rather than the delivery hook, so a 503 keeps the batch and a 200 clears it. End to end against a local server, real fetch and real retry timing: events arrive and carry a uuid, a 503 keeps the batch, an immediate retry is suppressed by the backoff, the retained event is delivered on recovery with its original uuid and no duplicate, and a refused connection behaves the same way. Nine of nine. Identity checked on the same path: opencode's project_hash is salted, differs per account, is stable within one, and no raw project id appears in the payload. openclaw emits two distinct identities across a key change, which is the defect this branch fixes, observed on the wire rather than through a mock. agent-plugin-core/ts 32, openclaw 431, opencode 35, pi-agent 89, deepseek 47, Python core 242 unaffected. Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb