152 lines
6.6 KiB
Markdown
152 lines
6.6 KiB
Markdown
|
|
# 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.
|