Files
hermes-skills/ask-claude/references/ask-claude-payload-pattern.md

152 lines
6.6 KiB
Markdown
Raw Permalink Normal View History

# Ask-Claude Payload Pattern
Session-specific detail for the `ask-claude` skill: how to send questions to Claude
Opus 4.8 on 10.0.0.28 reliably, capture the session_id, handle shell metacharacters,
and avoid the filename-collision gotcha that bites when `read_file` returns a
different schema than expected.
## Pattern: scp + ask.sh
The `ask.sh` wrapper on 10.0.0.28 handles mktemp snapshot, claude invocation
(`--output-format json --model claude-opus-4-8 --max-turns 30`, `timeout 280`, `|| true`),
fetches the usage API, and runs `merge.py` to produce a single JSON envelope.
**Why scp, not heredoc:** shell metacharacters (`"`, `$`, `(`, `)`, backticks) in
the question content will break heredoc-based writes. For ANY non-trivial question
write the question to a local temp file, scp it, and pass the remote path to
`ask.sh --qfile`.
```bash
# 1. Write question locally
write_file("/tmp/ask-claude-q.txt", content=question)
# 2. Copy to remote
terminal("scp /tmp/ask-claude-q.txt n8n@10.0.0.28:/tmp/ask-claude-q.txt")
# 3. Run via ask.sh
terminal("ssh n8n@10.0.0.28 '~/claude/hermes_support/ask.sh --qfile /tmp/ask-claude-q.txt'")
```
## JSON envelope fields
Always parse these from the `--output-format json` response:
| Field | When | Use |
|---|---|---|
| `session_id` | Always (including errors) | Capture for `--resume` follow-ups |
| `result` | Success | The answer text |
| `is_error` | Always | `true` if anything failed |
| `subtype` | Always | `success` / `error_max_turns` / `error_budget` |
| `total_cost_usd` | Always | Track spend |
| `num_turns` | Always | How many agentic loops |
| `usage.input_tokens` | Always | Context usage |
| `usage.cache_creation_input_tokens` | Always | Context usage |
| `usage.cache_read_input_tokens` | Always | Context usage |
| `output_tokens` | Always | Context usage |
| `modelUsage.{model}.contextWindow` | Always | Context window size — iterate keys, don't hardcode |
| `stop_reason` | Always | Why Claude stopped |
**Critical: capture `session_id` from EVERY response, including errors.** A turn can
error yet still advance the session. If you only store it on success, `--resume` breaks.
**Critical: detect `error_max_turns` via `subtype`, not `is_error`.** `error_max_turns`
has `is_error: false` but no `result` field. Empty-answer detection requires checking
`subtype == 'error_max_turns'`.
## Asking Claude to validate a file
Claude (Opus on 10.0.0.28) has **no filesystem access to the operator's machine**.
When asking Claude to validate a plan, code, or doc:
1. **Read the file locally** with `read_file` or `terminal cat > /tmp/file.txt`
2. **Inline the content in the question** as a fenced block
3. **Tell Claude explicitly** the file is pasted inline (not a path to read)
**Common failure mode:** pasting a path like `/home/n8n/workspace/...` and asking
Claude to validate it. Claude will reply "I can't read that file" and refuse to
validate. Always inline.
## Filename-collision gotcha (execute_code + read_file)
`hermes_tools.read_file` inside `execute_code` returns a different schema than the
top-level `read_file` tool. Inside a script:
```python
from hermes_tools import read_file
r = read_file("/path/to/file")
# Schema: {"status": ..., "message": ..., "path": ..., "dedup": ..., "content_returned": ...}
# NO "content" key. Use the top-level read_file tool, OR use terminal cat.
```
**Workaround patterns:**
```python
# Pattern 1: terminal cat to a temp file, then open()
import subprocess
subprocess.run(["cp", "/path/to/file", "/tmp/working.txt"])
with open("/tmp/working.txt") as f:
content = f.read()
# Pattern 2: shell-out to the file directly
with open("/path/to/file") as f:
content = f.read() # works in execute_code without hermes_tools
```
This gotcha caused patch errors in a multi-file edit session: a patch string was
constructed from `r["content"]` which didn't exist inside `execute_code`. Switched
to `subprocess.run` + `with open()` to read reliably.
## Pre-dispatch model and config
- **Always pass `--model claude-opus-4-8` explicitly.** Profile defaults drift silently.
- **Always set `--max-turns`.** Default 30 is good for analysis. Lower for trivial
questions; higher (50+) for deep architectural reviews.
- **Always wrap with `timeout 280`.** Prevents runaway hangs. SSH itself has 300s.
- **Always add `|| true` after claude invocation.** `set -e` would kill the script
on non-zero exit, but the error JSON is still parseable.
- **Always use `mktemp` for temp files on the remote.** `/tmp/ask-claude-q.txt` is
a fixed path that concurrent Hermes sessions clobber.
## Multi-turn session lifecycle
- First `ask-claude` call in a Hermes session → fresh session (no `--resume`).
- Every subsequent call in the same Hermes session → `--resume <session_id>`.
- Sessions auto-expire after ~5 hours of inactivity. No cleanup needed.
- If the operator starts a new Hermes session (`.hermes/sessions/` rotates),
the Claude session is also gone — `--resume` will fail.
- For maximum continuity, capture `session_id` in the operator-visible relay
text so the operator can resume manually if the Hermes session restarts.
## Usage and context alerts
Apply on every turn:
| Check | Threshold | Action |
|---|---|---|
| Context % (from JSON envelope) | ≥ 80% | Warn: "wrap up or start fresh" |
| Session 5h utilization (OAuth API) | ≥ 80% | Warn + show reset time, offer Sonnet |
| Weekly 7d utilization (OAuth API) | ≥ 90% | Critical warning + reset time |
| Unavailable (error_budget / rate-limit) | N/A | Report exact reset time |
**The OAuth usage API aggressively rate-limits.** Call at most every ~5 minutes.
Cache the last good value. Swallow all errors (429s, timeouts). Never gate a turn
on this call.
## Common error patterns
- `error_max_turns` — question too complex, increase `--max-turns` or simplify.
`is_error: false`, but no `result` field — check `subtype` explicitly.
- `error_budget` — hit spending cap, check Pro limits.
- Empty response — timeout (increase from 280s), OOM, or crash.
- Non-JSON response — SSH dropped mid-call, Claude crashed before writing output.
- `ask.sh` returns `{"is_error": true, "error": "no Claude output"}` but direct
`claude -p` works — wrapper has a known failure mode. Fall back to direct:
`ssh n8n@10.0.0.28 'cd ~/claude/hermes_support && timeout 280 /home/n8n/.local/bin/claude -p "$(cat /tmp/q.txt)" --output-format json --model claude-opus-4-8 --max-turns 30 || true'`
## When the operator pushes back
After applying Claude's recommendations, ALWAYS emit the disagreement scan (see
SKILL.md §3.5). Skipping this is a failure mode — both silently-accepting and
silently-dropping are bugs. The scan is mandatory before declaring a Claude turn
resolved.