Trace
Trace: watch which boundary functions run, and on whose behalf, with a limit you cannot leave out.
A session watches named functions of a --hot-reload build (the ones forge deploy hot can replace: your app's, not the stdlib's). Each call into one writes an event into the session's ring: the calling actor's pid, the function, the code epoch it ran at, and when. NEVER ITS ARGUMENTS: they can hold anything the program holds, passwords and tokens included.
Every session has a limit, and start takes it as an argument so there is no way to call it without one:
MaxEvents(n) stops itself after exactly n events (1..65536);
Rate(n, secs) keeps at most n events per `secs`-second window (n in
1..100000, secs in 1..3600) and counts the rest as dropped.Anything else (0, negative, over the caps) is refused with LimitOutOfRange. A session also ends by itself after 600 seconds, or 60 seconds without a drain, and at most 4 exist at once. A session belongs to the process, not to your connection: stop frees it early; a forgotten one frees itself.
You pull events with drain; the tracer is never waited for. A full ring drops the event and counts it (dropped), so a slow reader costs completeness, never the traced program's speed.
Actor events. A session can also record what actors do: Sent, Received, Spawned, Exited and Dropped, with the pids, the message's constructor tag, why (for the last two) and when. NEVER a message's contents. They are recorded only for the actors you NAME (pids, one to eight live pids as pid_to_int gives them): an event is kept when its pid or its peer is one of them. There is no "every actor" form, because a stream of the whole node would make every send on it pay for the tracer. Build the options with actors, calls and with_calls and start with start_with:
Trace.start_with(d, Trace.actors([Trace.Sent, Trace.Received], [pid_to_int(a)]),
Trace.MaxEvents(100))The limit counts boundary calls and actor events together. See ActorEvent.
Backends. Compiled with --hot-reload only: other builds, and the interpreter, have no dispatch table to watch and answer Unavailable. Over the observe socket the same operations are the signed debug verbs TRACE_START, TRACE_DRAIN and TRACE_STOP (docs/observe.md).
Debug tier: every function takes Cap(Actor.Debug) (mint it with Actor.debug(io) in main; the remote shell has it pre-bound as debug).
R8 of specs/plans/2026-09-28-observe-recon-shell-plan.md.
Types
Functions
Options that watch actor events of the kinds kinds for the actors pids (pid indices, as pid_to_int gives them; one to eight, all live).
The events recorded since the last drain (at most 1024; drain_max takes a bound up to 4096), oldest first, and the session's state.
Like drain, taking at most max events (1..4096).
One line per event: <at_ns>ns pid <pid> <function> epoch <epoch>.
One line per actor event: <at_ns>ns <kind> pid <pid> [peer <p>] [tag <t>] [<reason>].
Start watching the boundary functions names (exact names, as Recon's and forge deploy hot print them) under limit. At most 32 names and 4 sessions.
match Trace.start(d, ["Billing.charge"], Trace.MaxEvents(10)) do
Ok(s) -> ...
Err(Trace.UnknownFunction) -> println("not a boundary function")
Err(e) -> println("cannot trace")
endStart a session over opts (see calls, actors, with_calls) under limit. Actor events without pids, or pids without events, are refused; so are more than 8 pids and pids that are not live actors.
match Trace.start_with(d, Trace.actors([Trace.Sent, Trace.Exited], [pid_to_int(a)]),
Trace.MaxEvents(50)) do
Ok(s) -> ...
Err(Trace.PidsRequired) -> println("name the actors to watch")
Err(e) -> println("cannot trace")
endStop the session and free it; events not yet drained are discarded. The final state.