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-1per-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 indispatch.rs. The command type owns the decision; dispatch just asks. - No
build()-signature change and noPublishSpecpollution. 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: onlyconf helpis verbatim;/autotrade offetc. still prettify). - Empty-reply safety. If PM’s
messageis empty/None, it falls back toformat_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
/statusto render all global config, each line tagged[PM]or[producer]by which service owns it. This is deferred because rev9ce3975’sOpsState/OpsCapsonly 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):
| Status | Notes | |
|---|---|---|
| Per-source caps (#730) | Shipped | Pinned 9ce3975, gate green |
Full global-config listing in /status (#732) | Deferred | Needs 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.
Related
- connector-contracts-verification-playbook — how #730 was gated against un-pushed contracts
- telegram-connector-inbound-commands — command dispatch,
format_result, reply correlation - telegram-connector-complete — service overview, ops contracts (
OpsCommand/OpsResult/OpsState/OpsCaps) - position_manager — owner of the ops vocabulary + the #732 contract transport