Skip to content

feat(minimal,minimald): confirm single-session destroy with the workspace delta - #1148

Merged
norrietaylor merged 4 commits into
mainfrom
feat/destroy-dirty-gate
Jul 31, 2026
Merged

feat(minimal,minimald): confirm single-session destroy with the workspace delta#1148
norrietaylor merged 4 commits into
mainfrom
feat/destroy-dirty-gate

Conversation

@norrietaylor

@norrietaylor norrietaylor commented Jul 31, 2026

Copy link
Copy Markdown
Member

min session destroy deletes unsaved in-session work without a word; it now lists the files changed since activation and confirms with default No — and headless runs refuse without --force, so EOF can never read as consent.

  • A small oneshot RPC serves the running host's delta baseline (merged with the exit-prompt work); rows render exactly as the exit prompt's.
  • Headless refusal mirrors the existing --all precedent; --force is unhooked from --all and skips the confirm.
  • Delta unavailable (stopped session, error, timeout) degrades to the plain confirm; destroy never blocks on the listing. --all unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KyZLpkRf9G4A2hUDgDvn5f

Note

Add confirmation prompt with workspace delta to single-session destroy

  • Single-session min session destroy now prompts for confirmation before destroying, showing changed files since activation when available (up to 10 rows).
  • A new destroy_gate function decides whether to prompt or proceed: --force skips the prompt; headless runs (no TTY or --no-input) without --force return an error instructing the user to pass --force.
  • A new SessionDelta RPC fetches changed files from the daemon with a 6-second timeout; timeouts and errors silently map to None, so the prompt still appears without the file listing.
  • The --force flag is no longer restricted to --all and now works for single-session destroys as well.
  • Behavioral Change: headless single-session destroy without --force now fails with an explicit error instead of proceeding.

Changes since #1148 opened

  • Introduced VCS-aware workspace at-risk assessment in the daemon [87280ea]
  • Replaced SessionDeltaRequest enum with struct and introduced tagged SessionDeltaResponse enum in minimald-rpc [87280ea]
  • Implemented at-risk state classification and destroy gate logic in minimal client [87280ea]
  • Updated tests to verify new destroy gate behavior and at-risk classification [87280ea]
  • Updated end-to-end test script to reflect new destroy gate behavior [87280ea]

Macroscope summarized c10c935.

Summary by CodeRabbit

  • New Features

    • Added confirmation prompts before destroying sessions.
    • Added --force support for destroying individual sessions or all sessions.
    • Display up to 10 files changed since session activation before confirmation.
    • Added workspace change detection for active sessions.
  • Bug Fixes

    • Headless and no-input commands now safely refuse destruction unless --force is provided.
    • Unavailable change information now falls back to the standard confirmation prompt.

…pace delta

min session destroy deleted unsaved in-session work without a word. The
single-session path now lists the files changed since activation (a new
oneshot RPC serving the host's delta baseline) and confirms with
default No; headless runs refuse without --force, which is unhooked
from --all. Delta failures degrade to a plain confirm — destroy never
blocks on the listing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KyZLpkRf9G4A2hUDgDvn5f
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a SessionDelta RPC and asynchronous workspace-delta APIs. min session destroy <session> now confirms interactively, displays available changed files, requires --force in headless modes, and accepts --force for single-session destruction.

Changes

Session destruction confirmation

Layer / File(s) Summary
Workspace delta RPC contract and dispatch
crates/minimald-rpc/src/lib.rs, crates/minimald/src/rpc.rs
Adds SessionDelta requests by name or ID, optional changed-file responses, daemon handling, SSH registration, dispatch, and serialization tests.
Workspace delta actor plumbing
crates/minimald/src/session.rs, crates/minimald/src/session_host.rs
Adds asynchronous session and host actor requests for changed workspace files. Unavailable deltas return None. Integration tests cover available and unavailable results.
Destroy confirmation and force handling
crates/minimal/src/lib.rs
Adds interactive confirmation with a bounded changed-file display. Headless and --no-input execution requires --force. Parsing and behavior tests cover single-session force handling.

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

Possibly related issues

  • gominimal/inbox#471 — Covers the same session-destroy confirmation, headless refusal, force behavior, and workspace-delta changes.
  • gominimal/inbox#436 — Covers related workspace-delta retrieval and changed-file prompts for session lifecycle actions.

Possibly related PRs

Suggested labels: needs-human

Suggested reviewers: twitchyliquid64

Poem

A rabbit checks the changed-file trail,
Then guards the destroy command without fail.
“Force it,” I say, “when prompts cannot appear!”
The session delta makes the path clear.
Hop, hop—the actors keep the host loop near.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title is concise, uses Conventional Commit format, and clearly describes the single-session destroy confirmation and workspace delta change.
Description check ✅ Passed The description clearly explains the behavior changes and implementation, but it omits the required Testing and Checklist sections.
✨ 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 feat/destroy-dirty-gate

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

@norrietaylor
norrietaylor marked this pull request as ready for review July 31, 2026 21:37
@norrietaylor
norrietaylor requested a review from a team as a code owner July 31, 2026 21:37
…w dirty gate

The headless destroy of the --keep session now trips the gate this PR
introduces — which is the feature working. The teardown asserts the
refusal (and that it names --force) before destroying with --force.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KyZLpkRf9G4A2hUDgDvn5f
@norrietaylor

Copy link
Copy Markdown
Member Author

The native-daemon-e2e failure was the feature firing: the e2e's kept-session teardown destroys headlessly without --force, which this PR now refuses. c369897d updates the teardown to assert the refusal (including that it names --force) and then destroy with --force — the breakage becomes the e2e proof of the gate.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
crates/minimal/src/lib.rs (1)

2390-2411: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared row-cap/formatting into one place.

print_destroy_delta re-implements the header text, row cap, and "... and N more" line that the shell-exit prompt leads with — an unavailable delta renders the plain prompt so the exit path never blocks on change detection already renders in crates/minimald/src/session_host.rs's shell_exit_prompt. The code even documents the coupling as a comment rather than a shared source of truth: "Cap on rows printed, matching the shell-exit prompt's."

Two independently maintained copies of the same wording and magic number (10) will drift silently if one side changes. Move the row cap (and ideally the row-rendering logic) into minimald-rpc, which both crates/minimal and crates/minimald already depend on, so both call sites share one constant/function.

♻️ Suggested direction
-fn print_destroy_delta(changed: &Option<Vec<String>>) {
-    /// Cap on rows printed, matching the shell-exit prompt's.
-    const ROWS_SHOWN: usize = 10;
+fn print_destroy_delta(changed: &Option<Vec<String>>) {
+    use minimald_rpc::DELTA_ROWS_SHOWN as ROWS_SHOWN;
     match changed {

(Define pub const DELTA_ROWS_SHOWN: usize = 10; once in minimald-rpc, and have session_host.rs reference the same constant instead of its own local one.)

🤖 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 `@crates/minimal/src/lib.rs` around lines 2390 - 2411, Extract the shared delta
row limit into minimald-rpc as a public DELTA_ROWS_SHOWN constant, then update
print_destroy_delta and shell_exit_prompt in session_host.rs to use it instead
of local magic numbers or duplicated limits. Preserve the existing header, row,
and overflow wording while centralizing the shared cap.
🤖 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 `@crates/minimal/src/lib.rs`:
- Around line 2368-2388: The session_delta timeout path must clean up the
timed-out SSH channel before the shared client is reused by destroy_session.
Update session_delta or its caller around client.oneshot_rpc to explicitly close
the channel during cancellation, or recreate the SSH client before
destroy_session, while preserving the existing best-effort None behavior for RPC
failures and timeouts.

---

Nitpick comments:
In `@crates/minimal/src/lib.rs`:
- Around line 2390-2411: Extract the shared delta row limit into minimald-rpc as
a public DELTA_ROWS_SHOWN constant, then update print_destroy_delta and
shell_exit_prompt in session_host.rs to use it instead of local magic numbers or
duplicated limits. Preserve the existing header, row, and overflow wording while
centralizing the shared cap.
🪄 Autofix (Beta)

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

Run ID: 9284235a-0cb6-47ef-ad1c-42ab3f1129ab

📥 Commits

Reviewing files that changed from the base of the PR and between 4a738d4 and 9dfb3ac.

📒 Files selected for processing (5)
  • crates/minimal/src/lib.rs
  • crates/minimald-rpc/src/lib.rs
  • crates/minimald/src/rpc.rs
  • crates/minimald/src/session.rs
  • crates/minimald/src/session_host.rs

Comment thread crates/minimal/src/lib.rs Outdated
Comment on lines +2368 to +2388
/// Best-effort fetch of the files changed since activation, for the destroy
/// confirm. Any failure — RPC error, timeout, a daemon predating the RPC —
/// reads as `None`, and the confirm renders without the listing.
async fn session_delta(
client: &mut client::Client,
id: sessions::SessionId,
) -> Option<Vec<String>> {
/// Client-side ceiling on the delta fetch. The daemon bounds its
/// workspace walk at 5 s; this sits just above so a slow-but-healthy
/// walk still answers while a wedged daemon cannot stall the confirm.
const SESSION_DELTA_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(6);
use minimald_rpc::{SessionDelta, SessionDeltaRequest};
tokio::time::timeout(
SESSION_DELTA_TIMEOUT,
client.oneshot_rpc::<SessionDelta>(SessionDeltaRequest::Id(id)),
)
.await
.ok()?
.ok()?
.changed
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does russh guarantee that dropping an in-flight client channel operation (mid channel_open_session or mid read) leaves the client::Handle safe to reuse for further channels?

💡 Result:

Yes, dropping an in-flight client channel operation in the russh library does not invalidate the client::Handle, and the handle remains safe to reuse for further channels [1][2]. The russh::client::Handle is designed as an interface to send messages to the SSH session asynchronously [1][2]. It uses an internal MPSC (multi-producer, single-consumer) channel (via the sender field) to communicate with the session's event loop, which manages multiplexing and channel state [3][4][2]. Because the Handle acts as a decoupled sender, dropping a future representing an in-flight operation (such as channel_open_session or a read/write operation) only cancels that specific pending request; it does not shut down the session or invalidate the underlying Handle [1][3]. Since the event loop and session state are managed independently of the specific futures created by the Handle, you can continue to use the same Handle to open new channels or send further messages after an operation is dropped [1][2]. If an operation is cancelled, the session will continue to process other messages normally, provided the SSH connection itself remains healthy [4][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- russh dependency declarations ---'
rg -n -C 3 'russh' --glob 'Cargo.toml' --glob 'Cargo.lock' .
printf '%s\n' '--- Client and oneshot_rpc definitions/usages ---'
rg -n -C 8 'struct Client|fn oneshot_rpc|channel_open_session|destroy_session' crates
printf '%s\n' '--- vendored or cached russh sources ---'
find . -path '*/russh*' -type f -maxdepth 6 2>/dev/null | head -80
find "${CARGO_HOME:-/nonexistent}" -path '*russh*' -type f 2>/dev/null | head -80

Repository: gominimal/minimal

Length of output: 47483


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- minimal Client::oneshot_rpc ---'
sed -n '220,305p' crates/minimal/src/client.rs
printf '%s\n' '--- russh 0.62.4 source locations from crates.io metadata ---'
curl --fail --silent --show-error https://crates.io/api/v1/crates/russh/0.62.4 \
  | jq -r '.version.downloads, .crate, .repository'
printf '%s\n' '--- russh 0.62.4 source archive inspection ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl --fail --silent --show-error -L https://static.crates.io/crates/russh/russh-0.62.4.crate \
  -o "$tmpdir/russh.crate"
tar -xzf "$tmpdir/russh.crate" -C "$tmpdir"
rg -n -C 8 'pub struct Handle|channel_open_session|struct Channel|impl.*Drop|channel_open' \
  "$tmpdir/russh-0.62.4/src"

Repository: gominimal/minimal

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl --fail --silent --show-error -L \
  https://static.crates.io/crates/russh/russh-0.62.4.crate \
  -o "$tmpdir/russh.crate"
tar -xzf "$tmpdir/russh.crate" -C "$tmpdir"
src="$tmpdir/russh-0.62.4/src"

printf '%s\n' '--- client Handle and channel_open_session ---'
rg -n -C 20 'pub struct Handle|pub async fn channel_open_session|wait_channel_confirmation' \
  "$src/client" "$src/server"

printf '%s\n' '--- channel ownership and drop behavior ---'
rg -n -C 12 'ChannelCloseOnDrop|impl.*Drop|channel_close|ChannelMsg::Close|close\(' \
  "$src/channels" "$src/client"

printf '%s\n' '--- pending channel-open state and reply handling ---'
rg -n -C 16 'PendingChannelOpen|ChannelOpenReply|channel_ref|oneshot' \
  "$src/client" "$src/channels"

Repository: gominimal/minimal

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl --fail --silent --show-error -L \
  https://static.crates.io/crates/russh/russh-0.62.4.crate \
  -o "$tmpdir/russh.crate"
tar -xzf "$tmpdir/russh.crate" -C "$tmpdir"
src="$tmpdir/russh-0.62.4/src"

printf '%s\n' '--- client channel-open implementation ---'
sed -n '270,310p' "$src/client/mod.rs"
sed -n '580,625p' "$src/client/mod.rs"
sed -n '680,705p' "$src/client/mod.rs"

printf '%s\n' '--- client message definitions and event-loop handling ---'
rg -n -C 12 'enum Msg|ChannelOpenSession|ChannelOpenReply|channels:|HashMap.*Channel|channels\.insert|channels\.remove' \
  "$src/client" "$src"

printf '%s\n' '--- channel reference and channel-close-on-drop implementation ---'
rg -n -C 20 'struct ChannelRef|impl.*ChannelRef|struct ChannelCloseOnDrop|impl.*ChannelCloseOnDrop|Drop for Channel' \
  "$src/channels" "$src"

Repository: gominimal/minimal

Length of output: 50374


🏁 Script executed:

#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl --fail --silent --show-error -L \
  https://static.crates.io/crates/russh/russh-0.62.4.crate \
  -o "$tmpdir/russh.crate"
tar -xzf "$tmpdir/russh.crate" -C "$tmpdir"
src="$tmpdir/russh-0.62.4/src"

printf '%s\n' '--- ChannelRef definition ---'
rg -n 'struct ChannelRef|impl.*ChannelRef|ChannelRef' "$src/channels/mod.rs"
sed -n '80,145p' "$src/channels/mod.rs"

printf '%s\n' '--- confirmation and failure handling ---'
sed -n '330,455p' "$src/client/encrypted.rs"

printf '%s\n' '--- channel-close handling ---'
sed -n '390,430p' "$src/client/encrypted.rs"
sed -n '1335,1360p' "$src/client/mod.rs"

printf '%s\n' '--- low-level channel allocation ---'
rg -n -C 10 'fn channel_open_generic|fn channel_open_session' \
  "$src/client/session.rs" "$src/session.rs"

Repository: gominimal/minimal

Length of output: 13697


🏁 Script executed:

#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl --fail --silent --show-error -L \
  https://static.crates.io/crates/russh/russh-0.62.4.crate \
  -o "$tmpdir/russh.crate"
tar -xzf "$tmpdir/russh.crate" -C "$tmpdir"

python3 - "$tmpdir/russh-0.62.4/src" <<'PY'
import pathlib, re, sys

root = pathlib.Path(sys.argv[1])
client_mod = (root / "client/mod.rs").read_text()
client_encrypted = (root / "client/encrypted.rs").read_text()
channels_mod = (root / "channels/mod.rs").read_text()
channel_ref = (root / "channels/channel_ref.rs").read_text()

def section(text, start, end):
    m = re.search(start + r".*?" + end, text, re.S)
    if not m:
        raise SystemExit(f"missing section: {start}")
    return m.group(0)

open_fn = section(
    client_mod,
    r"pub async fn channel_open_session",
    r"\n    }\n\n    /// Request an X11 channel",
)
handle_fn = section(
    client_mod,
    r"fn handle_msg",
    r"\n    fn finalize_server_channel_open_reply",
)
confirmation = section(
    client_encrypted,
    r"Some\(\(&msg::CHANNEL_OPEN_CONFIRMATION",
    r"\n            Some\(\(&msg::CHANNEL_CLOSE",
)

print("channel_open_session sends ChannelOpenSession, then awaits confirmation:")
print("  ", " ".join(open_fn.split()))
print("handle_msg inserts the ChannelRef into self.channels:")
print("  ", " ".join(handle_fn[handle_fn.find("Msg::ChannelOpenSession"):handle_fn.find("Msg::ChannelOpenX11")].split()))
print("confirmation sends ChannelMsg::Open but does not remove the channel:")
print("  ", " ".join(confirmation.split()))
print("Channel has an explicit Drop implementation:",
      bool(re.search(r"impl(?:<[^>]*>)?\s+Drop\s+for\s+Channel", channels_mod)))
print("ChannelRef has an explicit Drop implementation:",
      bool(re.search(r"impl(?:<[^>]*>)?\s+Drop\s+for\s+ChannelRef", channel_ref)))
print("channel close removes the channel:",
      "self.channels.remove(&channel_num)" in client_encrypted)
PY

Repository: gominimal/minimal

Length of output: 2020


Clean up the timed-out SSH channel before reusing client.

russh 0.62.4 keeps the shared Handle reusable, but dropping oneshot_rpc does not close the channel. A timeout can leave an orphaned channel and server RPC running. Send CHANNEL_CLOSE during cancellation or recreate the SSH client before destroy_session.

🤖 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 `@crates/minimal/src/lib.rs` around lines 2368 - 2388, The session_delta
timeout path must clean up the timed-out SSH channel before the shared client is
reused by destroy_session. Update session_delta or its caller around
client.oneshot_rpc to explicitly close the channel during cancellation, or
recreate the SSH client before destroy_session, while preserving the existing
best-effort None behavior for RPC failures and timeouts.

Comment thread crates/minimal/src/lib.rs Outdated
.context("a session or --all is required")?;
let record = resolve_session(&mut client, session).await?;

match destroy_gate(args.force, global.no_input, std::io::stdin().is_terminal())? {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

No need for such verbosity, fold all those conditionals into one match here with different arms

Comment thread crates/minimald-rpc/src/lib.rs Outdated
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionDeltaRequest {
Name(String),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For listing sessions and fetching sessions it made sense for there to be a query path based on name or id.

In this case this is an internal RPC where you always know the session ID, so just take an ID and lookup via ID.

…tivation delta

The activation-delta gate over-claimed: committed-and-pushed work still
listed as "changed". The SessionDelta RPC now reports a precision
ladder — VCS-exact (uncommitted files via git status --porcelain,
unpushed commits via rev-list --branches --not --remotes) when the
workspace is a real git repository, the activation delta otherwise,
with an honest "may include committed work" header — and the client
gates dirty-only: proven-clean sessions destroy without a word (even
headless), dirty sessions confirm with the listing, and unknowable
state (stopped session, RPC failure) confirms without one, refusing
headless without --force in both non-clean arms. Git failures degrade
down the ladder, never error, bounded by the walk timeout.

Review follow-ups folded in: the request is by id only (callers have
already resolved the record), the gate is one match over
(force, at-risk, interactivity), and the pre-existing headless destroy
tests (cli.rs, session-e2e) now assert the refusal-then---force path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KyZLpkRf9G4A2hUDgDvn5f
@norrietaylor

Copy link
Copy Markdown
Member Author

Revised per review — thanks @twitchyliquid64 for the catch that the activation-delta gate over-claimed: committed-and-pushed work still listed as "changed".

The gate now fires only for genuinely at-risk work:

  • VCS-exact when possible: with a real root .git and a working git, the daemon reports uncommitted files (git status --porcelain=v1 -unormal) and unpushed commits (rev-list --branches --not --remotes --count). Committed-and-pushed sessions are proven clean and destroy without a word — no prompt, headless or not.
  • Honest fallback: without usable VCS state the listing degrades to the activation delta with wording that no longer over-claims: "N files differ from activation (may include committed work):".
  • Conservative when unknowable: a stopped session or failed RPC can prove neither dirt nor cleanliness, so it confirms without a listing (and refuses headless without --force). Asymmetry documented in the gate.
  • Review follow-ups: the SessionDelta request is ID-only (callers have already resolved the record), and the gate is a single match over (force, at-risk state, interactivity). The pre-existing headless-destroy tests (cli.rs, session-e2e teardown) now assert the refusal-then---force path they actually exercise.

Verified via cross (aarch64-musl): session_delta filter test result: ok. 11 passed; 0 failed (incl. the VCS ladder against a real repo + bare remote), shell_exit anchor test result: ok. 1 passed; 0 failed.

# Conflicts:
#	crates/minimald/src/session_host.rs
@norrietaylor
norrietaylor merged commit b472900 into main Jul 31, 2026
30 checks passed
@norrietaylor
norrietaylor deleted the feat/destroy-dirty-gate branch July 31, 2026 23:43
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.

2 participants