Skip to content

feat(tools): add ask tool with pending question store and answer correlation - #638

Merged
jamiepine merged 3 commits into
mainfrom
capy/add-ask-tool-with
Aug 10, 2026
Merged

feat(tools): add ask tool with pending question store and answer correlation#638
jamiepine merged 3 commits into
mainfrom
capy/add-ask-tool-with

Conversation

@jamiepine

Copy link
Copy Markdown
Member

Adds an ask tool — a first-class question primitive with selectable options, rendered as buttons or a select menu on platforms that support them, with a numbered-text fallback on text-only channels.

What's included

  • src/tools/ask.rs — The AskTool. Takes a question + 2–10 options, builds InteractiveElements (buttons for ≤5 options, select menu otherwise), sends via the existing RichMessage path, and persists to the question store. Answer enrichment tells the model to end its turn without speculating.

  • src/questions.rsQuestionStore: SQLite-backed CRUD for pending questions. Survives restarts. Timestamps follow project conventions (TIMESTAMP with CURRENT_TIMESTAMP default).

  • migrations/20260810000002_pending_questions.sql — New pending_questions table with indexes for channel lookup and resolution state.

  • src/agent/channel.rs — Answer correlation at both flattening points. Interactions with ask:-prefixed action_ids are enriched from the store: Alice answered "Which environment?": staging. Already-resolved and expired questions get an (expired) marker. Non-ask interactions pass through unchanged.

  • Tool registration and prompt template — Registered in add_channel_tools / remove_channel_tools, available on every channel with allow_direct_reply.

Rendering rules

Condition Rendered as
≤5 options, no multi-select Buttons (one per option)
>5 options or multi-select Select menu
Every channel Numbered text fallback in message body

What works today

Discord works end-to-end with zero adapter changes — the existing RichMessage + Interaction path handles buttons and select menus natively. Text-only channels get the numbered fallback.

Not in this PR

Per the design doc (docs/design-docs/ask-tool.md), the remaining phases are:

  • Phase 2: Telegram InlineKeyboardMarkup + callback_query
  • Phase 3: Slack Block Kit synthesis
  • Phase 4: Portal interactive rendering
  • Phase 5: Parity, docs, and tests

Verification

  • cargo check — zero warnings
  • cargo fmt — clean
  • cargo test --lib — 1150/1150 passed
  • preflight.sh --ci — passed
  • Migration safety — only new file, no existing migrations touched

…elation

Add an `ask` tool that presents the user with a question and selectable
answer options. Renders as buttons or a select menu on platforms that
support interactive elements, and as a numbered list on text-only
channels. Answers arrive as enriched interaction messages that include
the original question context for the model.

- New `AskTool` in `src/tools/ask.rs` — generates `InteractiveElements`
  from options and sends via the existing `RichMessage` path
- New `QuestionStore` in `src/questions.rs` — SQLite-backed pending
  question CRUD with TIMESTAMP columns matching project conventions
- New migration `20260810000002_pending_questions.sql`
- Answer correlation in `src/agent/channel.rs` — resolves pending
  questions on inbound interaction clicks and renders enriched text
- Tool description prompt template + registration in channel tools
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a SQLite-backed pending-question store, a selectable ask tool, channel registration, interactive message delivery, and response enrichment for button and select-menu interactions.

Changes

Ask interaction flow

Layer / File(s) Summary
Pending-question persistence
migrations/20260810000002_pending_questions.sql, src/questions.rs, src/lib.rs
Adds the pending_questions schema and QuestionStore operations for insertion, lookup, conditional resolution, and TTL-based pruning.
Ask tool creation and delivery
src/tools/ask.rs, src/tools.rs, prompts/en/tools/ask_description.md.j2, src/prompts/text.rs
Adds argument validation, button and select-menu rendering, custom-ID parsing, persistence, message delivery, tool-call logging, and channel registration.
Interaction response enrichment
src/agent/channel.rs
Resolves ask interactions, maps selections to option labels, marks questions resolved, and passes enriched content through single-message and batched handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: adding the ask tool, pending-question storage, and answer correlation.
Description check ✅ Passed The description directly explains the ask tool, pending-question store, rendering behavior, answer correlation, scope, and verification.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch capy/add-ask-tool-with

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/tools/ask.rs
};

vec![crate::InteractiveElements::Select {
select: crate::SelectMenu {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

multi_select currently only changes the placeholder. The shared SelectMenu type and Discord renderer never set a max selected count, so Discord string selects still default to one selection. Either carry that through to the renderer, or reject multi_select until the UI can honor it.

Comment thread src/agent/channel.rs Outdated
}

// Resolve the question so duplicate clicks get the expired path
let _ = store.resolve(question_id, &answer_labels).await;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ignores whether the conditional update actually resolved the row. If two clicks arrive at the same time, both can be shown to the model as accepted answers even though only one update wins.

Suggested change
let _ = store.resolve(question_id, &answer_labels).await;
if !store.resolve(question_id, &answer_labels).await.unwrap_or(false) {
return format!("[interaction: {action_id}] (expired)");
}

Comment thread src/questions.rs Outdated
/// already resolved or not found.
pub async fn resolve(&self, question_id: &str, answer: &[String]) -> Result<bool> {
let answer_json = serde_json::to_string(answer).context("failed to serialize answer")?;
let now = now_iso();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolved_at is written as YYYY-MM-DDTHH:MM:SSZ, but prune_expired compares it lexicographically to SQLite datetime('now', ?) values shaped like YYYY-MM-DD HH:MM:SS. Because T sorts after a space, resolved rows on or after the cutoff date can fail to prune. Store resolved_at in the same format as created_at, or compare with datetime(resolved_at).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/questions.rs (1)

73-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Rename the q parameter.

Use question or new_question instead of q.

As per coding guidelines: "Don't abbreviate variable names. Use queue not q, message not msg, channel not ch."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/questions.rs` at line 73, Rename the insert method’s q parameter to
question or new_question, and update every reference within insert to use the
new descriptive name without changing behavior.

Source: Coding guidelines

src/agent/channel.rs (1)

154-157: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Render the values as plain text, not Debug.

{:?} on Vec<String> emits ["a", "b"] with quotes and escapes. This text goes to the model. Join the values instead.

♻️ Proposed fix
             if !values.is_empty() {
-                return format!("[interaction: {action_id} → {:?}]", values);
+                return format!("[interaction: {action_id} → {}]", values.join(", "));
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent/channel.rs` around lines 154 - 157, Update the interaction
formatting branch around the values collection to render non-empty values as
plain text by joining the strings, replacing the Debug-style {:?} formatting in
the format! call. Preserve the existing value-less output and interaction
prefix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/agent/channel.rs`:
- Around line 185-193: Use the Result returned by QuestionStore::resolve in the
answer-handling flow instead of discarding it with let _. Propagate or handle
resolution errors according to the surrounding function’s conventions, and
return the documented expired marker when resolve succeeds with false so
concurrent duplicate clicks do not render an answered message.

In `@src/questions.rs`:
- Around line 63-65: Align the timestamp representation used by now_iso and the
SQLite retention query so resolved_at and created_at are compared consistently
with datetime('now', ?). Prefer emitting the SQLite CURRENT_TIMESTAMP format
from now_iso, or normalize resolved_at with datetime() in prune_expired; apply
the same fix to the related logic around the additional referenced range.
- Line 178: Update the answer retrieval and the analogous fields at the other
highlighted call sites to use `try_get::<Option<String>, _>` without `.ok()`,
preserving SQL NULL as `Ok(None)` while propagating or explicitly handling
decode errors. Do not convert `Err` results into silent `None` values.
- Around line 146-164: Call QuestionStore::prune_expired with
DEFAULT_QUESTION_TTL_DAYS from an appropriate periodic maintenance or startup
path, ensuring the operation runs automatically without requiring an external
caller. Reuse the existing QuestionStore instance and preserve its error
handling while preventing pending_questions from growing indefinitely.

In `@src/tools/ask.rs`:
- Around line 275-282: Remove the full question content from the info-level
tracing in the “ask tool sent question” log within the ask flow. Replace
question = %question with a non-sensitive summary such as its length or a
bounded truncated representation, while preserving the existing question_id,
channel_id, options_count, and multi_select fields.

---

Nitpick comments:
In `@src/agent/channel.rs`:
- Around line 154-157: Update the interaction formatting branch around the
values collection to render non-empty values as plain text by joining the
strings, replacing the Debug-style {:?} formatting in the format! call. Preserve
the existing value-less output and interaction prefix.

In `@src/questions.rs`:
- Line 73: Rename the insert method’s q parameter to question or new_question,
and update every reference within insert to use the new descriptive name without
changing behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6bc9c76f-9fe6-4400-b043-002f9a1eab43

📥 Commits

Reviewing files that changed from the base of the PR and between fac5cc4 and 6c003b6.

📒 Files selected for processing (8)
  • migrations/20260810000002_pending_questions.sql
  • prompts/en/tools/ask_description.md.j2
  • src/agent/channel.rs
  • src/lib.rs
  • src/prompts/text.rs
  • src/questions.rs
  • src/tools.rs
  • src/tools/ask.rs

Comment thread src/agent/channel.rs
Comment on lines +185 to +193
if answer_labels.is_empty() {
return format!("[interaction: {action_id}] (expired — no matching options)");
}

// Resolve the question so duplicate clicks get the expired path
let _ = store.resolve(question_id, &answer_labels).await;

let labels_str = answer_labels.join(", ");
format!("{sender_name} answered \"{}\": {labels_str}", q.question)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the result of resolve; do not discard it.

QuestionStore::resolve returns Result<bool>. The false value is the duplicate-click guard: it means another click already resolved the question. Line 190 discards both the error and the boolean, so this function still renders answered "…" for a losing concurrent click. The doc comment on line 142 promises the (expired) marker for that case. The get check on line 164 is a separate read, so a check-then-act race is possible between two clicks.

Branch on the returned value.

🐛 Proposed fix
-            // Resolve the question so duplicate clicks get the expired path
-            let _ = store.resolve(question_id, &answer_labels).await;
-
-            let labels_str = answer_labels.join(", ");
-            format!("{sender_name} answered \"{}\": {labels_str}", q.question)
+            // Resolve the question so duplicate clicks get the expired path.
+            // The conditional UPDATE is the authoritative winner check.
+            match store.resolve(question_id, &answer_labels).await {
+                Ok(true) => {
+                    let labels_str = answer_labels.join(", ");
+                    format!("{sender_name} answered \"{}\": {labels_str}", q.question)
+                }
+                Ok(false) => format!("[interaction: {action_id}] (expired)"),
+                Err(error) => {
+                    tracing::warn!(%error, question_id, "failed to resolve pending question");
+                    format!("[interaction: {action_id}] (expired)")
+                }
+            }

As per coding guidelines: "Don't silently discard errors. No let _ = on Results. Handle them, log them, or propagate them."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if answer_labels.is_empty() {
return format!("[interaction: {action_id}] (expired — no matching options)");
}
// Resolve the question so duplicate clicks get the expired path
let _ = store.resolve(question_id, &answer_labels).await;
let labels_str = answer_labels.join(", ");
format!("{sender_name} answered \"{}\": {labels_str}", q.question)
if answer_labels.is_empty() {
return format!("[interaction: {action_id}] (expired — no matching options)");
}
// Resolve the question so duplicate clicks get the expired path.
// The conditional UPDATE is the authoritative winner check.
match store.resolve(question_id, &answer_labels).await {
Ok(true) => {
let labels_str = answer_labels.join(", ");
format!("{sender_name} answered \"{}\": {labels_str}", q.question)
}
Ok(false) => format!("[interaction: {action_id}] (expired)"),
Err(error) => {
tracing::warn!(%error, question_id, "failed to resolve pending question");
format!("[interaction: {action_id}] (expired)")
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/agent/channel.rs` around lines 185 - 193, Use the Result returned by
QuestionStore::resolve in the answer-handling flow instead of discarding it with
let _. Propagate or handle resolution errors according to the surrounding
function’s conventions, and return the documented expired marker when resolve
succeeds with false so concurrent duplicate clicks do not render an answered
message.

Source: Coding guidelines

Comment thread src/questions.rs Outdated
Comment thread src/questions.rs
Comment on lines +146 to +164
pub async fn prune_expired(&self, ttl_days: i64) -> Result<u64> {
let cutoff = format!("-{} days", ttl_days);

let affected = sqlx::query(
r#"
DELETE FROM pending_questions
WHERE (resolved_at IS NOT NULL AND resolved_at < datetime('now', ?))
OR (resolved_at IS NULL AND created_at < datetime('now', ?))
"#,
)
.bind(&cutoff)
.bind(&cutoff)
.execute(&self.pool)
.await
.context("failed to prune expired questions")?
.rows_affected();

Ok(affected)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find callers of prune_expired and DEFAULT_QUESTION_TTL_DAYS.
rg -nP -C3 '\bprune_expired\s*\(' --type=rust
rg -nP -C3 '\bDEFAULT_QUESTION_TTL_DAYS\b' --type=rust

Repository: spacedriveapp/spacebot

Length of output: 160


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- tracked Rust files ---'
git ls-files '*.rs' | sed -n '1,120p'
printf '%s\n' '--- all tracked references ---'
rg -n -P -C3 '\bprune_expired\s*\(|\bDEFAULT_QUESTION_TTL_DAYS\b' --hidden -g '!target' -g '!node_modules' . || true
printf '%s\n' '--- questions.rs structure ---'
ast-grep outline src/questions.rs --lang rust || true
printf '%s\n' '--- questions.rs relevant range ---'
cat -n src/questions.rs | sed -n '1,220p'

Repository: spacedriveapp/spacebot

Length of output: 12465


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
import subprocess
from pathlib import Path

files = subprocess.check_output(["git", "ls-files", "*.rs"], text=True).splitlines()
patterns = ("prune_expired", "DEFAULT_QUESTION_TTL_DAYS", "QuestionStore")
for pattern in patterns:
    matches = []
    for filename in files:
        text = Path(filename).read_text(errors="replace").splitlines()
        for number, line in enumerate(text, 1):
            if pattern in line:
                matches.append(f"{filename}:{number}:{line.strip()}")
    print(f"--- {pattern} ({len(matches)} matches) ---")
    print("\n".join(matches) or "(none)")
PY
printf '%s\n' '--- non-Rust references ---'
rg -n -P -C2 '\bprune_expired\b|\bDEFAULT_QUESTION_TTL_DAYS\b|\bQuestionStore\b' --hidden -g '!target' -g '!node_modules' . || true

Repository: spacedriveapp/spacebot

Length of output: 3597


Schedule QuestionStore::prune_expired(DEFAULT_QUESTION_TTL_DAYS)

No caller exists. Without periodic pruning, pending_questions can grow without bound.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/questions.rs` around lines 146 - 164, Call QuestionStore::prune_expired
with DEFAULT_QUESTION_TTL_DAYS from an appropriate periodic maintenance or
startup path, ensuring the operation runs automatically without requiring an
external caller. Reuse the existing QuestionStore instance and preserve its
error handling while preventing pending_questions from growing indefinitely.

Comment thread src/questions.rs Outdated
Comment thread src/tools/ask.rs
- Add #[allow(clippy::too_many_arguments)] to AskTool::new (8 args)
- Rename q parameter to question in QuestionStore::insert
- Align now_sqlite timestamp format with SQLite CURRENT_TIMESTAMP so
  datetime comparisons in prune_expired are consistent
- Replace .ok() with try_get::<Option<String>, _> for proper error
  propagation in question_from_row
- Log resolve failures instead of silently discarding them
- Remove full question text from info-level tracing (security)
- Auto-prune expired questions on insert to prevent unbounded growth
- Replace Debug-style {:?} formatting of values with plain join

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/questions.rs (2)

146-147: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Reject negative ttl_days values.

For ttl_days = -1, the code builds "--1 days". SQLite evaluates this invalid modifier as NULL, so both delete predicates match no rows. Validate ttl_days before constructing cutoff.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/questions.rs` around lines 146 - 147, Update prune_expired to validate
ttl_days before constructing the cutoff string, rejecting negative values with
the function’s existing error type; preserve the current expiration-pruning
behavior for zero and positive values.

122-141: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Scope question resolution to the interaction channel.

pending_questions stores channel_id, but get and resolve match only question_id. Pass the inbound channel ID through and include it in both predicates. Do not rely only on message routing for this authorization boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/questions.rs` around lines 122 - 141, Update the pending-question lookup
and resolution flow, including get and resolve, to accept the inbound channel ID
and include channel_id alongside question_id in both SQL predicates. Propagate
the channel ID from the interaction entry point through these calls, preserving
the existing unresolved-question condition in resolve.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/questions.rs`:
- Around line 146-147: Update prune_expired to validate ttl_days before
constructing the cutoff string, rejecting negative values with the function’s
existing error type; preserve the current expiration-pruning behavior for zero
and positive values.
- Around line 122-141: Update the pending-question lookup and resolution flow,
including get and resolve, to accept the inbound channel ID and include
channel_id alongside question_id in both SQL predicates. Propagate the channel
ID from the interaction entry point through these calls, preserving the existing
unresolved-question condition in resolve.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: deab35a0-3737-47f4-b819-4ae45f017e95

📥 Commits

Reviewing files that changed from the base of the PR and between 6c003b6 and 99dff0c.

📒 Files selected for processing (2)
  • src/questions.rs
  • src/tools/ask.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/tools/ask.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/questions.rs (1)

97-107: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Coalesce background pruning and index both expiration branches.

Each successful QuestionStore::insert starts a pruning task. Overlapping prune_expired deletes repeat work and contend for SQLite writes during bursts. The current plan filters created_at after using resolved_at for unanswered rows.

Run pruning once per interval or from one periodic task. Add a composite (resolved_at, created_at) index and verify both expiration branches use it. Keep cleanup atomic if you split the OR condition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/questions.rs` around lines 97 - 107, Coalesce the background pruning
launched by QuestionStore::insert into a single periodic or otherwise
deduplicated task so concurrent inserts do not overlap prune_expired calls.
Update the pending_questions schema with a composite index on (resolved_at,
created_at), and adjust prune_expired so both expiration branches can use the
index while preserving atomic cleanup when splitting the OR condition.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/questions.rs`:
- Around line 97-107: Coalesce the background pruning launched by
QuestionStore::insert into a single periodic or otherwise deduplicated task so
concurrent inserts do not overlap prune_expired calls. Update the
pending_questions schema with a composite index on (resolved_at, created_at),
and adjust prune_expired so both expiration branches can use the index while
preserving atomic cleanup when splitting the OR condition.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e512095-f2bc-49e6-abc3-a71f8ac884b6

📥 Commits

Reviewing files that changed from the base of the PR and between 99dff0c and 3326b1a.

📒 Files selected for processing (3)
  • src/agent/channel.rs
  • src/questions.rs
  • src/tools/ask.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/tools/ask.rs
  • src/agent/channel.rs

@jamiepine
jamiepine merged commit 70413c6 into main Aug 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant