Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,23 @@ st2 agents --json --enrich
st2 context read --full
```

Use one optional key when a sender can retry the same message:

```sh
check_status | st2 message send <recipient> \
--idempotency-key daily-check-2026-07-31 \
--subject "Daily check"
```

st2 stores the key in the normal message frontmatter. It takes a short local
lock and searches the recipient inbox first. It then searches the archive. A
retry returns the first matching filename. It does not create another inbox
file or another DING. If the archived message is deleted, st2 forgets the key.

This rule applies to one recipient on one local catalog filesystem. It is not a
global exactly-once rule across replicas. Without `--idempotency-key`, `message
send` keeps its existing bytes, filename output, and inbox behavior.

The roster includes retired declarations instead of silently conflating them with runtime
presence. Both JSON shapes keep stable `identity` separate from optional `name` and `description`,
and contain `retired` plus the declaration's ordered `resources` descriptors. `--enrich`
Expand Down
1 change: 1 addition & 0 deletions src/ding/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,7 @@ mod tests {
in_reply_to: None,
tags: vec![],
priority: None,
idempotency_key: None,
body: String::new(),
}
}
Expand Down
53 changes: 51 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,9 @@ enum MessageCmd {
/// Comma-separated tags.
#[arg(long, value_delimiter = ',')]
tags: Vec<String>,
/// Return the first matching normal message for this local recipient and key.
#[arg(long = "idempotency-key")]
idempotency_key: Option<String>,
#[command(flatten)]
ctx: MsgCtx,
},
Expand Down Expand Up @@ -1658,6 +1661,7 @@ fn message_cmd(cmd: MessageCmd) -> Result<()> {
subject,
in_reply_to,
tags,
idempotency_key,
ctx,
} => {
let (root, host) = resolve_ctx(&ctx)?;
Expand All @@ -1672,6 +1676,7 @@ fn message_cmd(cmd: MessageCmd) -> Result<()> {
in_reply_to.as_deref(),
&tags,
&body,
idempotency_key.as_deref(),
)?;
println!("{filename}");
Ok(())
Expand Down Expand Up @@ -1702,6 +1707,7 @@ fn message_cmd(cmd: MessageCmd) -> Result<()> {
Some(&filename),
&[],
&body,
None,
)?;
println!("{sent}");
Ok(())
Expand Down Expand Up @@ -1793,6 +1799,9 @@ fn message_cmd(cmd: MessageCmd) -> Result<()> {
if !m.tags.is_empty() {
println!("tags: {}", m.tags.join(", "));
}
if let Some(key) = &m.idempotency_key {
println!("idempotency-key: {key}");
}
println!();
print!("{}", m.body);
Ok(())
Expand Down Expand Up @@ -1847,12 +1856,46 @@ fn send_resolved_message(
in_reply_to: Option<&str>,
tags: &[String],
body: &str,
idempotency_key: Option<&str>,
) -> Result<String> {
if std::env::var("ST2_EVAL_REQUESTER").as_deref() == Ok(to) {
let inbox = resolve_message_inbox(root, to, host)?;
message::send_to_inbox(&inbox, from, subject, in_reply_to, tags, body)
match idempotency_key {
Some(key) => message::send_idempotent_to_inbox(
&inbox,
from,
subject,
in_reply_to,
tags,
body,
key,
),
None => message::send_to_inbox(&inbox, from, subject, in_reply_to, tags, body),
}
} else {
message::send_to_resolved_inbox(root, to, host, from, subject, in_reply_to, tags, body)
match idempotency_key {
Some(key) => message::send_idempotent_to_resolved_inbox(
root,
to,
host,
from,
subject,
in_reply_to,
tags,
body,
key,
),
None => message::send_to_resolved_inbox(
root,
to,
host,
from,
subject,
in_reply_to,
tags,
body,
),
}
}
}

Expand All @@ -1867,6 +1910,8 @@ struct LsItemJson<'a> {
in_reply_to: Option<&'a str>,
tags: &'a [String],
priority: Option<&'a str>,
#[serde(rename = "idempotencyKey", skip_serializing_if = "Option::is_none")]
idempotency_key: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
body: Option<&'a str>,
}
Expand All @@ -1881,6 +1926,7 @@ impl<'a> From<&'a st2::message::Message> for LsItemJson<'a> {
in_reply_to: m.in_reply_to.as_deref(),
tags: &m.tags,
priority: m.priority.as_deref(),
idempotency_key: m.idempotency_key.as_deref(),
body: None,
}
}
Expand All @@ -1907,6 +1953,8 @@ struct MessageJson<'a> {
in_reply_to: Option<&'a str>,
tags: &'a [String],
priority: Option<&'a str>,
#[serde(rename = "idempotencyKey", skip_serializing_if = "Option::is_none")]
idempotency_key: Option<&'a str>,
body: &'a str,
}

Expand All @@ -1920,6 +1968,7 @@ impl<'a> From<&'a st2::message::Message> for MessageJson<'a> {
in_reply_to: m.in_reply_to.as_deref(),
tags: &m.tags,
priority: m.priority.as_deref(),
idempotency_key: m.idempotency_key.as_deref(),
body: &m.body,
}
}
Expand Down
Loading
Loading