NullClaw
NullClaw adds remote agent calls while repairing runtime liveness
NullClaw adds an A2A client tool, proposes aging out dead polling threads after repeated failures and raises the agent-turn stack ceiling after runtime overflow. The cluster expands agent-to-agent reach while exposing the liveness limits of a compact Zig runtime.
nullclaw/nullclaw issue #700 is the inspected primary source: “Add a2a_call client tool for calling remote agents.” The related records below were inspected as supporting context rather than independent confirmation.
The facts
- nullclaw/nullclaw issue #700 was created 2026-03-23T13:19:27Z and was closed at inspection; its title is “Add a2a_call client tool for calling remote agents.” - nullclaw/nullclaw PR #984 was created 2026-08-05T19:19:59Z and was open at inspection; its title is “fix(channels): let poll failures age out a dead polling thread.” - nullclaw/nullclaw PR #985 was created 2026-08-05T19:57:45Z and was open at inspection; its title is “fix(runtime): give the agent turn path a 16 MiB stack.” - The repository reported 8024 stars, 84 open issues, default branch main, and last push 2026-07-19T04:20:06Z. - The latest tagged-release baseline checked was v2026.5.29 published 2026-05-29T13:41:10Z.
What changed
nullclaw/nullclaw issue #700 reports: Summary nullclaw serves the A2A protocol (v0.3.0) but has no client-side implementation. I've built an a2a_call tool that lets the agent send message/send JSON-RPC requests to remote agents. Use case I run two nullclaw instances — a public-facing doorman and a private personal agent on a separate box. The doorman needs to route scheduling and availability requests to the private agent. The a2a_call tool handles this over Tailscale. What I built src/tools/a2a_call.zig — new tool (~320 lines) RemoteAgentConfig struct + a2a.remote_agents config array Config parsing and threading to all allTools() call sites HTTPS enforced for public endpoints, HTTP allowed for RFC 1918 and Tailscale CGNAT (100.64/10) Synchronous message/send only (no streaming, no polling — YAGNI) 36 unit tests (SSRF variants, response parsing, injection checks) Tested live between two nullclaw instances over Tailscale Validation zig build test --summary all — 6156/6167 passed, 0 failed, 0 leaks zig build -Doptimize=ReleaseSmall — 4.1 MB (+100KB) zig fmt --check src/ — clean 15 e2e tests against a live instance (multi-turn, unicode, concurrent, prompt injection, SSRF) Branch https://github.com/georgeglarson/nullclaw/tree/feature/a2a-client Questions for
nullclaw/nullclaw PR #984 reports: Closes #972. Summary Telegram and Matrix channels go silent after an idle night while nullclaw agent still answers, and only a full gateway restart brings them back. The supervisor is supposed to catch exactly this, but it was structurally blind to it. Root cause supervisionLoop decides a polling thread is dead by age: But every polling loop refreshed that timestamp from inside its own failure path: So a poll that fails forever kept now - last near zero and stale never became true. That left healthCheck() as the only signal, and it is a different request: a short getMe / whoami that keeps returning 200 while the long-poll connection is dead. The channel was functionally dead and reported healthy. All five polling loops had the write: runTelegramLoop, runSignalLoop, runWeixinLoop, runMatrixLoop, runMaxLoop. The reporter in #972 and a second operator in the thread both landed on this line independently. Changes 1. Failure path no longer touches last_activity** (all five loops). The timestamp now tracks successful polls only, which is the invariant the 600s threshold was written
nullclaw/nullclaw PR #985 reports: Closes #976. Root cause SESSION_TURN_STACK_SIZE was aliased to HEAVY_RUNTIME_STACK_SIZE: That constant sizes every thread which runs SessionManager.processMessage*() / Agent.turn(). The turn path is the deepest stack in the runtime (channel decode -> session -> agent loop -> provider -> tool dispatch), and aarch64 frames are larger than x86_64 ones, so 2 MiB was not enough there: the stack pointer crossed into the PROT_NONE guard page below the thread stack and the process died on **every** inbound message. nullclaw agent -m "hi" was never affected because it runs on the 8 MiB main-thread stack, which is why the config, provider and LLM paths all looked healthy. Why 16 MiB The reporter established the mechanism from both directions: At the crash $sp was inside the guard mapping immediately below the worker stack, and the return address appeared **zero** times on the stack - so a stack overflow, not runaway recursion. An LD_PRELOAD shim clamping every pthread_attr_setstacksize call to 16 MiB made the bot reply normally with NRestarts staying at 0. Their LD_PRELOAD trace observed 512 KiB and
Why it matters
Remote calls increase the number of places a tiny runtime can stall, recurse or retain dead transport state. A working client is only useful if failed polling and deep turns recover without process restart. The operator test is whether visible output, retained state and authority still describe the same event after retries, background work or restart.
Current
The primary record was closed when captured on 2026-08-13. The tagged-release baseline was v2026.5.29 published 2026-05-29T13:41:10Z. Repository metadata, full source bodies, current pull requests and issues, releases, Google News, Hacker News, Lobsters, Metamesh and the rendered ClawCharts row were inspected. ClawCharts selected the subject; it did not prove the claim.
Evidence
The primary URL is https://github.com/nullclaw/nullclaw/issues/700. Supporting records are https://github.com/nullclaw/nullclaw/pull/984 and https://github.com/nullclaw/nullclaw/pull/985. Source bodies, timestamps and states are preserved in the daily evidence bundle. Test counts and reproductions remain attributed to their authors unless identified as independently rerun.
Source boundary
Open work is described as open, closed work as closed, and operator reports as reports. A pull request is evidence of proposed or reviewed direction, not proof of a shipped release. Search residue, package mirrors and historically published source spines were excluded.
Operator take
Bound remote call depth and time, expose polling-thread generation and failure age, and treat larger stacks as a mitigation while measuring the call path that consumed them. Preserve a before-state receipt, make the smallest reversible change, and verify the original failure independently.
Caveat
Public project records are mutable. Status, scope and evidence can change after publication. This brief records the inspected state and does not authorize changes to a reader’s deployment.
Source inspected; source state, environment and release boundary remain explicit.