Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/trace/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
{
"type": "command",
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/stop_hook.sh",
"async": true
"async": false
}
]
}
Expand Down
15 changes: 1 addition & 14 deletions plugins/trace/hooks/lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,12 @@ ensure_session_initialized() {
local requested_start_ns="${2:-}"
[[ -z "$sid" ]] && return 1

local trace_id session_span_id session_parent_span_id session_start_ns init_source root_emitted pending_tool_calls
local trace_id session_span_id session_parent_span_id session_start_ns init_source pending_tool_calls
trace_id="$(get_session_state "$sid" trace_id)"
session_span_id="$(get_session_state "$sid" session_span_id)"
session_parent_span_id="$(get_session_state "$sid" session_parent_span_id)"
session_start_ns="$(get_session_state "$sid" session_start_ns)"
init_source="$(get_session_state "$sid" session_init_source)"
root_emitted="$(get_session_state "$sid" session_root_emitted)"
pending_tool_calls="$(get_session_state "$sid" pending_tool_calls)"

# Normal path: SessionStart already created state.
Expand All @@ -262,9 +261,6 @@ ensure_session_initialized() {
if [[ -z "$init_source" ]]; then
set_session_state "$sid" session_init_source "unknown"
fi
if [[ -z "$root_emitted" ]]; then
set_session_state "$sid" session_root_emitted "false"
fi
if [[ -z "$pending_tool_calls" ]]; then
set_session_state "$sid" pending_tool_calls "[]"
fi
Expand All @@ -280,12 +276,6 @@ ensure_session_initialized() {
if [[ -z "$(get_session_state "$sid" trace_context_source)" ]]; then
set_session_state "$sid" trace_context_source "generated"
fi
if [[ -z "$(get_session_state "$sid" session_end_requested)" ]]; then
set_session_state "$sid" session_end_requested "false"
fi
if [[ -z "$(get_session_state "$sid" stop_in_flight)" ]]; then
set_session_state "$sid" stop_in_flight "false"
fi
return 0
fi

Expand All @@ -306,9 +296,6 @@ ensure_session_initialized() {
set_session_state "$sid" session_traceparent_version "${PL_INITIAL_TRACEPARENT_VERSION:-}"
set_session_state "$sid" session_trace_flags "${PL_INITIAL_TRACE_FLAGS:-}"
set_session_state "$sid" trace_context_source "${PL_INITIAL_TRACE_CONTEXT_SOURCE:-generated}"
set_session_state "$sid" session_root_emitted "false"
set_session_state "$sid" session_end_requested "false"
set_session_state "$sid" stop_in_flight "false"

log "INFO" "Session initialized lazily session_id=$sid trace_id=$trace_id"
}
Expand Down
77 changes: 44 additions & 33 deletions plugins/trace/hooks/parse_stop_transcript.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ def flatten_indexed(prefix, items, out):
out[attr_key] = value


def append_history_item(history, item):
if (
item.get("role") == "user"
and history
and history[-1].get("role") == "user"
and history[-1].get("content") == item.get("content")
):
return
history.append(item)


def is_tool_result_user(rec):
if rec.get("type") != "user":
return False
Expand Down Expand Up @@ -121,9 +132,7 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
turn_start_idx = i
break

turn_records = records[turn_start_idx:]
history = []
llm_input_cursor = 0
tools = []
llms = []
pending_tool_uses = []
Expand All @@ -134,9 +143,10 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
turn_end_ns = turn_start_fallback
last_input_ns = turn_start_fallback

for rec in turn_records:
for idx, rec in enumerate(records):
emit_for_turn = idx >= turn_start_idx
timestamp_ns = parse_iso_to_ns(rec.get("timestamp"))
if timestamp_ns is not None:
if emit_for_turn and timestamp_ns is not None:
if turn_start_ns is None or timestamp_ns < turn_start_ns:
turn_start_ns = timestamp_ns
if turn_end_ns is None or timestamp_ns > turn_end_ns:
Expand All @@ -148,7 +158,7 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
if operation == "enqueue":
content = content_to_text(rec.get("content"))
if content:
history.append({"role": "user", "content": content})
append_history_item(history, {"role": "user", "content": content})
last_input_ns = timestamp_ns or last_input_ns
saw_human_input = True
continue
Expand All @@ -171,7 +181,7 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
tool_use = pending_tool_uses.pop(match_idx) if match_idx is not None else {}

payload = {}
if pending_payload_idx < len(pending_payloads):
if emit_for_turn and pending_payload_idx < len(pending_payloads):
maybe_payload = pending_payloads[pending_payload_idx]
pending_payload_idx += 1
if isinstance(maybe_payload, dict):
Expand All @@ -191,21 +201,22 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
if tool_end_ns is None:
tool_end_ns = tool_start_ns

tools.append(
{
"name": f"Tool: {tool_name}",
"start_ns": int(tool_start_ns),
"end_ns": int(tool_end_ns),
"attributes": {
"source": "claude-code",
"hook": "PostToolUse",
"node_type": "CODE_EXECUTION",
"tool_name": tool_name,
"function_input": function_input,
"function_output": function_output,
},
}
)
if emit_for_turn:
tools.append(
{
"name": f"Tool: {tool_name}",
"start_ns": int(tool_start_ns),
"end_ns": int(tool_end_ns),
"attributes": {
"source": "claude-code",
"hook": "PostToolUse",
"node_type": "CODE_EXECUTION",
"tool_name": tool_name,
"function_input": function_input,
"function_output": function_output,
},
}
)

history.append(
{
Expand All @@ -218,7 +229,7 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
continue

user_text = content_to_text(content)
history.append({"role": "user", "content": user_text})
append_history_item(history, {"role": "user", "content": user_text})
last_input_ns = timestamp_ns or last_input_ns
saw_human_input = True
continue
Expand Down Expand Up @@ -284,6 +295,7 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
"source": "claude-code",
"hook": "Stop",
"node_type": "PROMPT_TEMPLATE",
"promptlayer.prompt_history_mode": "full_session",
"gen_ai.operation.name": "chat",
"gen_ai.provider.name": provider,
"gen_ai.request.model": model,
Expand All @@ -296,8 +308,7 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp
if stop_reason:
attrs["gen_ai.completion.0.finish_reason"] = stop_reason

immediate_input = history[llm_input_cursor:]
flatten_indexed("gen_ai.prompt", immediate_input, attrs)
flatten_indexed("gen_ai.prompt", history, attrs)

completion_item = {"role": "assistant", "content": output_text}
if tool_calls:
Expand All @@ -306,20 +317,20 @@ def parse_transcript(transcript_path, turn_start_fallback, pending_payloads, exp

span_name = "LLM Call (User)" if saw_human_input else "LLM call"

llms.append(
{
"name": span_name,
"start_ns": int(llm_start_ns),
"end_ns": int(llm_end_ns),
"attributes": attrs,
}
)
if emit_for_turn:
llms.append(
{
"name": span_name,
"start_ns": int(llm_start_ns),
"end_ns": int(llm_end_ns),
"attributes": attrs,
}
)

assistant_history = {"role": "assistant", "content": output_text}
if tool_calls:
assistant_history["tool_calls"] = tool_calls
history.append(assistant_history)
llm_input_cursor = len(history)
saw_human_input = False

if turn_start_ns is None:
Expand Down
20 changes: 0 additions & 20 deletions plugins/trace/hooks/session_end.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,8 @@ trace_id="$(get_session_state "$session_id" trace_id)"
session_span_id="$(get_session_state "$session_id" session_span_id)"
session_parent_span_id="$(get_session_state "$session_id" session_parent_span_id)"
session_start_ns="$(get_session_state "$session_id" session_start_ns)"
stop_in_flight="$(get_session_state "$session_id" stop_in_flight)"
current_turn_start_ns="$(get_session_state "$session_id" current_turn_start_ns)"
[[ -z "$trace_id" || -z "$session_span_id" ]] && exit 0
[[ -z "$session_start_ns" ]] && session_start_ns="$(now_ns)"
[[ -z "$stop_in_flight" ]] && stop_in_flight="false"

if [[ -n "$current_turn_start_ns" || "$stop_in_flight" == "true" ]]; then
set_session_state "$session_id" session_end_requested "true"
log "INFO" "SessionEnd deferred until Stop session_id=$session_id"
exit 0
fi

release_session_lock
trap - EXIT
Expand All @@ -42,16 +33,5 @@ emit_span "$trace_id" "$session_span_id" "$session_parent_span_id" "Claude Code

acquire_session_lock "$session_id" || exit 0
trap 'release_session_lock' EXIT

stop_in_flight="$(get_session_state "$session_id" stop_in_flight)"
current_turn_start_ns="$(get_session_state "$session_id" current_turn_start_ns)"
[[ -z "$stop_in_flight" ]] && stop_in_flight="false"
if [[ -n "$current_turn_start_ns" || "$stop_in_flight" == "true" ]]; then
set_session_state "$session_id" session_end_requested "true"
log "INFO" "SessionEnd deferred until Stop session_id=$session_id"
exit 0
fi

set_session_state "$session_id" session_root_emitted "true"
rm -f "$PL_SESSION_STATE_DIR/$session_id.json"
log "INFO" "SessionEnd finalized session_id=$session_id"
9 changes: 0 additions & 9 deletions plugins/trace/hooks/session_start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ if [[ -n "$existing_trace_id" && -n "$existing_session_span_id" ]]; then
if [[ -z "$(get_session_state "$session_id" trace_context_source)" ]]; then
set_session_state "$session_id" trace_context_source "generated"
fi
if [[ -z "$(get_session_state "$session_id" session_end_requested)" ]]; then
set_session_state "$session_id" session_end_requested "false"
fi
if [[ -z "$(get_session_state "$session_id" stop_in_flight)" ]]; then
set_session_state "$session_id" stop_in_flight "false"
fi
log "INFO" "SessionStart ignored existing state session_id=$session_id trace_id=$existing_trace_id"
exit 0
fi
Expand All @@ -59,8 +53,5 @@ set_session_state "$session_id" session_init_source "session_start_hook"
set_session_state "$session_id" session_traceparent_version "${PL_INITIAL_TRACEPARENT_VERSION:-}"
set_session_state "$session_id" session_trace_flags "${PL_INITIAL_TRACE_FLAGS:-}"
set_session_state "$session_id" trace_context_source "${PL_INITIAL_TRACE_CONTEXT_SOURCE:-generated}"
set_session_state "$session_id" session_root_emitted "false"
set_session_state "$session_id" session_end_requested "false"
set_session_state "$session_id" stop_in_flight "false"

log "INFO" "SessionStart captured session_id=$session_id trace_id=$trace_id"
55 changes: 0 additions & 55 deletions plugins/trace/hooks/stop_hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,21 @@ session_span_id="$(get_session_state "$session_id" session_span_id)"
session_parent_span_id="$(get_session_state "$session_id" session_parent_span_id)"
turn_start_ns="$(get_session_state "$session_id" current_turn_start_ns)"
pending_tool_calls="$(get_session_state "$session_id" pending_tool_calls)"
session_end_requested="$(get_session_state "$session_id" session_end_requested)"
session_init_source="$(get_session_state "$session_id" session_init_source)"
session_start_ns="$(get_session_state "$session_id" session_start_ns)"

[[ -z "$trace_id" || -z "$session_span_id" ]] && exit 0
[[ -z "$pending_tool_calls" ]] && pending_tool_calls='[]'
[[ -z "$session_end_requested" ]] && session_end_requested="false"
[[ -z "$session_start_ns" ]] && session_start_ns="$(now_ns)"

[[ -z "$turn_start_ns" ]] && turn_start_ns="$(now_ns)"

# Keep lock scope short: snapshot + clear turn-specific mutable state.
set_session_state "$session_id" stop_in_flight "true"
set_session_state "$session_id" current_turn_start_ns ""
set_session_state "$session_id" pending_tool_calls "[]"

release_session_lock

emitted_root="false"

parse_transcript_with_retry() {
local attempts=0
local parsed llm_count
Expand Down Expand Up @@ -101,7 +96,6 @@ else
fi
session_attrs="{\"source\":\"claude-code\",\"hook\":\"$session_hook_attr\",\"node_type\":\"WORKFLOW\",\"session.lifecycle\":\"$session_lifecycle_attr\"}"
add_span_to_batch "$trace_id" "$session_span_id" "$session_parent_span_id" "Claude Code session" "1" "$session_start_ns" "$turn_end_ns" "$session_attrs" || true
emitted_root="true"

while IFS= read -r tool; do
[[ -z "$tool" ]] && continue
Expand All @@ -124,55 +118,6 @@ else
done < <(echo "$parsed" | jq -c '.llms[]?')
fi

# If SessionEnd arrived while Stop was running, re-emit root span with final end time.
if [[ "$session_end_requested" == "true" ]]; then
end_ns="$(now_ns)"
session_end_attrs='{"source":"claude-code","hook":"SessionEnd","node_type":"WORKFLOW","session.lifecycle":"deferred_finalize"}'
add_span_to_batch "$trace_id" "$session_span_id" "$session_parent_span_id" "Claude Code session" "1" "$session_start_ns" "$end_ns" "$session_end_attrs" || true
emitted_root="true"
fi

emit_spans_batch_file "$spans_file" || true

acquire_session_lock "$session_id" || exit 0

# Stop is no longer actively processing this turn.
set_session_state "$session_id" stop_in_flight "false"
if [[ "$emitted_root" == "true" ]]; then
set_session_state "$session_id" session_root_emitted "true"
fi

latest_end_requested="$(get_session_state "$session_id" session_end_requested)"
latest_turn_start_ns="$(get_session_state "$session_id" current_turn_start_ns)"
latest_trace_id="$(get_session_state "$session_id" trace_id)"
latest_session_span_id="$(get_session_state "$session_id" session_span_id)"
latest_session_parent_span_id="$(get_session_state "$session_id" session_parent_span_id)"
latest_session_start_ns="$(get_session_state "$session_id" session_start_ns)"
[[ -z "$latest_end_requested" ]] && latest_end_requested="false"
[[ -z "$latest_session_start_ns" ]] && latest_session_start_ns="$(now_ns)"

need_finalize_root="false"
if [[ "$latest_end_requested" == "true" && -z "$latest_turn_start_ns" ]]; then
need_finalize_root="true"
fi

if [[ "$need_finalize_root" == "true" && -n "$latest_trace_id" && -n "$latest_session_span_id" ]]; then
release_session_lock
end_ns="$(now_ns)"
finalize_attrs='{"source":"claude-code","hook":"SessionEnd","node_type":"WORKFLOW","session.lifecycle":"deferred_finalize"}'
emit_span "$latest_trace_id" "$latest_session_span_id" "$latest_session_parent_span_id" "Claude Code session" "1" "$latest_session_start_ns" "$end_ns" "$finalize_attrs" || true
acquire_session_lock "$session_id" || exit 0
set_session_state "$session_id" stop_in_flight "false"
set_session_state "$session_id" session_root_emitted "true"
fi

latest_end_requested="$(get_session_state "$session_id" session_end_requested)"
latest_turn_start_ns="$(get_session_state "$session_id" current_turn_start_ns)"
[[ -z "$latest_end_requested" ]] && latest_end_requested="false"

if [[ "$latest_end_requested" == "true" && -z "$latest_turn_start_ns" ]]; then
rm -f "$PL_SESSION_STATE_DIR/$session_id.json"
log "INFO" "SessionEnd finalized by Stop session_id=$session_id"
fi

log "INFO" "Stop finalized session_id=$session_id"
1 change: 1 addition & 0 deletions plugins/trace/testdata/stop_input_full_history.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"session_id":"example-session-id","transcript_path":"plugins/trace/testdata/stop_transcript_full_history.jsonl"}
6 changes: 6 additions & 0 deletions plugins/trace/testdata/stop_transcript_full_history.jsonl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{"sessionId":"example-session-id","type":"user","timestamp":"2026-03-16T12:00:00Z","message":{"content":[{"type":"text","text":"hello"}]}}
{"sessionId":"example-session-id","type":"assistant","timestamp":"2026-03-16T12:00:01Z","message":{"model":"claude-sonnet-4-6","id":"msg_example_1","stop_reason":"end_turn","usage":{"input_tokens":12,"output_tokens":6},"content":[{"type":"text","text":"Hi there"}]}}
{"sessionId":"example-session-id","type":"user","timestamp":"2026-03-16T12:01:00Z","message":{"content":[{"type":"text","text":"check the current status"}]}}
{"sessionId":"example-session-id","type":"assistant","timestamp":"2026-03-16T12:01:01Z","message":{"model":"claude-sonnet-4-6","id":"msg_example_2","stop_reason":"tool_use","usage":{"input_tokens":40,"output_tokens":12},"content":[{"type":"tool_use","id":"toolu_example_1","name":"DocsSearch","input":{"query":"current status"}}]}}
{"sessionId":"example-session-id","type":"user","timestamp":"2026-03-16T12:01:02Z","message":{"content":[{"type":"tool_result","tool_use_id":"toolu_example_1","content":"Current status: all systems operational.","is_error":false}]}}
{"sessionId":"example-session-id","type":"assistant","timestamp":"2026-03-16T12:01:03Z","message":{"model":"claude-sonnet-4-6","id":"msg_example_3","stop_reason":"end_turn","usage":{"input_tokens":55,"output_tokens":18},"content":[{"type":"text","text":"All systems are operational."}]}}
Loading
Loading