When the reactor sets pending_work while an agent run is in progress,
checkPendingWork fires a synthetic follow-up event with FromAgent=
__coalesced__ and body="Coalesced trigger: process all pending
messages." That event gets delivered to the container as a
message.json with that placeholder body, and the wrapper happily
feeds it to gemini — which then produces a spurious "What does this
demo do?" reply because the only thing the model sees is a generic
filler body.
The doc-gardener coordinator kept emitting stray replies to algis
between real DELEGATED: messages because of this. The reactor would
fail the coalesced run ("Received system trigger..."), the critic
would get confused by intermediate traffic, and the /goals panel
would accumulate garbage.
Wrapper now checks $FROM at the top of main. If it's __coalesced__
we log it and exit 0 without invoking the CLI. The reactor marks
the run succeeded, no tokens burned, no spurious DMs produced. Any
real pending work re-triggers naturally when the next actual
message arrives.
Smoke-verified:
docker run --rm -v /tmp/test:/workspace synapbus-agent:latest \
/usr/local/bin/synapbus-agent-wrapper.sh
[wrapper test] cli=gemini from=__coalesced__ body_bytes=9
[wrapper test] synthetic coalesced trigger — skipping CLI invocation
EXIT=0
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SynapBus container images
The docker harness backend (internal/harness/docker/) runs each agent
inside an ephemeral container. This directory holds the canonical agent
image SynapBus's bundled examples reference.
synapbus-agent
The default image. Debian bookworm-slim base with:
geminiCLI (@google/gemini-cli)claudeCLI (@anthropic-ai/claude-code)tinias PID 1 (signal forwarding + zombie reaping)- Standard tooling the example wrappers use:
jq,sqlite3,curl,git,python3 - Non-root
agentuser (uid 1000, gid 1000) matching the typical host user
No SynapBus binary lives in the image. Agents reach the SynapBus MCP
server on the host at host.docker.internal:<port> — the harness
rewrites .gemini/settings.json URLs from 127.0.0.1 to the gateway
hostname automatically.
Build
Local single-arch:
docker build -t synapbus-agent:latest image-build/synapbus-agent
Multi-arch via buildx (recommended for sharing the image):
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t synapbus-agent:latest \
--load \
image-build/synapbus-agent
Pin specific CLI versions with build args:
docker build \
--build-arg GEMINI_CLI_VERSION=0.37.1 \
--build-arg CLAUDE_CODE_VERSION=1.0.0 \
-t synapbus-agent:0.37.1 \
image-build/synapbus-agent
Wire an agent to use it
In harness_config_json add a docker block:
{
"gemini_md": "...",
"mcp_servers": [...],
"env": {...},
"docker": {
"image": "synapbus-agent:latest",
"memory": "1g",
"cpus": "1.0",
"network": "bridge"
}
}
The reactor will pick the docker backend automatically when it sees the
docker.image field. Default security posture: --cap-drop=ALL,
--security-opt=no-new-privileges, --read-only root with tmpfs
/tmp, --pids-limit=512, --user=<host uid:gid>. Override via the
typed fields in the docker block (memory, cpus, cap_add,
extra_mounts, read_only_root, user).