KeiSeiKit-1.0/hooks/extract-task-durations.sh
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

103 lines
4.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# extract-task-durations.sh — Stop event: scan current session trace for
# <task-notification> blocks, extract duration_ms, append to tasks.jsonl.
#
# Why: PreToolUse:Agent + PostToolUse:Agent fire at the SAME time for
# async (isolation:worktree) agents — Claude Code returns immediately
# from the spawn ("Agent launched in background"), and real completion
# arrives as task-notification (NOT a hook event). So task-timer.sh
# can't measure async durations from Pre/Post timing.
#
# This hook fixes that: at Stop, we read the session trace, find every
# task-notification block, parse `task-id` + `duration_ms`, and append
# real duration entries. Idempotent: skips IDs already in tasks.jsonl.
#
# Reads (JSON stdin):
# .session_id — session UUID, used to locate the trace file
#
# Writes:
# ~/.claude/memory/time-metrics/tasks.jsonl (appends real entries)
set -uo pipefail
JOURNAL_DIR="$HOME/.claude/memory/time-metrics"
JOURNAL="$JOURNAL_DIR/tasks.jsonl"
mkdir -p "$JOURNAL_DIR"
INPUT="$(cat 2>/dev/null || true)"
SESSION_ID="$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)"
[[ -z "$SESSION_ID" ]] && SESSION_ID="${CLAUDE_SESSION_ID:-}"
[[ -z "$SESSION_ID" ]] && exit 0 # no session = no trace = nothing to do
TRACE="$HOME/.claude/memory/traces/${SESSION_ID}.jsonl"
[[ ! -f "$TRACE" ]] && exit 0 # no trace yet (first session?) — silent
NOW_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Build set of already-recorded tool_use_ids to avoid duplicates.
KNOWN=$(grep -oE '"id":"toolu_[A-Za-z0-9_]+"' "$JOURNAL" 2>/dev/null \
| sort -u | sed 's/"id":"//;s/"//g')
# Scan trace for task-notification XML-ish blocks. Format observed:
# <task-notification>
# <task-id>aXXX...</task-id>
# <tool-use-id>toolu_XXX...</tool-use-id>
# <summary>...</summary>
# <result>...</result>
# <usage><total_tokens>N</total_tokens><tool_uses>M</tool_uses>
# <duration_ms>NNNNN</duration_ms></usage>
# </task-notification>
#
# We emit one tasks.jsonl line per notification with REAL duration_ms.
added=0
while IFS= read -r line; do
[[ -z "$line" ]] && continue
# Extract from this single line (each trace line is a JSON message).
# Look for embedded task-notification using string ops.
if [[ "$line" != *"task-notification"* ]]; then continue; fi
# Parse fields. The trace stores assistant/user message text as JSON
# strings, so the XML-ish block lives inside escaped content.
task_id=$(echo "$line" | grep -oE '<task-id>[a-z0-9]+</task-id>' | head -1 \
| sed 's|<task-id>||;s|</task-id>||')
tool_use_id=$(echo "$line" | grep -oE '<tool-use-id>toolu_[A-Za-z0-9_]+</tool-use-id>' | head -1 \
| sed 's|<tool-use-id>||;s|</tool-use-id>||')
summary=$(echo "$line" | grep -oE '<summary>[^<]+</summary>' | head -1 \
| sed 's|<summary>||;s|</summary>||' | head -c 200)
duration_ms=$(echo "$line" | grep -oE '<duration_ms>[0-9]+</duration_ms>' | head -1 \
| sed 's|<duration_ms>||;s|</duration_ms>||')
total_tokens=$(echo "$line" | grep -oE '<total_tokens>[0-9]+</total_tokens>' | head -1 \
| sed 's|<total_tokens>||;s|</total_tokens>||')
# Skip if we lack the minimum to record.
[[ -z "$tool_use_id" ]] && continue
[[ -z "$duration_ms" ]] && continue
# Skip if already in journal.
if grep -q "\"id\":\"$tool_use_id\".*\"duration_s\":[1-9]" "$JOURNAL" 2>/dev/null; then
continue
fi
# Convert ms → s (round, not truncate, so 999ms → 1s).
duration_s=$(( (duration_ms + 500) / 1000 ))
# Escape summary for JSON (quotes already stripped by sed; just guard
# against rogue backslashes).
safe_summary=$(echo "$summary" | sed 's/\\/\\\\/g; s/"/\\"/g')
# Append. Use a marker so future re-runs see this is from extraction.
printf '{"kind":"task","id":"%s","agent_id":"%s","desc":"%s","duration_s":%s,"duration_ms":%s,"total_tokens":%s,"source":"task-notification","ts":"%s"}\n' \
"$tool_use_id" "${task_id:-}" "$safe_summary" "$duration_s" "$duration_ms" "${total_tokens:-0}" "$NOW_ISO" \
>> "$JOURNAL"
added=$((added + 1))
done < "$TRACE"
# Quiet on no-op; brief stderr note if we added.
if [[ $added -gt 0 ]]; then
echo "extract-task-durations: appended $added entries from $(basename "$TRACE")" >&2
fi
exit 0