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 .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "autodev",
"description": "Autonomous development workflow skills for coding agents",
"version": "6.1.4",
"version": "6.1.5",
"source": "./",
"author": {
"name": "Jon Langevin",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "autodev",
"description": "Autonomous development workflow skills for coding agents: design, review, planning, execution, monitoring, and retrospectives",
"version": "6.1.4",
"version": "6.1.5",
"author": {
"name": "Jon Langevin",
"email": "jon@gocodealone.com"
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "autodev",
"displayName": "Autonomous Dev Kit",
"description": "Autonomous development workflow skills for coding agents",
"version": "6.1.4",
"version": "6.1.5",
"author": {
"name": "Jon Langevin",
"email": "jon@gocodealone.com"
Expand Down
9 changes: 9 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Autonomous Dev Kit Release Notes

## v6.1.5 — 2026-05-28

SessionStart time-based dedup as defense in depth.

- `hooks/session-start`: added a 5-second time-window dedup that suppresses ANY rapid re-fire regardless of payload shape. The per-`session_id:source_kind` dedup added in v6.1.2 only covers startup-class fires where the host populates `session_id` consistently. Codex was observed firing SessionStart 9+ times in rapid succession near session limits — possibly during internal resume/wrap-up lifecycle events — with rotating `session_id` and source values our existing dedup didn't anticipate. Time dedup catches that uniformly: if any SessionStart payload was emitted in the last 5 seconds, the new fire short-circuits silently.
- The 5-second window is intentionally short so legitimate user-driven compacts spaced minutes apart still emit their resumption context. The observed bug was 9 fires inside ~1 second.
- Added regression test asserting four rapid fires with rotating `session_id` + source produce exactly one emission.
- Test isolation: `tests/hook-contracts.sh` SessionStart tests now use isolated tmpdir cwds so the per-cwd dedup state doesn't leak across tests.

## v6.1.4 — 2026-05-28

PreToolUse guard quote-strip extended to all destructive-command checks.
Expand Down
31 changes: 31 additions & 0 deletions hooks/session-start
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,32 @@ STATE_DIR="${cwd_dir}/.claude/autodev-state"
STATE_FILE="${STATE_DIR}/in-progress.jsonl"
SEEN_FILE="${STATE_DIR}/session-start-seen"
SESSION_ID_FILE="${STATE_DIR}/current-session-id"
LAST_EMIT_FILE="${STATE_DIR}/session-start-last-emit"

# Time-based dedup (defense in depth, applies to ALL source kinds).
#
# The per-session_id:source_kind dedup below handles startup-class re-fires
# where Codex / the host populates session_id consistently. But Codex has
# also been observed firing SessionStart 9+ times in rapid succession near
# session limits (and on resume/wrap-up lifecycle events), with payload
# fields we don't always see -- session_id may rotate, source may be a
# value we don't anticipate, agent_id may be absent on subagent-style
# fires. Time dedup catches all of those regardless of payload shape:
# if a SessionStart payload was emitted in the last N seconds, skip.
#
# Window is intentionally short (5s) so legitimate user-driven compacts
# spaced minutes apart still emit their resumption context. The 9-in-
# rapid-succession bug happens within ~1 second.
DEDUP_WINDOW_SEC=5
if [ -f "$LAST_EMIT_FILE" ]; then
now=$(date +%s 2>/dev/null || echo 0)
last=$(stat -f %m "$LAST_EMIT_FILE" 2>/dev/null \
|| stat -c %Y "$LAST_EMIT_FILE" 2>/dev/null \
|| echo 0)
if [ "$now" -gt 0 ] && [ "$last" -gt 0 ] && [ $((now - last)) -lt "$DEDUP_WINDOW_SEC" ]; then
exit 0
fi
fi

# Detect session transitions via session_id rather than source=startup.
# Codex has been observed re-firing SessionStart 6+ times on a single
Expand Down Expand Up @@ -207,4 +233,9 @@ PY

emit_additional_context "SessionStart" "$session_context"

# Touch the dedup timestamp file AFTER emit. Subsequent invocations within
# DEDUP_WINDOW_SEC will short-circuit via the time-dedup check at top.
mkdir -p "$STATE_DIR" 2>/dev/null || true
: > "$LAST_EMIT_FILE" 2>/dev/null || true

exit 0
57 changes: 53 additions & 4 deletions tests/hook-contracts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,53 @@ assert_hook_context_json() {
pass "${name}: emits valid ${event} additionalContext JSON"
}

test_session_start_time_dedup_suppresses_rapid_refires() {
# Regression for v6.1.5: Codex was observed firing SessionStart 9+ times
# in rapid succession near session limits. Session-id-based dedup misses
# this when session_id rotates or source is a value we don't anticipate.
# Time-based dedup (default 5s window) must catch ALL rapid re-fires
# regardless of payload shape -- different session_id, different source.
local tmp out1 out2 out3 out4
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' RETURN
# Fire 1: fresh state, emits.
out1="$(run_hook session-start '{"source":"startup","cwd":"'"$tmp"'","session_id":"sA"}')"
if [ -z "$out1" ]; then
fail "session-start: first fire must emit, got empty"
return
fi
# Fire 2: same session+source within window -> suppressed by session-id dedup OR time dedup.
out2="$(run_hook session-start '{"source":"startup","cwd":"'"$tmp"'","session_id":"sA"}')"
if [ -n "$out2" ]; then
fail "session-start: same-session re-fire within window must be suppressed, got: ${out2}"
return
fi
# Fire 3: rotated session_id, same window -> session-id dedup wouldn't catch this;
# time dedup must.
out3="$(run_hook session-start '{"source":"startup","cwd":"'"$tmp"'","session_id":"sB"}')"
if [ -n "$out3" ]; then
fail "session-start: rotated-session_id re-fire within window must be suppressed (time dedup), got: ${out3}"
return
fi
# Fire 4: different source (compact, normally NOT deduped), same window.
# Time dedup must still suppress to prevent the 9-in-rapid-succession bug.
out4="$(run_hook session-start '{"source":"compact","cwd":"'"$tmp"'","session_id":"sC"}')"
if [ -n "$out4" ]; then
fail "session-start: compact re-fire within window must be suppressed (time dedup), got: ${out4}"
return
fi
pass "session-start: time-based dedup suppresses rapid re-fires across session_id/source rotations"
}

test_session_start_json() {
local output
output="$(run_hook session-start '{"source":"startup","cwd":"'"$REPO_ROOT"'"}')"
# Use isolated tmpdir cwd so the hook's per-cwd state dir
# (.claude/autodev-state) doesn't leak across tests -- the time-based
# dedup added in v6.1.5 would otherwise suppress emissions in tests
# that share the same cwd within the 5-second window.
local tmp output
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' RETURN
output="$(run_hook session-start '{"source":"startup","cwd":"'"$tmp"'"}')"
assert_hook_context_json "session-start" "SessionStart" "$output"
}

Expand Down Expand Up @@ -193,13 +237,17 @@ test_subagent_scope_guard_block_exits_zero_with_stderr_reason() {
}

test_wrapper_suppresses_unavailable_c_utf8_locale_noise() {
local tmp stdout_file stderr_file stderr_text stdout_text
local tmp stdout_file stderr_file stderr_text stdout_text cwd_dir
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' RETURN
stdout_file="$tmp/stdout.json"
stderr_file="$tmp/stderr.txt"
# Use isolated cwd so v6.1.5's time-based session-start dedup doesn't
# suppress this emission when other tests just ran in REPO_ROOT.
cwd_dir="$tmp/cwd"
mkdir -p "$cwd_dir"

run_hook_wrapper session-start '{"source":"startup","cwd":"'"$REPO_ROOT"'"}' "$stdout_file" "$stderr_file"
run_hook_wrapper session-start '{"source":"startup","cwd":"'"$cwd_dir"'"}' "$stdout_file" "$stderr_file"
stdout_text="$(cat "$stdout_file")"
stderr_text="$(cat "$stderr_file")"

Expand Down Expand Up @@ -1472,6 +1520,7 @@ JSONL

require_jq
test_session_start_json
test_session_start_time_dedup_suppresses_rapid_refires
test_wrapper_suppresses_unavailable_c_utf8_locale_noise
test_prompt_strict_json
test_prompt_strict_no_output_without_trigger
Expand Down
Loading