Summary
The agent server's headless task stdout parser crashes when Claude Code emits a stream-json line that is a valid JSON string literal (not an object). This causes the entire execution to fail with "Task returned empty response" — discarding all work done during the session. Observed on a scheduled task that ran for ~9 minutes before the crash.
Component
Agent Server / claude_code.py / Headless Task stdout parser
Priority
P2
Error
ERROR:agent_server.services.claude_code:[Headless Task] Error reading stdout: 'str' object has no attribute 'get'
Downstream effect:
ERROR:services.task_execution_service:[TaskExecService] Failed to execute task on <agent>: Task returned empty response
Location
- File:
src/agent-server/agent_server/services/claude_code.py
- Line: ~883 (in the
_run_headless_with_streaming method)
- Function: stdout reading loop inside
_run_headless_with_streaming
Root Cause
The stdout reading loop does:
raw_msg = json.loads(line.strip()) # line 877
raw_msg = sanitize_dict(raw_msg) # line 879
...
if raw_msg.get("type") == "init" ... # line 883
json.loads() can successfully parse a JSON string literal like "some text" into a Python str. The code then calls .get() on that string, which raises AttributeError: 'str' object has no attribute 'get'.
This AttributeError is caught by the broad except Exception as e at ~line 912, which logs the error and exits the stdout reading loop entirely — meaning all subsequent output (including the result message with cost/session data) is never processed. The execution is then recorded as failed with null cost, null session_id, and no response.
Reproduction Steps
- Run a scheduled task that uses
context: fork with a subagent (or any task where Claude Code emits a non-object JSON line in stream-json mode)
- Observe the execution fails with "Task returned empty response"
- Agent container logs show:
Error reading stdout: 'str' object has no attribute 'get'
Suggested Fix
Add a type check after json.loads() to skip non-dict lines:
try:
raw_msg = json.loads(line.strip())
if not isinstance(raw_msg, dict):
# stream-json can emit string literals; skip them
continue
raw_msg = sanitize_dict(raw_msg)
raw_messages.append(raw_msg)
...
The same check should be added in process_stream_line() (line ~276) for defense in depth, though the primary fix in the stdout loop would prevent the crash.
Additionally, sanitize_dict() should handle non-dict input gracefully.
Environment
- Trinity version:
0523ccd
- Agent server: built into agent container image
Related
src/agent-server/agent_server/services/claude_code.py — both _run_headless_with_streaming and process_stream_line
parse_stream_json_output() (line 124) has the same pattern but already returns early on JSONDecodeError; it still calls .get() without checking type
Summary
The agent server's headless task stdout parser crashes when Claude Code emits a stream-json line that is a valid JSON string literal (not an object). This causes the entire execution to fail with "Task returned empty response" — discarding all work done during the session. Observed on a scheduled task that ran for ~9 minutes before the crash.
Component
Agent Server /
claude_code.py/ Headless Task stdout parserPriority
P2
Error
Downstream effect:
Location
src/agent-server/agent_server/services/claude_code.py_run_headless_with_streamingmethod)_run_headless_with_streamingRoot Cause
The stdout reading loop does:
json.loads()can successfully parse a JSON string literal like"some text"into a Pythonstr. The code then calls.get()on that string, which raisesAttributeError: 'str' object has no attribute 'get'.This
AttributeErroris caught by the broadexcept Exception as eat ~line 912, which logs the error and exits the stdout reading loop entirely — meaning all subsequent output (including theresultmessage with cost/session data) is never processed. The execution is then recorded as failed with null cost, null session_id, and no response.Reproduction Steps
context: forkwith a subagent (or any task where Claude Code emits a non-object JSON line in stream-json mode)Error reading stdout: 'str' object has no attribute 'get'Suggested Fix
Add a type check after
json.loads()to skip non-dict lines:The same check should be added in
process_stream_line()(line ~276) for defense in depth, though the primary fix in the stdout loop would prevent the crash.Additionally,
sanitize_dict()should handle non-dict input gracefully.Environment
0523ccdRelated
src/agent-server/agent_server/services/claude_code.py— both_run_headless_with_streamingandprocess_stream_lineparse_stream_json_output()(line 124) has the same pattern but already returns early onJSONDecodeError; it still calls.get()without checking type