mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-27 01:04:58 +08:00
fix: #165 — load-session CLI now parity-matches list/delete (--directory, --output-format, typed JSON errors)
The #160 session-lifecycle CLI triplet was asymmetric: list-sessions and delete-session accepted --directory + --output-format and emitted typed JSON error envelopes, but load-session had neither flag and dumped a raw Python traceback (including the SessionNotFoundError class name) on a missing session. Three concrete impacts this fix closes: 1. Alternate session-store locations (e.g. /tmp/claw-run-XXX/.port_sessions) were unreachable via load-session; claws had to chdir or monkeypatch DEFAULT_SESSION_DIR to work around it. 2. Not-found emitted a multi-line Python stack, not a parseable envelope. Claws deciding retry/escalate/give-up had only exit code 1 to work with. 3. The traceback leaked 'src.session_store.SessionNotFoundError' verbatim, coupling version-pinned claws to our internal exception class name. Now all three triplet commands accept the same flag pair and emit the same JSON error shape: Success (json mode): {"session_id": "alpha", "loaded": true, "messages_count": 3, "input_tokens": 42, "output_tokens": 99} Not-found: {"session_id": "missing", "loaded": false, "error": {"kind": "session_not_found", "message": "session 'missing' not found in /path", "directory": "/path", "retryable": false}} Corrupted file: {"session_id": "broken", "loaded": false, "error": {"kind": "session_load_failed", "message": "...", "directory": "/path", "retryable": true}} Exit code contract: - 0 on successful load - 1 on not-found (preserves existing $?) - 1 on OSError/JSONDecodeError (distinct 'kind' in JSON) Backward compat: legacy 'claw load-session ID' text output unchanged byte-for-byte. Only new behaviour is the flags and structured error path. Tests (tests/test_load_session_cli.py, 13 tests): - TestDirectoryFlagParity (2): --directory works + fallback to CWD/.port_sessions - TestOutputFormatFlagParity (2): json schema + text-mode backward compat - TestNotFoundTypedError (2): JSON envelope on not-found; no traceback in either mode; no internal class name leak - TestLoadFailedDistinctFromNotFound (1): corrupted file = session_load_failed with retryable=true, distinct from session_not_found - TestTripletParityConsistency (6): parametrised over [list, delete, load] * [--directory, --output-format] — explicit parity guard for future regressions Full suite: 80/80 passing, zero regression. Discovered via Jobdori dogfood sweep 2026-04-22 17:44 KST — ran 'claw load-session nonexistent' expecting a clean error, got a Python traceback. Filed #165 + fixed in same commit. Closes ROADMAP #165.
This commit is contained in:
69
src/main.py
69
src/main.py
@@ -84,8 +84,20 @@ def build_parser() -> argparse.ArgumentParser:
|
||||
flush_parser = subparsers.add_parser('flush-transcript', help='persist and flush a temporary session transcript')
|
||||
flush_parser.add_argument('prompt')
|
||||
|
||||
load_session_parser = subparsers.add_parser('load-session', help='load a previously persisted session')
|
||||
load_session_parser = subparsers.add_parser(
|
||||
'load-session',
|
||||
help='load a previously persisted session (#160/#165: claw-native session API)',
|
||||
)
|
||||
load_session_parser.add_argument('session_id')
|
||||
load_session_parser.add_argument(
|
||||
'--directory', help='session storage directory (default: .port_sessions)'
|
||||
)
|
||||
load_session_parser.add_argument(
|
||||
'--output-format',
|
||||
choices=['text', 'json'],
|
||||
default='text',
|
||||
help='output format',
|
||||
)
|
||||
|
||||
list_sessions_parser = subparsers.add_parser(
|
||||
'list-sessions',
|
||||
@@ -227,8 +239,59 @@ def main(argv: list[str] | None = None) -> int:
|
||||
print(f'flushed={engine.transcript_store.flushed}')
|
||||
return 0
|
||||
if args.command == 'load-session':
|
||||
session = load_session(args.session_id)
|
||||
print(f'{session.session_id}\n{len(session.messages)} messages\nin={session.input_tokens} out={session.output_tokens}')
|
||||
from pathlib import Path as _Path
|
||||
directory = _Path(args.directory) if args.directory else None
|
||||
# #165: catch typed SessionNotFoundError + surface a JSON error envelope
|
||||
# matching the delete-session contract shape. No more raw tracebacks.
|
||||
try:
|
||||
session = load_session(args.session_id, directory)
|
||||
except SessionNotFoundError as exc:
|
||||
if args.output_format == 'json':
|
||||
import json as _json
|
||||
resolved_dir = str(directory) if directory else '.port_sessions'
|
||||
print(_json.dumps({
|
||||
'session_id': args.session_id,
|
||||
'loaded': False,
|
||||
'error': {
|
||||
'kind': 'session_not_found',
|
||||
'message': str(exc),
|
||||
'directory': resolved_dir,
|
||||
'retryable': False,
|
||||
},
|
||||
}))
|
||||
else:
|
||||
print(f'error: {exc}')
|
||||
return 1
|
||||
except (OSError, ValueError) as exc:
|
||||
# Corrupted session file, IO error, JSON decode error — distinct
|
||||
# from 'not found'. Callers may retry here (fs glitch).
|
||||
if args.output_format == 'json':
|
||||
import json as _json
|
||||
resolved_dir = str(directory) if directory else '.port_sessions'
|
||||
print(_json.dumps({
|
||||
'session_id': args.session_id,
|
||||
'loaded': False,
|
||||
'error': {
|
||||
'kind': 'session_load_failed',
|
||||
'message': str(exc),
|
||||
'directory': resolved_dir,
|
||||
'retryable': True,
|
||||
},
|
||||
}))
|
||||
else:
|
||||
print(f'error: {exc}')
|
||||
return 1
|
||||
if args.output_format == 'json':
|
||||
import json as _json
|
||||
print(_json.dumps({
|
||||
'session_id': session.session_id,
|
||||
'loaded': True,
|
||||
'messages_count': len(session.messages),
|
||||
'input_tokens': session.input_tokens,
|
||||
'output_tokens': session.output_tokens,
|
||||
}))
|
||||
else:
|
||||
print(f'{session.session_id}\n{len(session.messages)} messages\nin={session.input_tokens} out={session.output_tokens}')
|
||||
return 0
|
||||
if args.command == 'list-sessions':
|
||||
from pathlib import Path as _Path
|
||||
|
||||
Reference in New Issue
Block a user