Shipped the connector half of per-source spend caps (babylon task #730 from position_manager). Pinned pmv2-contracts 9ce3975, gate green. Captures the non-obvious reply_is_verbatim dispatch pattern, the DRY decision to let PM own the canonical config-key list, and why the #732 global-config listing is deferred as a fast-follow.

For Agents

#730 added per-source config overrides via /autotrade conf [source] <key> <value> (a -1 per-source set clears the override). Connector-side this needed (a) a way to print PM’s reply raw for /autotrade conf help, and (b) removal of the connector’s stale hardcoded config-key list. #732 (render ALL global config in /status) is a separate, deferred fast-follow — see below.

Pattern — verbatim command replies (reply_is_verbatim)

Problem

The connector’s format_result() prettifies an OpsResult for chat — it prefixes the message, e.g. ✅ applied — <message>. That’s right for state-changing commands, but wrong for a command whose reply is itself a formatted listing (the canonical config-key help that PM owns). /autotrade conf help must be printed exactly as PM sent it, with no ✅ applied — prefix.

Solution — one polymorphic trait method, no dispatch special-casing

Added a default trait method to Command (crates/telegram_connector/src/inbound/command.rs):

pub trait Command: Send + Sync {
    fn name(&self) -> &str;
    fn min_role(&self) -> Role;
    fn expects_reply(&self) -> bool { false }
    fn reply_is_verbatim(&self, _args: &[&str]) -> bool { false }   // default: prettify
    fn build(&self, args: &[&str], ctx: &CommandCtx) -> Result<PublishSpec, String>;
}

AutotradeCommand overrides it to return true for exactly ["conf", "help"] (crates/telegram_connector/src/inbound/ops_commands.rs):

fn reply_is_verbatim(&self, args: &[&str]) -> bool {
    args == ["conf", "help"]
}

Dispatch reads the flag (args-dependent, so it’s per-invocation not per-command) and branches only when the reply arrives (crates/telegram_connector/src/inbound/dispatch.rs):

let verbatim = cmd.reply_is_verbatim(&args);
// …publish, await reply…
Ok(Ok(result)) => Reply::Text(if verbatim {
    result.message
        .as_deref()
        .filter(|m| !m.is_empty())
        .map_or_else(|| format_result(&result), ToString::to_string)  // raw, with safe fallback
} else {
    format_result(&result)                                            // "✅ applied — …"
}),

Why this shape

  • Dispatch stays polymorphic. No if command == "autotrade" && args == … branch in dispatch.rs. The command type owns the decision; dispatch just asks.
  • No build()-signature change and no PublishSpec pollution. Verbatim-ness is a reply-rendering concern, not a publish concern, so it lives on its own trait method — not smuggled into the publish payload.
  • Args-aware, not command-aware. reply_is_verbatim(&args) lets one command be verbatim for some arg sets and prettified for others (here: only conf help is verbatim; /autotrade off etc. still prettify).
  • Empty-reply safety. If PM’s message is empty/None, it falls back to format_result — a verbatim command can never produce a blank chat reply.

Decision — PM owns the canonical config-key list (DRY)

The connector previously carried a hardcoded CONF_HELP listing of valid config keys. That duplicated PM’s source of truth and would silently drift whenever PM added/renamed a key.

Replaced it with a short static CONF_USAGE that points the operator at PM’s authoritative listing instead of re-stating the keys (crates/telegram_connector/src/inbound/ops_commands.rs):

const CONF_USAGE: &str = "usage: /autotrade conf <key> <value>  ·  per-source: \
/autotrade conf <source> <key> <value>  ·  list keys: /autotrade conf help  ·  \
-1 on a per-source set clears the override";

/autotrade conf help now round-trips to PM (OpsCmd::ConfigHelp), and its reply is printed verbatim (see pattern above) so the connector never has to know the key list. The connector owns the grammar; PM owns the vocabulary. Single source of truth, no drift.

Decision — #732 (full global-config in /status) deferred as a fast-follow

Blocked on a contract transport, not on connector work

#732 wants /status to render all global config, each line tagged [PM] or [producer] by which service owns it. This is deferred because rev 9ce3975’s OpsState / OpsCaps only carries the 4 PM caps — there is no carrier for the ~13 producer fields (base_usd, min_size_usd, score_threshold, …). The connector literally cannot render values it isn’t sent.

The fast-follow is gated on PM cutting a pmv2-contracts rev whose OpsState/OpsCaps actually delivers the full config values (PM caps + producer fields). Until that transport exists, there is nothing for the connector to display.

What shipped now (#730) vs. what waits (#732):

StatusNotes
Per-source caps (#730)ShippedPinned 9ce3975, gate green
Full global-config listing in /status (#732)DeferredNeeds a contract rev carrying all ~13 producer fields, not just the 4 PM caps

How it was built/gated

Built and tested the connector against PM’s un-pushed contract change using the local-path [patch] technique, then re-pinned to the published rev 9ce3975 and re-ran the gate green. Full method: connector-contracts-verification-playbook.