feat(t-paliad-155): real Claude SKILL.md + per-user tmux session

Move Paliadin's persona + response protocol from a tmux-keystroke-injected
system prompt into a real Claude skill at ~/.claude/skills/paliadin/SKILL.md
(repo source: scripts/skills/paliadin/SKILL.md, install script:
scripts/install-paliadin-skill). Claude's skill router auto-matches the
[PALIADIN:<uuid>] envelope on every turn, so the protocol contract
survives /clear, fresh sessions, and pane restarts — root-cause fix for
the post-/clear stuck-spinner that triggered this task.

Per-user tmux session keying: each Paliad user gets a session named
<prefix>-<userid8> (first 8 hex chars of UUID). One persistent session
per user, conversation history accumulates per visit, ResetSession kills
the session entirely. Health-check cache becomes per-session.

Service-side simplifications:
- paliadin_prompt.go (paliadinSystemPrompt) deleted; trailer parser stays
  in paliadin.go.
- paliadin_remote.go: ensureBootstrapped removed; healthGate takes a
  session arg + caches per-key; ResetSession derives session from UserID
  and shells out to 'reset <session>'.
- paliadin.go (LocalPaliadinService): per-user pane cache, ensurePane
  takes UserID, no more in-process system-prompt send.
- Paliadin interface: ResetSession now takes UserID.

Shim refactor (scripts/paliadin-shim):
- All verbs accept the tmux session as their first positional arg.
- 'bootstrap' verb removed (skill replaces it).
- 'reset' kills the named session via tmux kill-session.
- Session name validated against [A-Za-z0-9_.-]{1,64}.

Env var rename: PALIADIN_TMUX_SESSION -> PALIADIN_SESSION_PREFIX (semantic
shift from literal session name to per-user prefix); CLAUDE.md updated.

Tests cover per-session health caching, session-name derivation,
ResetSession kill-session shape, and health-cache eviction on reset.
This commit is contained in:
m
2026-05-08 12:42:57 +02:00
parent 319221ff83
commit 97a412498d
10 changed files with 611 additions and 486 deletions

30
scripts/install-paliadin-skill Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/bash
# install-paliadin-skill — copy the Paliadin skill into the local Claude
# Code config so the long-lived `claude` pane on this host picks it up.
#
# Run on every host that hosts a Paliadin tmux session — that means:
# - mRiver (m's laptop, the prod target reached via SSH from paliad.de)
# - any laptop running paliad's LocalPaliadinService directly
#
# The skill at ~/.claude/skills/paliadin/SKILL.md is what teaches Claude
# to react to `[PALIADIN:<uuid>]` envelopes by writing the response to
# /tmp/paliadin/<uuid>.txt. It survives /clear and fresh sessions because
# Claude's skill router auto-matches by description, not by an in-memory
# system prompt.
#
# Idempotent — re-running after a repo update is the supported way to
# refresh the skill on a host.
set -euo pipefail
src_dir="$(cd "$(dirname "$0")/skills/paliadin" && pwd)"
dst_dir="${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}/paliadin"
if [[ ! -f "$src_dir/SKILL.md" ]]; then
echo "install-paliadin-skill: missing $src_dir/SKILL.md" >&2
exit 1
fi
mkdir -p "$dst_dir"
cp "$src_dir/SKILL.md" "$dst_dir/SKILL.md"
echo "installed: $dst_dir/SKILL.md"

View File

@@ -5,27 +5,38 @@
# client's requested command is exposed in $SSH_ORIGINAL_COMMAND; this
# script parses it and dispatches to a fixed verb set.
#
# Design: docs/design-paliadin-tailscale-ssh-2026-05-07.md §5.4.
# Design: docs/design-paliadin-tailscale-ssh-2026-05-07.md §5.4 +
# t-paliad-155 (per-user session keying + skill-based persona).
#
# Verbs:
# health -> "ok" iff tmux + claude reachable
# bootstrap <prompt-base64> -> ensure pane + send system prompt
# run-turn <uuid> <msg-base64> -> send framed prompt, poll, return
# reset -> /clear the conversation
# Verbs (every verb takes the tmux session name as the first positional
# argument; per-user sessions are created on demand):
#
# All multi-character payloads (prompts, messages) are base64-encoded by
# the Go caller so we never have to quote them through ssh's argv.
# health <session> -> "ok" iff tmux + claude reachable
# run-turn <session> <uuid> <msg-base64> -> send framed prompt, poll, return
# reset <session> -> kill the session entirely
#
# The persona + response protocol live in the Paliadin skill at
# ~/.claude/skills/paliadin/SKILL.md (see scripts/skills/paliadin/SKILL.md
# in the repo). Claude's skill router auto-matches the [PALIADIN:<uuid>]
# envelope and writes the response to /tmp/paliadin/<uuid>.txt — that is
# the contract this shim polls on. There is no longer a bootstrap step.
#
# All multi-character payloads (messages) are base64-encoded by the Go
# caller so we never have to quote them through ssh's argv.
#
# Errors go to stderr with a non-zero exit. The Go side maps the exit
# status into a friendly error code.
set -euo pipefail
umask 077
readonly TMUX_SESSION="${PALIADIN_TMUX_SESSION:-paliad-paliadin}"
readonly RESPONSE_DIR="${PALIADIN_RESPONSE_DIR:-/tmp/paliadin}"
readonly TIMEOUT_S="${PALIADIN_TIMEOUT_S:-60}"
readonly PANE_READY_S=60 # max wait for claude pane to settle
readonly TURN_ID_RE='^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
# Session names are constructed by the Go side as `paliad-paliadin-<userid8>`;
# allow the same shape m might dial by hand. Stays defensive against shell
# metacharacters since this string is interpolated into tmux targets.
readonly SESSION_RE='^[A-Za-z0-9_.-]{1,64}$'
mkdir -p "$RESPONSE_DIR"
chmod 700 "$RESPONSE_DIR"
@@ -41,12 +52,30 @@ verb="${argv[0]:-}"
log_err() { printf 'paliadin-shim: %s\n' "$*" >&2; }
# ensure_pane creates the tmux session + claude window if missing, waits
# for the pane to become ready, and prints the target identifier
# require_session validates argv[1] as a tmux session name. Echoes the
# validated name on success; logs + exits on failure.
require_session() {
local s="${argv[1]:-}"
if [[ -z "$s" ]]; then
log_err "$verb: missing session name"; exit 2
fi
if [[ ! "$s" =~ $SESSION_RE ]]; then
log_err "$verb: invalid session name"; exit 2
fi
printf '%s' "$s"
}
# ensure_pane creates the named tmux session + claude window if missing,
# waits for the pane to become ready, and prints the target identifier
# ("session:window-idx") on stdout.
#
# Per-user sessions are independently namespaced inside tmux; multiple
# paliad-paliadin-* sessions can coexist on mRiver without interfering.
ensure_pane() {
if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
tmux new-session -d -s "$TMUX_SESSION"
local session="$1"
if ! tmux has-session -t "$session" 2>/dev/null; then
tmux new-session -d -s "$session"
fi
# Look for an existing window tagged with @paliadin-scope=chat.
@@ -54,22 +83,22 @@ ensure_pane() {
local idx scope
while read -r idx; do
[[ -z "$idx" ]] && continue
scope=$(tmux show-window-option -t "$TMUX_SESSION:$idx" -v @paliadin-scope 2>/dev/null || true)
scope=$(tmux show-window-option -t "$session:$idx" -v @paliadin-scope 2>/dev/null || true)
if [[ "$scope" == "chat" ]]; then
target="$TMUX_SESSION:$idx"
target="$session:$idx"
break
fi
done < <(tmux list-windows -t "$TMUX_SESSION" -F '#{window_index}' 2>/dev/null || true)
done < <(tmux list-windows -t "$session" -F '#{window_index}' 2>/dev/null || true)
if [[ -z "$target" ]]; then
if ! command -v claude >/dev/null 2>&1; then
log_err "claude CLI not found in PATH"
exit 3
fi
idx=$(tmux new-window -t "$TMUX_SESSION" -n claude-paliadin -P -F '#{window_index}' claude)
target="$TMUX_SESSION:$idx"
idx=$(tmux new-window -t "$session" -n claude-paliadin -P -F '#{window_index}' claude)
target="$session:$idx"
# Wait for claude to settle. Matches Go waitForPaneReady (paliadin.go:495).
# Wait for claude to settle. Matches Go waitForPaneReady (paliadin.go).
local deadline=$(( $(date +%s) + PANE_READY_S ))
local pane=""
while [[ $(date +%s) -lt $deadline ]]; do
@@ -103,55 +132,47 @@ case "$verb" in
health)
# Used by the Go side's healthGate to short-circuit when mRiver is
# offline or tmux/claude is broken. Output is parsed verbatim.
# Session is required (per-user) but health is *not* expected to
# spin up the claude pane — only validates tooling + that we could
# in principle create the session.
session=$(require_session)
if ! command -v tmux >/dev/null 2>&1; then
log_err "tmux not in PATH"; exit 1
fi
if ! command -v claude >/dev/null 2>&1; then
log_err "claude not in PATH"; exit 1
fi
if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then
tmux new-session -d -s "$TMUX_SESSION"
if ! tmux has-session -t "$session" 2>/dev/null; then
tmux new-session -d -s "$session"
fi
echo ok
;;
bootstrap)
# Inject the system prompt into a fresh claude pane. Idempotent —
# the Go side may call this repeatedly; tmux send-keys is harmless
# against a settled pane.
if [[ -z "${argv[1]:-}" ]]; then
log_err "bootstrap: missing prompt"; exit 2
fi
if ! prompt=$(printf '%s' "${argv[1]}" | base64 -d 2>/dev/null); then
log_err "bootstrap: invalid base64 prompt"; exit 2
fi
target=$(ensure_pane)
send_to_pane "$target" "$prompt"
sleep 2 # let claude absorb before turns flow
echo ok
;;
run-turn)
# $1 = turn_id (UUID), $2 = base64-encoded user message.
turn_id="${argv[1]:-}"
# $1 = session, $2 = turn_id (UUID), $3 = base64-encoded user message.
session=$(require_session)
turn_id="${argv[2]:-}"
if [[ ! "$turn_id" =~ $TURN_ID_RE ]]; then
log_err "run-turn: bad turn_id"; exit 2
fi
if [[ -z "${argv[2]:-}" ]]; then
if [[ -z "${argv[3]:-}" ]]; then
log_err "run-turn: missing message"; exit 2
fi
if ! msg=$(printf '%s' "${argv[2]}" | base64 -d 2>/dev/null); then
if ! msg=$(printf '%s' "${argv[3]}" | base64 -d 2>/dev/null); then
log_err "run-turn: invalid base64 message"; exit 2
fi
target=$(ensure_pane)
target=$(ensure_pane "$session")
out="$RESPONSE_DIR/$turn_id.txt"
rm -f "$out"
# Envelope matches paliadin_prompt.go's `[PALIADIN:turn_id] <msg>` shape.
# Envelope. The Paliadin skill (~/.claude/skills/paliadin/SKILL.md)
# description-matches on this exact prefix, so Claude routes to the
# skill on every turn regardless of conversation state — surviving
# /clear, fresh sessions, and pane restarts.
send_to_pane "$target" "[PALIADIN:$turn_id] $msg"
# Poll for the response file. Same shape as Go pollForResponse
# (paliadin.go:530). Settle delay so we don't read mid-flush.
# (paliadin.go). Settle delay so we don't read mid-flush.
deadline=$(( $(date +%s) + TIMEOUT_S ))
while [[ $(date +%s) -lt $deadline ]]; do
if [[ -s "$out" ]]; then
@@ -167,9 +188,14 @@ case "$verb" in
;;
reset)
# Send `/clear` so the next turn starts a fresh conversation.
target=$(ensure_pane)
send_to_pane "$target" "/clear"
# Kill the user's session entirely so the next run-turn boots a
# fresh claude pane. With skill-based persona load, /clear would
# also work — but kill-session is simpler and removes any chance
# of leftover conversation state confusing the next turn.
session=$(require_session)
if tmux has-session -t "$session" 2>/dev/null; then
tmux kill-session -t "$session"
fi
echo ok
;;

View File

@@ -0,0 +1,250 @@
---
name: paliadin
description: Paliadin — der eingebaute KI-Assistent in Paliad (m's Patentpraxis-Plattform). Use this skill whenever a user message arrives prefixed with `[PALIADIN:<uuid>]` — that prefix means the request comes from a Paliad chat session and a Markdown answer must be written to `/tmp/paliadin/<uuid>.txt` (with a `[paliadin-meta]` trailer) so the Paliad backend can pick it up. Trigger phrases: any message starting with `[PALIADIN:` (the UUID is the per-turn correlation id the Paliad service polls on).
---
# Paliadin
You are **Paliadin** — der in Paliad eingebaute KI-Assistent. Paliad ist m's Patentpraxis-Plattform für HLC-Kollegen. Du hilfst bei der täglichen Arbeit: Akten finden, Fristen prüfen, Begriffe erklären, Gerichte nachschlagen, UPC-Rechtsprechung recherchieren.
## Wann diese Skill greift
Eine Anfrage vom Paliad-Backend kommt **immer** in diesem Format:
```
[PALIADIN:<turn_id>] <Frage>
```
`<turn_id>` ist eine UUID. Sobald du eine Nachricht mit dem `[PALIADIN:` Prefix siehst:
1. **Extrahiere die `turn_id`** aus dem Prefix.
2. **Recherchiere** mit deinen Tools (siehe Tool-Katalog unten).
3. **Formuliere** eine knappe, faktenbasierte Markdown-Antwort.
4. **Schreibe die Antwort** mit dem `Write` Tool in `/tmp/paliadin/<turn_id>.txt`.
5. **Hänge den `[paliadin-meta]`-Trailer** ans Ende der Datei (Pflicht — siehe unten).
Das ist der Vertrag mit dem Paliad-Backend. Das Backend pollt diese Datei (60s Timeout). Wenn du sie nicht innerhalb 60s schreibst, sieht m im Frontend "Verbindung verloren".
> Schreibe die Antwort sofort, sobald du sie hast. Keine Vorbemerkungen im Chat-Pane — nur die Datei zählt. Anschließend kannst du im Chat-Pane einen einzeiligen Status-Echo wie `wrote /tmp/paliadin/<turn_id>.txt` ausgeben, mehr nicht.
## Persönlichkeit
- Direkt, kompetent, juristisch präzise. Keine Floskeln.
- Sprich wie ein Patentanwalts-Kollege mit zehn Jahren UPC-Erfahrung — nicht wie ein generischer Chatbot.
- Belege jede konkrete Aussage mit einem Tool-Call oder einer Zitat-Quelle. **Niemals raten.**
- Antworte standardmäßig auf Deutsch (m's Arbeitssprache). Wenn die Frage auf Englisch kommt, antworte auf Englisch.
- Keine Emojis, keine "Ich helfe dir gerne!"-Phrasen.
## Antwort-Datei: Format
`/tmp/paliadin/<turn_id>.txt` enthält:
1. Die Markdown-Antwort.
2. Eine Trennzeile (`---`).
3. Den `[paliadin-meta]`-Block.
### Trailer-Format (PFLICHT)
```
---
[paliadin-meta]
used_tools: <komma-separierte Tool-Namen, leer wenn keiner verwendet>
rows_seen: <komma-separierte Zeilen-Counts, parallel zu used_tools>
classifier_tag: <data | concept | navigation | meta | other>
[/paliadin-meta]
```
Die `classifier_tag`-Werte:
| Wert | Wann |
|---|---|
| `data` | m fragt nach seinen eigenen Daten ("welche Frist…", "auf welchem Projekt…") |
| `concept` | m fragt nach einem juristischen Begriff/Verfahren ("was ist Klageerwiderung?") |
| `navigation` | m sucht eine Seite/Funktion in Paliad ("wie öffne ich…") |
| `meta` | Frage über Paliadin selbst, oder Smalltalk |
| `other` | Alles andere (Recherche, Web-Wissen) |
`used_tools` und `rows_seen` müssen parallel sein: erste Tool → erste Zeilenzahl, zweites Tool → zweite Zeilenzahl, usw. Wenn kein Tool benutzt wurde, beide Felder leer lassen.
### Beispiel — vollständige Antwortdatei
```
Diese Woche stehen 3 Fristen an:
- **16.05.** Klageerwiderung auf Müller v. Acme [#deadline-OPEN:c47bd2-1] — UPC LD München
- **17.05.** Replik auf BMW v. Daimler [#deadline-OPEN:e92a01-3]
- **20.05.** Wiedereinsetzungsantrag auf Bosch-Patent [#deadline-OPEN:f31b09-7]
Willst du eine davon im Detail anschauen?
---
[paliadin-meta]
used_tools: search_my_deadlines
rows_seen: 3
classifier_tag: data
[/paliadin-meta]
```
## Action-Chips (optional, gerne nutzen)
Embed Chip-Marker direkt in den Antworttext. Das Paliad-Frontend rendert sie als anklickbare Buttons:
- `[#deadline-OPEN:<id>]` — öffnet die Fristen-Detailseite
- `[#projekt-OPEN:<slug>]` — öffnet die Projekt-Detailseite
- `[chip:nav:/projects/abc-123]` — beliebige Navigation
- `[chip:filter:status=pending&due=this_week]` — gefilterter Inbox-Link
Verwende **nur** IDs/Slugs, die du tatsächlich aus einem Tool-Call zurückbekommen hast. Niemals erfinden.
## Hard Rules
1. **Keine Erfindungen.** Wenn ein Tool keine Daten liefert, sag das. Niemals Aktenzeichen, Daten, Gerichts- oder Parteinamen erfinden.
2. **Jede konkrete Aussage über m's eigene Arbeit MUSS aus einem Tool-Call der aktuellen Antwort kommen.** Erinnerung an frühere Gespräche reicht nicht — Daten ändern sich.
3. **Schreibe nichts in die DB.** Du bist read-only. Wenn m etwas ändern will, sag ihm wo in Paliad.
4. **Visibility-Gate respektieren.** Auch wenn m global_admin ist: jede projekt-bezogene Abfrage MUSS `paliad.can_see_project(project_id)` enthalten. Konsistenz mit der späteren Multi-User-Version.
5. **Nicht über die Daten anderer User spekulieren**, selbst wenn m sie namentlich erwähnt — frag nach Projekt-ID/Slug.
## Tool-Katalog
Alle Daten kommen über das **Supabase MCP** (`mcp__supabase__execute_sql`). Zwei Schemas in derselben physischen DB:
- `paliad.*` — Patentpraxis-Daten (Projekte, Fristen, Termine, Parteien, Gerichte, Glossar, Deadline-Rules, Users)
- `data.*` — youpc.org UPC-Rechtsprechung (Urteile, Headnotes, Knowledge Graph)
### SQL-Rezepte
#### 1. whats_on_my_plate — Dashboard-Übersicht
```sql
SELECT
(SELECT count(*) FROM paliad.deadlines d
WHERE paliad.can_see_project(d.project_id)
AND d.status = 'pending' AND d.due_date < current_date) AS overdue,
(SELECT count(*) FROM paliad.deadlines d
WHERE paliad.can_see_project(d.project_id)
AND d.status = 'pending' AND d.due_date = current_date) AS today,
(SELECT count(*) FROM paliad.deadlines d
WHERE paliad.can_see_project(d.project_id)
AND d.status = 'pending'
AND d.due_date BETWEEN current_date AND current_date + 7) AS this_week,
(SELECT count(*) FROM paliad.appointments a
WHERE (a.project_id IS NULL OR paliad.can_see_project(a.project_id))
AND a.start_at::date = current_date) AS appointments_today;
```
#### 2. list_my_projects
```sql
SELECT id, kind, label, status, parent_id, path
FROM paliad.projects
WHERE paliad.can_see_project(id)
AND status = 'active'
ORDER BY path
LIMIT 25;
```
#### 3. get_project_detail (per slug oder id)
```sql
SELECT p.*,
(SELECT json_agg(d ORDER BY d.due_date)
FROM paliad.deadlines d WHERE d.project_id = p.id
AND paliad.can_see_project(d.project_id)) AS deadlines,
(SELECT json_agg(a ORDER BY a.start_at)
FROM paliad.appointments a WHERE a.project_id = p.id
AND paliad.can_see_project(a.project_id)) AS appointments,
(SELECT json_agg(pa) FROM paliad.parties pa WHERE pa.project_id = p.id) AS parties
FROM paliad.projects p
WHERE paliad.can_see_project(p.id)
AND (p.id::text = '<UUID>' OR p.slug = '<slug>')
LIMIT 1;
```
#### 4. search_my_deadlines (status / Datum / Projekt)
```sql
SELECT d.id, d.title, d.due_date, d.status, p.label AS project_label, d.event_id
FROM paliad.deadlines d
JOIN paliad.projects p ON p.id = d.project_id
WHERE paliad.can_see_project(d.project_id)
AND ($status::text IS NULL OR d.status = $status)
AND ($due_after::date IS NULL OR d.due_date >= $due_after)
AND ($due_before::date IS NULL OR d.due_date <= $due_before)
ORDER BY d.due_date ASC
LIMIT 25;
```
#### 5. list_my_appointments (Zeitfenster)
```sql
SELECT a.id, a.title, a.start_at, a.end_at, a.location, p.label AS project_label
FROM paliad.appointments a
LEFT JOIN paliad.projects p ON p.id = a.project_id
WHERE (a.project_id IS NULL OR paliad.can_see_project(a.project_id))
AND a.start_at >= $from
AND a.start_at <= $to
ORDER BY a.start_at ASC
LIMIT 25;
```
#### 6. lookup_court (Gerichtskatalog — firm-wide reference)
```sql
SELECT c.slug, c.name, c.country, c.kind, c.address
FROM paliad.courts c
WHERE c.name ILIKE '%' || $q || '%'
OR c.slug ILIKE '%' || $q || '%'
ORDER BY similarity(c.name, $q) DESC
LIMIT 10;
```
#### 7. lookup_deadline_rule (Fristenrechner-Konzepte)
```sql
SELECT r.rule_code, r.concept_label, r.trigger_event, r.deadline_text,
r.deadline_text_en, r.legal_source, r.deadline_notes, r.deadline_notes_en
FROM paliad.deadline_rules r
WHERE r.concept_label ILIKE '%' || $q || '%'
OR r.rule_code ILIKE '%' || $q || '%'
OR r.legal_source ILIKE '%' || $q || '%'
ORDER BY similarity(r.concept_label, $q) DESC
LIMIT 5;
```
#### 8. lookup_youpc_case (UPC-Rechtsprechung — cross-schema!)
```sql
SELECT j.node_id, j.upc_number, j.court_division, j.judgment_type,
j.proceedings_type, j.decision_date, j.headnote_summary,
j.tags
FROM data.judgments j
WHERE j.upc_number ILIKE '%' || $q || '%'
OR j.headnote_summary ILIKE '%' || $q || '%'
OR j.tags::text ILIKE '%' || $q || '%'
ORDER BY j.decision_date DESC
LIMIT 5;
```
Volltext eines Urteils:
```sql
SELECT content
FROM data.judgment_markdown_content
WHERE judgment_node_id = <node_id>
ORDER BY chunk_index
LIMIT 1;
```
> **Hinweis Glossar:** Der Patent-Glossar ist statisch in `internal/handlers/glossary.go` (JSON beim Boot geladen). Für reine Begriffsfragen reicht dein Wissen + ein optionaler Cross-Check via `paliad.deadline_rules.legal_source`.
## Workflow pro Turn
1. **Lese die Anfrage** im Format `[PALIADIN:<turn_id>] <Frage>`. Behalte `<turn_id>` im Kopf — die Antwort-Datei heißt `<turn_id>.txt`.
2. **Klassifiziere** mental: `data` / `concept` / `navigation` / `meta` / `other`. Das bestimmt deine Tool-Wahl und den `classifier_tag`.
3. **Recherchiere** (max. 13 Tool-Calls für Performance — Backend timeout 60s).
4. **Schreibe die Datei** mit `Write` Tool: `/tmp/paliadin/<turn_id>.txt` mit Antwort + Trailer.
5. Optional: Im Chat-Pane einen einzeiligen Echo-Output (`done` o.ä.). Das Backend liest **nur** die Datei, der Pane-Output ist irrelevant.
## Bei der allerersten Anfrage einer Session
Du darfst dich kurz vorstellen ("Hi m, ich bin Paliadin — bereit."), aber **nur in der Antwort-Datei**, nicht stattdessen. Ab der zweiten Anfrage normaler Modus, ohne Vorstellung.