Mesh Configuration - Dark-Alex-17/coyote GitHub Wiki

The mesh: block of config.yaml says how this Coyote joins and behaves on the Coyote Mesh. This page gives you the block with every key commented, a table of the keys, the errors a bad value produces, and a closer look at the keys whose behavior does not fit on one line. Every key has a default, so a config.yaml without a mesh: block is the same as the block below with enabled: false. The wire format these keys drive is specified separately as SCOPE; the full specification lives in the scope-protocol repository, https://github.com/Dark-Alex-17/scope-protocol. You do not need it to set these keys.

A few Reticulum words come up in the block and the tables. An announce is the periodic "I am here" broadcast by which nodes learn of each other. A relay is a node that forwards traffic between others. rnsd is the Reticulum daemon, a small background program that owns the network interfaces so several programs on one machine can share them; its AutoInterface finds other machines on the same local network by itself. Loopback is 127.0.0.1, the machine itself. A propagation node holds a message for a recipient who is offline until they fetch it; that is what "store-and-forward" means below. The envoy is the built-in agent that answers trusted peers for you.

mesh:
  enabled: false                 # Join the mesh whenever this REPL has a session: at start with --session, or when you enter one with .session (default: false);
                                 # .mesh off keeps it off for the rest of the run; one-shot runs (--macro, a prompt on the command line) never join. Nothing
                                 # leaves the machine while this is false
  announce: true                 # Announce this node so peers can see it (default: true); false = outbound-only, see peers without being seen; also turns the
                                 # automatic propagation sync off (`.mesh sync` still works)
  display_name: null             # Opt-in plaintext label carried in the announce (default: null = nothing but the protocol version is announced)
  display_name_on_public: false  # Also carry display_name when any interface is type public (default: false; public presence is world-visible)
  about: null                    # One human-written line (up to 200 characters) on what this node's envoy can help with; carried on the status card to trusted
                                 # peers only, never in the announce (default: null = nothing)
  interfaces:                    # Interfaces the node joins. Exactly what is listed here and nothing else; there is no auto-detection and no fallback between
                                 # types (default: a single private entry pointing at the local rnsd on 127.0.0.1:4242)
    - type: private              # private = TCP client to a relay. The default dials the local Reticulum daemon (rnsd) on loopback; run scripts/mesh-relay.sh
                                 # once per host
      host: 127.0.0.1
      port: 4242
    # - type: lan                # lan = link-local only (Reticulum AutoInterface), the no-daemon fallback. Does not cross sbx, WireGuard, Tailscale or
                                 # OpenVPN; only one process per host can bind it
    # - type: private            # private = TCP client to a relay your team runs; requires host + port
    #   host: relay.example.com
    #   port: 4242
    # - type: public             # public = same wire shape as private, but the relay is a community node so your presence is world-visible; requires host + port
    #   host: node.example.com
    #   port: 4242
    # Other transports (LoRa/RNode, I2P, UDP, serial) are configured in rnsd and reached through the loopback hop above.
  brief: auto                    # What the envoy serves to trusted peers (default: auto): auto = status card + session digest + user brief;
                                 # manual = card + user brief only, no model spend; off = card only
  digest_prompt: null            # Prompt used to build the shareable session digest. If null or blank, uses built-in default
  brief_model: null              # Model used to build the digest (e.g. 'openai:gpt-4o-mini') (default: null = the session's current model)
  envoy_model: null              # Model the envoy answers peers with (default: null = the configured model_id; the envoy never follows a `.model` switch)
  envoy_escalation_timeout: 0    # Seconds the envoy holds a peer's question open for the human before handing it off (default: 0 = hand off at once; the
                                 # question stays open for `.mesh answer`; a hold longer than the 120 s run ceiling is cut to it)
  knock_retention_hours: 24      # How long unanswered knocks from known identities are kept (default: 24; 1 or more)
  collision_protection: false    # Whether an identity trusted for all destinations is refused when it announces or claims an instance id recorded under another
                                 # identity, or heard in the peer table under one trusted for all destinations (default: false = served, with a warning to you;
                                 # true = refused with an error until you trust its new destination with `.mesh trust`; a destination you trusted explicitly is
                                 # served either way)
  peer_max_concurrent: 1         # Envoy runs one sending identity can have queued or running at once (default: 1; 0 = unlimited)
  peer_max_messages_per_hour: 60 # Messages accepted from one sending identity per hour, in fixed hourly windows kept in memory (default: 60; 0 = unlimited)
  peer_max_tokens_per_hour: 100000 # Model tokens one sending identity can cost per hour, counted after each envoy run (default: 100000; 0 = unlimited)
  peer_max_cost_usd_per_hour: 0  # USD one sending identity can cost per hour, enforced only when the envoy model's prices are known (default: 0 = no cost ceiling)
  request_timeout_secs: null     # Seconds to wait for a peer's answer on /message, /knock, /list, /access and /status; raises a path's built-in deadline (15 s; 30 s for /status) and never lowers one, so under 15 is refused; /fetch keeps its own 120 s and the mesh__peers status sweep its 5 s (default: null = each path's built-in deadline; 15 to 31536000)
  link_timeout_secs: null        # Seconds to wait for the link to open and identify before a request on the same five paths; raises the built-in 10 s and never lowers it, so under 10 is refused (default: null = 10; 10 to 31536000)
  propagation_sync_interval_secs: 300 # Seconds between automatic fetches of the messages a propagation node holds for this node; the first fetch runs once a
                                      # propagation node is heard after the node joins (default: 300; 0 = fetch only on `.mesh sync`; at most 31536000); off
                                      # while `announce` is false
  fetch:                         # File sharing between trusted peers
    inline_max_bytes: 65536      # Largest file a peer may attach inline to a message; larger files are dropped by the receiver (default: 65536; must be 1 to
                                 # 98304, the per-message total)
    max_bytes: 4194304           # Largest file this node serves to a peer that fetches it; a larger one is refused (default: 4194304; must be 1 to 4194304, 4 MiB; served limit is currently capped near 1 MiB by a transport workaround, see below)
    inbox_dir: null              # Absolute directory where files peers send inline, and files this node fetches, are staged, under <inbox_dir>/<instance id>;
                                 # keep it outside any workspace you share, or peers could fetch each other's staged files
                                 # (default: null = <cache dir>/mesh/inbox/<instance id>)
  envoy_memory:                  # What the envoy remembers of a trusted peer's thread between messages; off by default, so the envoy carries nothing a peer
                                 # said into its next run (the inbox still files every message and reply)
    enabled: false               # Keep the peer's turns and the envoy's answers per (sender identity, thread) so a follow-up in the same thread is answered
                                 # in context; kept under <cache dir>/mesh/envoy-sessions/<instance id>, owner-only (default: false)
    max_sessions: 256            # Most threads remembered across all peers; the least recently used go first (default: 256; must be 1 or more)
    max_per_identity: 16         # Most threads remembered for one peer identity; the least recently used go first (default: 16; must be 1 or more)
    max_turns: 40                # Most turns kept per thread; the oldest exchange goes first (default: 40; must be 1 or more)
    max_bytes: 65536             # Most bytes of turn text kept per thread; the oldest exchange goes first, and kept turns that do not fit the envoy model's
                                 # context are left out of the run, oldest first, and stay remembered (default: 65536; must be 1 or more)
    ttl_hours: 168               # Hours a thread is remembered after its last written exchange (default: 168; must be 1 or more)

Keys

Key Type Default Meaning Checked
enabled bool false Joins the mesh when the REPL starts with a session or when you enter one (the .mesh on --yes path); validates the block at load and arms the function-calling guard -
announce bool true Announce this node. false = outbound-only: see peers without being seen, and no automatic propagation sync -
display_name string or null null Plaintext label in the announce and the status card. At most 64 bytes, no control or invisible characters at .mesh on
display_name_on_public bool false Carry display_name in announces even when a public interface is configured -
about string or null null One human-written line on what this node's envoy can help with; carried on the status card to trusted peers only, never in the announce <= 200 characters when enabled
interfaces list [{type: private, host: 127.0.0.1, port: 4242}] {type: private|public, host, port} with port 1-65535 (the default is the local rnsd on loopback), or {type: lan} (no host or port; the no-daemon fallback) at parse; count when enabled
brief auto, manual, off auto What the envoy serves; see The brief at parse
digest_prompt string or null null Prompt for the session digest; null or blank uses the built-in prompt -
brief_model string or null null Model for the digest; null = the session's current model -
envoy_model string or null null Model the envoy answers with; null = the configured model. Never follows .model -
envoy_escalation_timeout seconds 0 How long the envoy holds a peer's question for your .mesh answer before handing off; cut to the 120 s run ceiling -
knock_retention_hours hours 24 How long knocks are kept >= 1 when enabled
collision_protection bool false Refuse an identity trusted for all destinations when it claims an instance recorded under another identity, or heard in the peer table under one trusted for all destinations; false = serve it with a warning. See collision_protection -
peer_max_concurrent count 1 Envoy runs one identity can have queued or running at once; 0 = unlimited -
peer_max_messages_per_hour count 60 Messages accepted from one identity per hour; 0 = unlimited -
peer_max_tokens_per_hour count 100000 Model tokens one identity can cost per hour; 0 = unlimited -
peer_max_cost_usd_per_hour number 0 USD one identity can cost per hour; 0 = no ceiling finite and >= 0 when enabled
request_timeout_secs seconds or null null How long to wait for a peer's answer on /message, /knock, /list, /access and /status; raises a path's built-in deadline (15 s; 30 s for /status) and never lowers one; null = each path's own; /fetch and the mesh__peers status sweep keep theirs. See Timers 15 to 31536000 when enabled
link_timeout_secs seconds or null null How long to wait for the link to open and identify before a request on the same five paths; raises the built-in 10 s and never lowers it; null = 10. See Timers 10 to 31536000 when enabled
propagation_sync_interval_secs seconds 300 Seconds between automatic fetches from a propagation node; the first runs once a node is heard after the join; 0 = only on .mesh sync; off while announce is false <= 31536000 when enabled
fetch.inline_max_bytes bytes 65536 Largest file a peer may attach inline to a message; larger files are dropped by the receiver 1 to 98304 when enabled
fetch.max_bytes bytes 4194304 Largest file this node serves to a peer that fetches it; a larger one is refused. See fetch for the ceiling currently applied under it 1 to 4194304 when enabled
fetch.inbox_dir absolute path or null null Where files peers send inline and files this node fetches are staged, under <inbox_dir>/<instance id>; null = <cache dir>/mesh/inbox/<instance id> absolute when set and enabled
envoy_memory.enabled bool false Remember a trusted peer's thread between messages: the peer's turns and what the node sent back, per (proved sender identity, thread), owner-only under <cache dir>/mesh/envoy-sessions/<instance id>; off by default, so nothing a peer says is carried into its next run. See envoy_memory -
envoy_memory.max_sessions count 256 Most threads remembered across all peers; the least recently used go first >= 1 when enabled
envoy_memory.max_per_identity count 16 Most threads remembered for one peer identity; the least recently used go first >= 1 when enabled
envoy_memory.max_turns count 40 Most turns kept per thread; the oldest exchange goes first >= 1 when enabled
envoy_memory.max_bytes bytes 65536 Most bytes of turn text kept per thread; the oldest exchange goes first. Kept turns that do not fit the envoy model's context are left out of the run, oldest first, and stay remembered >= 1 when enabled
envoy_memory.ttl_hours hours 168 Hours a thread is remembered after its last written exchange >= 1 when enabled

.mesh info prints every row as loaded, about, fetch.inline_max_bytes, fetch.max_bytes, fetch.inbox_dir and the six envoy_memory.* keys included; digest_prompt shows default or custom (the prompt body is never printed) and a zero cost ceiling shows 0 (off); a zero sync interval shows 0 (manual), and one made inert by announce: false shows 300 (off: announce is false). With the node on, an envoy memory row follows the node facts: off, or how many conversations of how many identities this instance's store holds.

Unknown keys inside mesh: are ignored, like unknown keys elsewhere in config.yaml. Interface entries are the exception: an unknown key inside one is refused.

Validation

Interface entries are checked whenever the config is parsed, enabled or not:

Input Error
type: lan with host or port type 'lan' takes no host or port; use 'private' or 'public' to attach to a relay
type: private or public missing one type 'private' requires both host and port
blank host type 'private' requires a non-empty host
port: 0 port 0 is out of range; use 1-65535
type: auto type 'auto' was removed and is no longer valid; use 'lan' for link-local discovery (the likely intent), or 'private'/'public' with host and port for a relay
any other type unknown type '<other>'; valid values are lan, private, public
a bare string instead of a mapping a mapping like {type: lan} or {type: private, host: relay.example.com, port: 4242}

The rest is checked only when enabled is true: at load, at .mesh on, and at node start:

Condition Error
function_calling_support: false see Function calling
interfaces: [] mesh.interfaces is empty; list at least one interface (the default is a single {type: private, host: 127.0.0.1, port: 4242} entry, the local rnsd)
two lan entries mesh.interfaces lists lan more than once; only one lan interface can be bound per process
knock_retention_hours: 0 mesh.knock_retention_hours is 0, which is out of range; use 1 or more
a negative or non-finite cost ceiling mesh.peer_max_cost_usd_per_hour is <value>, which is out of range; use 0 (no ceiling) or a positive amount
request_timeout_secs under 15 or over 31536000 mesh.request_timeout_secs is <value>, which is out of range; use 15 (the shortest built-in deadline, which this key raises but never lowers) to 31536000 (one year), or null to keep each path's built-in deadline
link_timeout_secs under 10 or over 31536000 mesh.link_timeout_secs is <value>, which is out of range; use 10 (the shortest built-in deadline, which this key raises but never lowers) to 31536000 (one year), or null to keep each path's built-in deadline
a sync interval over one year mesh.propagation_sync_interval_secs is <value>, which is out of range; use 0 (manual) or up to 31536000 (one year)
about over 200 characters mesh.about is <n> characters, which is over the cap; use 200 or fewer
fetch.inline_max_bytes out of range mesh.fetch.inline_max_bytes is <value>, which is out of range; use 1 to 98304
fetch.max_bytes out of range mesh.fetch.max_bytes is <value>, which is out of range; use 1 to 4194304
a relative fetch.inbox_dir mesh.fetch.inbox_dir is '<value>', which is not absolute; use an absolute path
envoy_memory.max_sessions: 0 mesh.envoy_memory.max_sessions is 0, which is out of range; use 1 or more
envoy_memory.max_per_identity: 0 mesh.envoy_memory.max_per_identity is 0, which is out of range; use 1 or more
envoy_memory.max_turns: 0 mesh.envoy_memory.max_turns is 0, which is out of range; use 1 or more
envoy_memory.max_bytes: 0 mesh.envoy_memory.max_bytes is 0, which is out of range; use 1 or more
envoy_memory.ttl_hours: 0 mesh.envoy_memory.ttl_hours is 0, which is out of range; use 1 or more

display_name is checked when .mesh on encodes the announce: mesh.display_name is N bytes, which is over the 64-byte limit announces carry; shorten it in config.yaml and mesh.display_name contains a control or invisible formatting character (U+XXXX); remove it in config.yaml so peers can read the name.

enabled

enabled: true does three things: the whole block is validated when the config loads, the function-calling guard below is armed, and the REPL joins the mesh whenever it has a session, in any context: before its first prompt when started with --session, and when you enter one later with .session <name> or .agent <name> <session> (an agent's default session included). The join is the .mesh on --yes path: it prints mesh.enabled is true in config.yaml: joining the mesh for this session (.mesh off leaves it)., then the same disclosure of what leaves the machine, and asks nothing. A precondition that .mesh on would refuse (no session, for example) is printed as the same error and the mesh stays off; the REPL goes on. Only the REPL joins: coyote --macro and a prompt given on the command line never start a node, so nothing outlives the command. The mesh needs a session: started without --session the REPL prints Mesh needs a session: ... and joins as soon as .session <name> or .agent <name> <session> has run. A macro run from the prompt whose steps enter a session joins once the macro is done. A typed .mesh off holds for the rest of the run (its notice says so) and the sessions entered after it stay off until .mesh on. .exit session leaves the mesh, and so does .exit agent while the agent holds a session, since the node's instance id belongs to the session being exited, without taking that decision: the next session joins again.

.mesh on sets enabled for the session only and never writes it back to config.yaml. With enabled: false, .mesh on still works: it validates the block as if enabled were true and joins for the session.

propagation_sync_interval_secs

How often the node fetches what a propagation node holds for it; see .mesh sync for what each sync prints. Sideband and NomadNet, the reference clients for LXMF (the messaging layer the mesh runs over), sync every 12 and 6 hours; a REPL that is being talked to waits 5 minutes. 0 turns the automatic fetch off, including the one at join; .mesh sync still works. Each automatic fetch identifies this node to the nearest propagation node heard (the node picks itself; you cannot pick one by hand yet), so a node with announce: false, which is meant to be unseen, never fetches on its own: only .mesh sync, which is you contacting a node, does. The .mesh on preview says which of the three applies.

collision_protection

What happens when an identity you trusted with .mesh trust --identity announces or claims an instance id whose record is bound to another identity, or that the peer table holds under another identity you trusted the same way; see Key changes. A colliding record is marked either way; when the collision is found only in the peer table nothing is marked in either mode. With false, the default, the identity is served and you get a warning: line. With true the collision is judged before the identity grant, so the identity is refused and you get an error: line until you run .mesh trust <new destination>; only in this mode, and only for an identity grant, does the verdict consult the peer table. A refusal found there is remembered for as long as the node runs from the refusal itself, whether or not its error: line reached you, so the old key's row ageing out of the table does not lift it. The memory names the key first heard holding the instance, which it never refuses, whichever key presents first, and refuses the other key only while the key first heard stays trusted for all destinations: .mesh trust <new destination> admits the new key and clears the memory, .mesh block of either key clears it too, and .mesh untrust --identity <old key> admits the new key for as long as the old one stays untrusted. The memory is in-process only (peers.json survives a restart, the memory does not), so after a restart the refusal stands only while the old key's row is still in the table. A destination you trusted explicitly is served in either mode, and an identity with no grant of its own is refused in either mode. It decides what this node serves; what it sends, and the trust column of .mesh peers, is unchanged. The setting is read at .mesh on and shown by .mesh info.

fetch

The three fetch.* keys bound file sharing; the share set, grants and the verbs are on Mesh File Sharing.

max_bytes is what this node is willing to serve, but the limit a request is held to today is min(max_bytes, 1048447), about 1 MiB: the current transport layer can only send a file that fits one segment. A larger file is refused as too_large with the limit actually applied, and a pre-flight on .mesh grant says it is <size> and this node serves at most <limit> bytes. The ceiling goes away once the transport layer can send larger files; max_bytes then applies alone. Raising max_bytes above the ceiling changes nothing today.

inline_max_bytes is the other direction: the largest file a peer may attach inline to a message this node receives. The per-message inline total is 98304 bytes, which is why the key cannot exceed it.

inbox_dir moves the staging inbox. Files peers send inline and files this node fetches land under <inbox_dir>/<instance id>/<peer's full destination hash, lowercase>/<path>; nothing is ever served from the inbox, but keep it outside any workspace you share all the same, and outside any tree you would rather not have peer-written files appear in. Nothing sweeps it: staged files stay until .mesh inbox --purge-files or you remove them.

envoy_memory

The six envoy_memory.* keys govern what the envoy remembers of a trusted peer's thread between messages; what is stored, and what never is, is on Mesh.

enabled is off by default: the envoy starts every run with no memory of the peer, nothing is created under <cache dir>/mesh/envoy-sessions/, and the inbox still files every message and reply. On, the envoy keeps the peer's turns and what the node sent back per (proved sender identity, thread), and a follow-up in the same thread is answered with them in context.

The five bounds are each 1 or more, enforced when a record is written and swept when the node starts and periodically while it runs: max_sessions caps the threads remembered across all peers and max_per_identity the threads of one identity, the least recently used going first in each; max_turns and max_bytes cap one thread, the oldest exchange going first, cut at the next turn the peer spoke so an answer is never kept without what it answered; ttl_hours counts from the thread's last write. Size max_bytes to the envoy model's context window: remembered turns that do not fit it are left out of the run, oldest first, and stay remembered, so a record larger than the window is never read in full.

The store is per instance: a fork (.mesh on --fresh, or a re-key) binds its own <instance id> directory and remembers its own threads, and the envoy memory row of .mesh info and .mesh status counts the running instance's store alone; .mesh memory forget all --dry-run shows the total across every instance store. .mesh memory forget <identity|all> [thread] wipes conversations at will, and .mesh untrust --identity and .mesh block sweep the identity's threads from every instance store under the cache directory, whether or not the memory is on; see Mesh Commands.

Function calling

The mesh sends and receives messages through tools, so it needs function calling on both sides. A config with enabled: true and function_calling_support: false is refused at load:

mesh.enabled is true but function_calling_support is false: the mesh sends and receives messages through tools, so it needs function calling. Set function_calling_support: true in config.yaml, or run .set function_calling_support true in the REPL. Function calling can only be enabled when at least one tool is installed, so install tools first if you have none.

.set function_calling_support false is refused while the node is running and while mesh.enabled is true in memory; the messages are on Mesh Commands.

The peer-spend budget

The four peer_max_* keys bound what one sending identity can make this node accept and spend. Windows are fixed one-hour periods per identity, kept in memory only, so a restart opens a fresh window; 256 identities are tracked. Any one of the four at 0 is unlimited for that budget alone; the others still apply. The message, run and token budgets default to bounded values; the cost ceiling defaults off (spend stays bounded through the token ceiling). Only this node's own config lifts a bounded one: a peer cannot ask for more.

Enforcement happens at two points:

  1. Message admission: peer_max_messages_per_hour. A reply to a question this node asked bypasses it.
  2. Envoy run reservation: peer_max_concurrent first, then the token ceiling, then the cost ceiling. Usage is charged after the run, so runs in flight can overshoot a ceiling by at most peer_max_concurrent runs before the next is refused.

What the two points do with a refused message differs:

  • Message admission (rate_limited). On a live link the message is not filed anywhere and the sender's request is answered with the throttled (0xf6) code, so it reads The peer refused the message: throttled (0xf6) and can retry. A message that arrived by store-and-forward is filed in your inbox without an envoy run, and the sender gets one typed refusal reply per identity per reason per hour.
  • Envoy run reservation (peer_concurrency, token_ceiling, cost_ceiling, and the envoy's own envoy_busy and envoy_stopping). The message is always filed in your inbox for you to read, and the sender always gets a reply for the reasons above.

Either way the REPL shows one folded line per identity per reason per hour. A typed refusal travels as fields: {"refusal": "<reason>", "retry_after_secs": N}; capacity refusals say to retry in 120 s.

Reason What the peer reads
rate_limited This node has taken as many messages from you as it accepts in an hour; try again when the hour is up.
envoy_busy This node's envoy is busy; try again in a couple of minutes.
envoy_stopping No answer: this node is shutting down.
peer_concurrency This node is not taking another message from you until the current one finishes.
token_ceiling This node has spent as many model tokens on you as it allows in an hour; try again when the hour is up.
cost_ceiling This node has spent as much on you as it allows in an hour; try again when the hour is up.

The cost ceiling is enforced only when the envoy model's prices are known.

Timers

Every peer request has a built-in deadline for the answer and another for the link to open and identify first: 15 s / 10 s on /message, /knock, /list and /access, 30 s / 10 s on /status, 120 s / 10 s on /fetch. request_timeout_secs and link_timeout_secs raise those deadlines on the first five paths, for a slow radio link or a peer whose envoy takes its time; neither ever lowers one, so a value under the shortest built-in deadline (15 for the request, 10 for the link) is refused at load, as is one over one year (31536000), the same cap as propagation_sync_interval_secs. /fetch and the status sweep behind mesh__peers with with_status: true (5 s / 5 s) keep their own deadlines whatever the two keys say. .mesh info shows both keys, null while unset. Before any of those deadlines runs, a request to a trusted peer the transport holds no route to (one known only from the saved peer table, after a restart) waits up to 5 s for the network to answer a path request; that wait is fixed, and the status sweep skips it, reporting such a peer unreachable at once instead of waiting for each.

display_name_on_public

The gate is announce-only and all-or-nothing across interfaces: when any entry is public, the display name is withheld from every announce, lan and private included, unless display_name_on_public is true. The status card still carries the name to trusted peers either way, because it reaches trusted destinations and nobody else.

Directories

Variable Holds
COYOTE_CONFIG_DIR mesh/identity.key, mesh/identity.key.lock, mesh/identity.predecessors.jsonl, mesh/trust.yaml, mesh/trust.yaml.lock, mesh/shares.yaml (the global share rules), mesh/shares.yaml.lock
COYOTE_CACHE_DIR mesh/peers.json, mesh/knocks.jsonl, the pending and inbound correlations, the instance locks, mesh/propagation.json, mesh/grants-<instance_id>.jsonl (one-off grants), mesh/inbox/<instance_id>/ (the staging inbox, unless fetch.inbox_dir moves it), mesh/envoy-sessions/<instance id>/ (the envoy memory, owner-only; absent while envoy_memory.enabled is false)

The workspace share rules are in neither: <workspace root>/.coyote/mesh-shares.yaml, where COYOTE_WORKSPACE_CONFIG_DIR renames the .coyote part.

There is no --config-dir flag; the directories are chosen by these environment variables (or XDG_CONFIG_HOME / XDG_CACHE_HOME). Two sessions on one machine need nothing split to run: with one config dir they share the identity and the cache (peers.json is last-writer-wins between them, which is harmless), each gets its own instance id and destination, and all of them dial the local rnsd. mesh/trust.yaml is shared live: a trust, untrust or block made in one REPL is seen by the other before its next decision, and two REPLs never overwrite each other's records; the write is serialised through mesh/trust.yaml.lock. Two separate identities on one machine, to exercise trust from both sides, need both directories split, so that each identity keeps its own trust, knock cache and propagation store; the recipe is on Mesh Deployment. See also Environment Variables.

⚠️ **GitHub.com Fallback** ⚠️