Specification §11a

The proxy and the daemon.

§11 specifies what crosses the wire; this specifies what puts it there. The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119.

FrogNet Specification — Draft 0.9, revision 2026-08-29. Cite a conformance claim against this revision, not against a section number alone.

1. Scope

Croakus: §27, Appendix F

§11 specifies what crosses the wire. This specifies the two components that put it there. Both are named at every step of the pathway in §11.2 and neither is defined elsewhere in this document.

ComponentSitsOwns
ProxyOn port 80 of every nodeInterception, the semantic-mode decision (§11.5), socket sets, the local read cache (§2a)
DaemonOn the FrogNet service port of every nodeThe codec, the worker pool, execution against the origin

They are separate processes and MUST be. The caches they hold are therefore separate and behave differently (§11a.6); a design that merges them collapses that distinction.

2. The proxy

The proxy takes port 80. An application on the node makes an ordinary HTTP call and MUST require no modification: interception is the whole of the integration.

Every request goes through the daemon, not only the templated ones. A request that cannot be templated is forwarded raw and cached raw; it is not diverted around the semantic path. An implementation MUST NOT make raw traffic a bypass, because a bypass means two code paths with different failure modes for the same request.

Concurrency is bounded: the proxy caps active requests. An implementation MUST bound it, because the collapse mechanisms in §11.12 bound duplicate work but not arrival rate.

No subprocess on the hot path. Earlier versions forked a shell per request to emit topology and link-quality telemetry. Telemetry MUST instead be emitted periodically from in-process state, on its own schedule. A per-request fork makes observability cost proportional to traffic, which is exactly backwards.

3. Socket sets

A worker for a peer is a socket set: one outbound socket, one return socket, its own sequence space, its own pending map, its own writer and cleanup threads. One set per peer is the default (§11.11).

A caller — a game, a media stream, a bulk transfer — MAY request its own independent set for a peer, keyed by protocol. The caller receives an opaque token and MUST NOT touch a socket: the set is an artifact of the driver, and the driver owns its lifecycle.

3.1 Why independence helps

Each set is a separate flow inside the one tunnel, so it has its own congestion window and its own head-of-line domain. A lost segment on the game's set stalls the game's set and not the sensor stream sharing the peer. That isolation is the entire purpose; a set that does not get its own flow provides nothing.

3.2 Overlay is not free

Independent sets are capped at three per peer. Beyond the cap, additional protocols MUST share an existing set — and the moment two protocols share a set they share one congestion window and one head-of-line domain, so a loss on one stalls the other.

Overlay MUST therefore collapse the least latency-sensitive protocols together, and MUST NOT overlay onto a latency-critical set where any alternative exists. Sensitivity is declared by the caller when the set is opened; a set's sensitivity is the maximum of the protocols on it.

Sets are transient. A caller opens one at start and releases it at end, and release MUST reap the underlying worker when its last protocol leaves. Because the application is blind to the set, an implementation MUST NOT make the application responsible for the lifecycle.

4. The daemon

The daemon runs the codec and executes against the origin.

Local execution MUST be gated on local resolution. A request is executed locally only where its stated destination resolves to an interface on this machine. The destination MUST be explicit and the daemon MUST fail closed where it is absent — a daemon that infers a missing destination executes somebody else's request against its own origin.

Upstream connections to the local origin MUST be marked before connect, so that the node's own translation rules do not re-capture them. Without the mark, the proxy's interception applies to the daemon's own upstream call and the request recurses into the node.

End-to-end timeout policy MUST accommodate slow bearers — the reference implementation allows up to 45 seconds — and MUST be overridable, because the bearer under FNWP-1 ranges from a wired LAN to a long-range radio (§18).

5. The worker pool

Database concurrency is sized at startup from three ceilings and MUST respect all three:

cpu_count() * 4            diminishing returns beyond this
max_connections / 4        leave headroom for the origin and others
RLIMIT_NOFILE / 8          file descriptors are not free

The executor and the pool MUST derive their size from one source. Where they drift, half the executor's workers race for connections that do not exist and the pool churns connect and close under load.

The pool MAY resize at runtime. The reference heuristics, evaluated over the window since the previous evaluation:

p95 acquire wait      > 100 ms   ->  grow 25%
p95 in-flight use     <  50%     ->  shrink 25%
otherwise                        ->  hold

Resizing MUST stay within the startup bounds, MUST be triggerable out of band by a sentinel as well as on its interval, and MUST log one line per evaluation regardless of the action taken. A resizer that logs only when it acts is indistinguishable from a resizer that is not running.

6. Two caches, two rules

Because the proxy and the daemon are separate processes, they hold separate caches. Both answer reads from RAM and both are conformant, for different reasons (§2a).

Neither is the semantic store, and neither is FrogNet Memory. The node's database holds templates and per-peer references at all times, and the tuple space only while that node is the elected host (§11.14). The two caches below sit above those, in memory, and are discarded freely.

DaemonProxy
WhereDatabase host onlyEvery node
ScopeThe store's own tablesOnly that proxy's own local forwards — nothing global, nothing cross-host
StandingProven current: served only where a store-maintained generation counter shows RAM equals disk on this requestBounded stale: scoped invalidation on writes it observes, plus a short TTL backstop
On doubtDisables itself for the life of the process — fail closedFalls through to the origin
WritesNever interceptedNever intercepted

Neither MUST ever acknowledge a write it has not durably placed. An earlier daemon cache acknowledged an upsert before the row reached the store, which is a false answer about durability rather than a fast one.

The invalidation scope on the proxy cache MUST be by name membership, not a flush on any write: writes run at hundreds per minute, and a blunt flush thrashes the cache to uselessness. A write busts every cached read whose result contained the written name, which covers narrow and broad queries alike because a broad query's result lists its members. The TTL exists solely to cover what name membership cannot see: a newly created item that should newly appear in a broad query whose cached result predates it.

7. Non-goals

The proxy is not a load balancer, a reverse proxy for third-party services, or a TLS terminator. The daemon is not an application server: it executes against the origin and holds no application logic. Neither is a member of the tuple space, and neither MUST be required for a node to serve its own store — a node with no reachable peer is a conforming pond of one (§3).

8. References

Specification §2a, §3, §9, §10, §11, §12 (Security), §18. Source: proxy/proxy_main.py, proxy/decision.py, proxy/channel_sets.py, proxy/local_read_cache.py, daemon/daemon_main.py, daemon/pool_resizer.py, daemon/engine/data_cache.py, core/sizing.py. Build manual: Croakus §27, Appendix F.