docs(skill): make the daemon the default way an agent works

The skill buried daemon mode two-thirds down as a scaling trick, and still
offered --username "$SOULSEEK_USERNAME-$RANDOM" as the way to run commands in
parallel. That is worse than underselling it: the server allows one session
per account, so a per-run name means registering a throwaway account on a
public server for every search — account churn on infrastructure other people
maintain and share.

Starting a daemon is now the first instruction in the file, with the reason
stated, and the random-username idiom is named as the anti-pattern rather than
offered as an alternative. Exit 7's advice changes accordingly: it means
something else logged in as you, and the fix is a daemon, not a new name.

Three agents given only this file and a parallel-work task all started a
daemon and none invented a username.

The startup snippet also had a shell precedence bug: in
`daemon status || daemon &`, the & backgrounds the whole list, so it returned
instantly and the next command raced the login it was meant to use. Replaced
with a bounded wait, checked against a live server.
This commit is contained in:
Michel de Graaf
2026-07-27 14:01:17 +02:00
parent d2599c82d3
commit fe56a070e1
2 changed files with 83 additions and 27 deletions
+20
View File
@@ -129,6 +129,19 @@ soulseek-rs search 'aphex twin' # no login, no flags — uses the daemon
soulseek-rs # the TUI attaches to the same session soulseek-rs # the TUI attaches to the same session
``` ```
From a script, start it and wait for it — it takes a second or two to log in,
and a command that arrives early opens a session of its own instead:
```bash
soulseek-rs daemon status --json >/dev/null 2>&1 || {
soulseek-rs daemon >/dev/null 2>&1 &
for _ in $(seq 30); do
soulseek-rs daemon status --json >/dev/null 2>&1 && break
sleep 1
done
}
```
Commands find it by themselves. The daemon listens on a Unix socket only your Commands find it by themselves. The daemon listens on a Unix socket only your
user can open, so there is nothing to configure and no secret to pass around — user can open, so there is nothing to configure and no secret to pass around —
the same arrangement Docker and ssh-agent use. With no daemon running, the same arrangement Docker and ssh-agent use. With no daemon running,
@@ -139,6 +152,13 @@ What this buys: downloads that outlive the command that queued them, several
clients (or agents) on one account at once, and a chat window that keeps clients (or agents) on one account at once, and a chat window that keeps
collecting while nothing is attached. collecting while nothing is attached.
It is also the neighbourly way to use the network. The server allows one
session per account, so parallel one-shot runs either displace each other or
push you into registering a throwaway name per run — account churn on
infrastructure other people maintain. A daemon is one login for as long as you
work, and one share index instead of a rescan per invocation. If you are
driving this from a script or an agent, start a daemon.
```bash ```bash
soulseek-rs daemon status # who it is, what it shares, who is attached soulseek-rs daemon status # who it is, what it shares, who is attached
soulseek-rs daemon stop soulseek-rs daemon stop
+63 -27
View File
@@ -5,7 +5,8 @@ description: >-
using the soulseek-rs command-line client. Use when the user wants to find or using the soulseek-rs command-line client. Use when the user wants to find or
fetch music or other files from Soulseek, list what a peer shares, check fetch music or other files from Soulseek, list what a peer shares, check
whether a peer is online, read or post in Soulseek chat rooms, send private whether a peer is online, read or post in Soulseek chat rooms, send private
messages, or stay online serving uploads. messages, stay online serving uploads, or run soulseek-rs as a background
service that several commands share.
--- ---
# soulseek-rs # soulseek-rs
@@ -14,6 +15,52 @@ A Soulseek client whose subcommands are built to be driven by a program. Run
`soulseek-rs` with no subcommand and it launches an interactive terminal UI — `soulseek-rs` with no subcommand and it launches an interactive terminal UI —
never do that; it needs a real terminal and never returns. never do that; it needs a real terminal and never returns.
## Start a daemon first
Before anything else, make sure one is running:
```bash
soulseek-rs daemon status --json >/dev/null 2>&1 || {
soulseek-rs daemon >/dev/null 2>&1 &
for _ in $(seq 30); do
soulseek-rs daemon status --json >/dev/null 2>&1 && break
sleep 1
done
}
```
Wait for it, do not just launch it. A daemon takes a second or two to log in,
and `soulseek-rs daemon status … || soulseek-rs daemon &` does **not** do this
— `&` backgrounds the whole list, so it returns instantly and the next command
races the login it is supposed to be using. A command that arrives too early
finds no daemon and quietly opens a session of its own, which is the outcome
this whole section exists to avoid.
Then use every other command exactly as documented below. They find the daemon
by themselves — it listens on a Unix socket only your user can open, so there
is nothing to configure, no address to pass, and no token to handle.
**Why this matters, and why it is not optional for you.** Soulseek is a real
network of other people's computers, and the server allows **one session per
account**. Without a daemon, every invocation is its own login, and running two
at once leaves you two bad choices: share a username and they silently cut each
other off (exit 7), or give each run a throwaway name and *register a new
account on a public server for every search you make*. The second is what a
naive script does, and it is the antisocial one — it is account churn on
infrastructure other people maintain and share.
A daemon is one login for as long as you work, however many commands you run
against it. It also indexes your shared folders once instead of re-scanning
them on every invocation, and downloads it starts outlive the command that
asked for them, so a transfer survives the process that queued it.
So: **one daemon, then as many commands as you like.** Reach for `--no-daemon`
only when you deliberately want an isolated session, and expect to justify it.
Two things change while a daemon is running: files land in the *daemon's*
download directory (`daemon status` reports which), and `shares add` updates it
straight away rather than at its next start.
## The contract ## The contract
Always pass `--json`. It is a global flag, so it works on every subcommand and Always pass `--json`. It is a global flag, so it works on every subcommand and
@@ -164,38 +211,22 @@ is the normal way to be selective.
## Running several at once ## Running several at once
The server allows one session per account, and the later login wins: two runs With a daemon running this needs no thought — they all share its one session:
sharing a username silently kill each other's session.
The daemon is the answer to this. Start one, and every command afterwards
borrows its session instead of opening one:
```bash
soulseek-rs daemon & # once
soulseek-rs search 'gary beck fold' --json # no login, no flags
soulseek-rs search 'marcal steady' --json # concurrent, same session
```
Commands find it on their own: it listens on a Unix socket only you can open,
so there is nothing to configure and no token to pass. `daemon status` says
whether one is running. This also makes downloads outlive the command that
started them — the transfer belongs to the daemon, so the queue survives.
Two things change when a daemon is in play: files land in the *daemon's*
download directory, and `--no-daemon` is how one run opts out and logs in for
itself.
Without a daemon, give every concurrent run its own name instead:
```bash ```bash
for q in 'gary beck fold' 'marcal steady'; do for q in 'gary beck fold' 'marcal steady'; do
soulseek-rs search "$q" --username "$SOULSEEK_USERNAME-$RANDOM" --json & soulseek-rs search "$q" --json &
done done
wait wait
``` ```
The listener port needs no such care: a run that finds the configured port Without one, the same loop is a bug: the runs share a username, so each login
taken binds a free one and tells the server about that one instead. displaces the last and they kill each other's session (exit 7). Giving each run
its own `--username` avoids the collision by registering a fresh account on the
server for every search, which is worse — do not do it. Start the daemon.
The listener port needs no care either way: a run that finds the configured
port taken binds a free one and tells the server about that one instead.
## Rules ## Rules
@@ -204,12 +235,17 @@ taken binds a free one and tells the server about that one instead.
or let a shell expand them. or let a shell expand them.
- **`--follow` never returns.** For `room listen`, `message read`, and `serve`, - **`--follow` never returns.** For `room listen`, `message read`, and `serve`,
always pass `--duration <seconds>` instead. always pass `--duration <seconds>` instead.
- **`daemon` never returns either.** It is a service: background it
(`soulseek-rs daemon &`) and check it with `daemon status`. Running it in the
foreground will hang you until the timeout.
- **Every wait is bounded.** `--timeout` per command, `--search-timeout` for - **Every wait is bounded.** `--timeout` per command, `--search-timeout` for
searches. There is no unbounded form. searches. There is no unbounded form.
- **Exit 4 is not a failure.** A search that finds nothing means the query was - **Exit 4 is not a failure.** A search that finds nothing means the query was
too narrow or misspelled. Widen it; use `whoami` to rule out the connection. too narrow or misspelled. Widen it; use `whoami` to rule out the connection.
- **Exit 7 is not an answer.** The session ended mid-command, so nothing it saw - **Exit 7 is not an answer.** The session ended mid-command, so nothing it saw
counts. Retry under a name no other run is using. counts. It means something else logged in as you — almost always a second
run of your own. Start a daemon and route through it rather than hunting for
an unused name.
- **Never put a password in argv** — `ps` shows it to every user on the box. - **Never put a password in argv** — `ps` shows it to every user on the box.
Use `--password-stdin`, the OS keychain, or `SOULSEEK_PASSWORD_CMD`. Use `--password-stdin`, the OS keychain, or `SOULSEEK_PASSWORD_CMD`.
- **Downloading pulls a file off a stranger's machine.** Confirm with the user - **Downloading pulls a file off a stranger's machine.** Confirm with the user