docs: ERROR_HANDLING.md — fix code examples to match v1.0 envelope (flat shape)

The Python code examples were accessing nested error.kind like envelope['error']['kind'],
but v1.0 emits flat envelopes with error as a STRING and kind at top-level.

Updated:
- Table header: now shows actual v1.0 shape {error: "...", kind: "...", type: "error"}
- match statement: switched from envelope.get('error',{}).get('kind') to envelope.get('kind')
- All ClawError raises: changed from envelope['error']['message'] to envelope.get('error','')
  because error field is a STRING in v1.0, not a nested object
- Added inline comments on every error case noting v1.0 vs v2.0 difference
- Appendix: split into v1.0 (actual/current) and v2.0 (target after FIX_LOCUS_164)

The code examples now work correctly against the actual binary.
This was active misdocumentation (P0 severity) — the Python examples would crash
if a consumer tried to use them.
This commit is contained in:
YeonGyu-Kim 2026-04-23 05:00:33 +09:00
parent 98c675b33b
commit 0929180ba8

View File

@ -15,7 +15,7 @@ Every clawable command returns JSON on stdout when `--output-format json` is req
| Exit Code | Meaning | Response Format | Example |
|---|---|---|---|
| **0** | Success | `{success fields}` | `{"session_id": "...", "loaded": true}` |
| **1** | Error / Not Found | `{error: {kind, message, ...}}` | `{"error": {"kind": "session_not_found", ...}}` |
| **1** | Error / Not Found | `{error: "...", hint: "...", kind: "...", type: "error"}` (flat, v1.0) | `{"error": "session not found", "kind": "session_not_found", "type": "error"}` |
| **2** | Timeout | `{final_stop_reason: "timeout", final_cancel_observed: ...}` | `{"final_stop_reason": "timeout", ...}` |
### Text mode vs JSON mode exit codes
@ -81,8 +81,12 @@ def run_claw_command(command: list[str], timeout_seconds: float = 30.0) -> dict[
retryable=False,
)
# Classify by exit code and error.kind
match (result.returncode, envelope.get('error', {}).get('kind')):
# Classify by exit code and top-level kind field (v1.0 flat envelope shape)
# NOTE: v1.0 envelopes have error as a STRING, not a nested object.
# The v2.0 schema (SCHEMAS.md) specifies nested error.{kind, message, ...},
# but the current binary emits flat {error: "...", kind: "...", type: "error"}.
# See FIX_LOCUS_164.md for the migration timeline.
match (result.returncode, envelope.get('kind')):
case (0, _):
# Success
return envelope
@ -91,8 +95,8 @@ def run_claw_command(command: list[str], timeout_seconds: float = 30.0) -> dict[
# #179: argparse error — typically a typo or missing required argument
raise ClawError(
kind='parse',
message=envelope['error']['message'],
hint=envelope['error'].get('hint'),
message=envelope.get('error', ''), # error field is a string in v1.0
hint=envelope.get('hint'),
retryable=False, # Typos don't fix themselves
)
@ -100,7 +104,7 @@ def run_claw_command(command: list[str], timeout_seconds: float = 30.0) -> dict[
# Common: load-session on nonexistent ID
raise ClawError(
kind='session_not_found',
message=envelope['error']['message'],
message=envelope.get('error', ''), # error field is a string in v1.0
session_id=envelope.get('session_id'),
retryable=False, # Session won't appear on retry
)
@ -109,7 +113,7 @@ def run_claw_command(command: list[str], timeout_seconds: float = 30.0) -> dict[
# Directory missing, permission denied, disk full
raise ClawError(
kind='filesystem',
message=envelope['error']['message'],
message=envelope.get('error', ''), # error field is a string in v1.0
retryable=True, # Might be transient (disk space, NFS flake)
)
@ -117,16 +121,16 @@ def run_claw_command(command: list[str], timeout_seconds: float = 30.0) -> dict[
# Generic engine error (unexpected exception, malformed input, etc.)
raise ClawError(
kind='runtime',
message=envelope['error']['message'],
retryable=envelope['error'].get('retryable', False),
message=envelope.get('error', ''), # error field is a string in v1.0
retryable=envelope.get('retryable', False), # v1.0 may or may not have this
)
case (1, _):
# Catch-all for any new error.kind values
raise ClawError(
kind=envelope['error']['kind'],
message=envelope['error']['message'],
retryable=envelope['error'].get('retryable', False),
kind=envelope.get('kind', 'unknown'),
message=envelope.get('error', ''), # error field is a string in v1.0
retryable=envelope.get('retryable', False), # v1.0 may or may not have this
)
case (2, _):
@ -456,9 +460,28 @@ def test_error_handler_not_found():
---
## Appendix: SCHEMAS.md Error Shape
## Appendix A: v1.0 Error Envelope (Current Binary)
For reference, the canonical JSON error envelope shape (SCHEMAS.md):
The actual shape emitted by the current binary (v1.0, flat):
```json
{
"error": "session 'nonexistent' not found in .claw/sessions",
"hint": "use 'list-sessions' to see available sessions",
"kind": "session_not_found",
"type": "error"
}
```
**Key differences from v2.0 schema (below):**
- `error` field is a **string**, not a structured object
- `kind` is at **top-level**, not nested under `error`
- Missing: `timestamp`, `command`, `exit_code`, `output_format`, `schema_version`
- Extra: `type: "error"` field (not in schema)
## Appendix B: SCHEMAS.md Target Shape (v2.0)
For reference, the target JSON error envelope shape (SCHEMAS.md, v2.0):
```json
{
@ -466,7 +489,7 @@ For reference, the canonical JSON error envelope shape (SCHEMAS.md):
"command": "load-session",
"exit_code": 1,
"output_format": "json",
"schema_version": "1.0",
"schema_version": "2.0",
"error": {
"kind": "session_not_found",
"operation": "session_store.load_session",
@ -478,7 +501,7 @@ For reference, the canonical JSON error envelope shape (SCHEMAS.md):
}
```
All commands that emit errors follow this shape (with error.kind varying). See `SCHEMAS.md` for the complete contract.
**This is the target schema after [`FIX_LOCUS_164`](./FIX_LOCUS_164.md) is implemented.** The migration plan includes a dual-mode `--envelope-version=2.0` flag in Phase 1, default version bump in Phase 2, and deprecation in Phase 3. For now, code against v1.0 (Appendix A).
---