Skip to content

fix: stale session-state guard + SID fallback for _notify_once - #13

Merged
teseo merged 5 commits into
mainfrom
fix/stale-session-state-and-sid-fallback
Mar 17, 2026
Merged

fix: stale session-state guard + SID fallback for _notify_once#13
teseo merged 5 commits into
mainfrom
fix/stale-session-state-and-sid-fallback

Conversation

@teseo

@teseo teseo commented Mar 16, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • SID fallback: When HUMAN_GUARD_SESSION_ID is not set (launched from IDE or bypassing the wrapper), _notify_once now falls back to session_id from session-state.json for deduplication. Previously, notifications fired on every tool use.
  • Staleness guard: If NOW >= max_epoch + min_break_seconds, the session-state is from a previous session that has ended. Skip informational notifications silently. Previously, stale state triggered the session_limit message in brand new sessions.

Test plan

  • test_stale_session_state_no_output: stale state produces no message
  • test_fresh_over_limit_still_fires: active session past limit still notifies
  • test_no_sid_env_falls_back_to_state_session_id: marker uses state session_id
  • test_no_sid_notification_fires_once_not_every_call: dedup works without env SID
  • test_with_sid_env_still_works: existing wrapper behavior unchanged
  • Full suite: 107 passed

Summary by CodeRabbit

  • New Features

    • Adds a stale-session guard that skips enforcement for sessions older than max duration plus a configurable break, reducing spurious notifications.
    • Session ID now falls back to stored session state when the env var is absent.
    • Exposes additional session metadata (start epoch and session-state SID) for decision logic.
  • Tests

    • New integration tests covering stale-session behavior, SID fallback, one-shot notifications, and marker handling.
  • Documentation

    • Updated comments/docs to reflect fallback and stale-session logic.

@coderabbitai

coderabbitai Bot commented Mar 16, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Guard hook now reads start_epoch, session_id, and min_break_seconds from session-state, falls back to state session_id when HUMAN_GUARD_SESSION_ID is unset, and adds a stale-session guard that early-exits when now ≥ max_epoch + min_break_seconds; tests added for stale-session and SID fallback.

Changes

Cohort / File(s) Summary
Guard Hook Script
guard/hook.sh
Reads additional fields (start_epoch, session_id, min_break_seconds) from session-state; derives SID as `HUMAN_GUARD_SESSION_ID
Integration Tests
tests/test_guard.py
Adds TestStaleStateAndSidFallback suite, hook_env fixture and _make_state helper; tests stale-session suppression, fresh-session firing within min_break_seconds, SID fallback vs env SID semantics, one-shot notification markers, and staleness ordering — note: the new test class appears duplicated in the diff.
Manifest
manifest.*
Minor line changes (+16/-7) reflecting the script and test additions.

Sequence Diagram

sequenceDiagram
    participant Hook as Hook Script
    participant State as Session-State JSON
    participant Env as Environment
    participant FS as Filesystem (Markers)
    participant Out as Output/Exit

    Hook->>State: read max_epoch, start_epoch, session_id, min_break_seconds
    Hook->>Env: read HUMAN_GUARD_SESSION_ID
    Env-->>Hook: env SID or empty
    Hook->>Hook: derive SID = env SID || state.session_id
    Hook->>Hook: compute STALE_LIMIT = max_epoch + min_break_seconds
    Hook->>Hook: compare NOW >= STALE_LIMIT?
    alt Stale (NOW ≥ STALE_LIMIT)
        Hook->>Out: exit 0 (suppress notifications)
    else Fresh (NOW < STALE_LIMIT)
        Hook->>FS: check marker .notified.session_limit.<SID>
        FS-->>Hook: marker exists?
        alt no marker
            Hook->>Out: emit session_limit notification
            Hook->>FS: write .notified.session_limit.<SID>
        else marker exists
            Hook->>Out: exit 0 (one-shot suppression)
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I found the session, checked the time and tide,
If age has crept, I fold my ears and hide.
Env or state — your SID I will trace,
One hop of notice, then vanish from the place.
I nibble markers, then nap in the log's warm embrace.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and concisely describes the main changes: introducing a stale session-state guard and SID fallback for _notify_once, which are the primary functional additions in this PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/stale-session-state-and-sid-fallback
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
guard/hook.sh (1)

103-106: Make the stale guard independent of start_epoch.

Lines 103-106 already derive staleness from MAX_EPOCH + min_break_seconds, so START_EPOCH > 0 only disables the fix for legacy or partially-written state files that omit start_epoch.

♻️ Minimal change
-if [ "$NOW" -ge "$MAX_EPOCH" ] && [ "$START_EPOCH" -gt 0 ] 2>/dev/null; then
+if [ "$NOW" -ge "$MAX_EPOCH" ]; then
   MIN_BREAK_STALE=$(jq -r '.min_break_seconds // 900' "$STATE" 2>/dev/null || echo 900)
   STALE_LIMIT=$(( MAX_EPOCH + MIN_BREAK_STALE ))
   [ "$NOW" -ge "$STALE_LIMIT" ] && exit 0
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@guard/hook.sh` around lines 103 - 106, The staleness check currently gates on
START_EPOCH > 0 which prevents the MAX_EPOCH + min_break_seconds logic from
running for legacy/partial state files; change the conditional so it does not
depend on START_EPOCH: evaluate if [ "$NOW" -ge "$MAX_EPOCH" ] (preserving the
2>/dev/null) and then compute MIN_BREAK_STALE from the STATE via jq and set
STALE_LIMIT=$(( MAX_EPOCH + MIN_BREAK_STALE )), finally exit 0 if [ "$NOW" -ge
"$STALE_LIMIT" ]; keep the same variable names (NOW, MAX_EPOCH, START_EPOCH,
MIN_BREAK_STALE, STALE_LIMIT, STATE) so the logic is independent of START_EPOCH
but still supports existing legacy handling elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@guard/hook.sh`:
- Around line 101-107: The stale-session guard currently only checks
START_EPOCH/MAX_EPOCH; also read and validate the stored end_allowed_epoch from
the state (e.g. END_ALLOWED_EPOCH=$(jq -r '.end_allowed_epoch // 0' "$STATE"
2>/dev/null || echo 0)) and treat the state as stale if that epoch is older than
NOW (or older than MAX_EPOCH as appropriate); add a check alongside the existing
START_EPOCH check so if END_ALLOWED_EPOCH is stale the script exits 0 just like
the existing stale-case (update variables END_ALLOWED_EPOCH and include it in
the stale-limit logic or a separate comparison mirroring the START_EPOCH logic).

---

Nitpick comments:
In `@guard/hook.sh`:
- Around line 103-106: The staleness check currently gates on START_EPOCH > 0
which prevents the MAX_EPOCH + min_break_seconds logic from running for
legacy/partial state files; change the conditional so it does not depend on
START_EPOCH: evaluate if [ "$NOW" -ge "$MAX_EPOCH" ] (preserving the
2>/dev/null) and then compute MIN_BREAK_STALE from the STATE via jq and set
STALE_LIMIT=$(( MAX_EPOCH + MIN_BREAK_STALE )), finally exit 0 if [ "$NOW" -ge
"$STALE_LIMIT" ]; keep the same variable names (NOW, MAX_EPOCH, START_EPOCH,
MIN_BREAK_STALE, STALE_LIMIT, STATE) so the logic is independent of START_EPOCH
but still supports existing legacy handling elsewhere.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ed2a59b0-7904-4b64-a1ef-b396a88e6c94

📥 Commits

Reviewing files that changed from the base of the PR and between caefd8e and 1ead71a.

📒 Files selected for processing (2)
  • guard/hook.sh
  • tests/test_guard.py

Comment thread guard/hook.sh Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
guard/hook.sh (1)

74-78: ⚠️ Potential issue | 🟠 Major

Stale-state predicate is too narrow for end_allowed_epoch-expired state.

This guard only treats state as stale when NOW passes MAX_EPOCH + min_break. If END_EPOCH is already stale but MAX_EPOCH is still in the future, the script can still hit outside-hours/blocked checks and block a new unmanaged session.

Please either include END_EPOCH freshness in stale detection or enforce/document an invariant that MAX_EPOCH <= END_EPOCH.

Proposed adjustment (conceptual)
-if [ "$NOW" -ge "$MAX_EPOCH" ] && [ "$START_EPOCH" -gt 0 ] 2>/dev/null; then
+if [ "$START_EPOCH" -gt 0 ] 2>/dev/null; then
   MIN_BREAK_STALE=$(jq -r '.min_break_seconds // 900' "$STATE" 2>/dev/null || echo 900)
-  STALE_LIMIT=$(( MAX_EPOCH + MIN_BREAK_STALE ))
-  [ "$NOW" -ge "$STALE_LIMIT" ] && exit 0
+  STALE_LIMIT_MAX=$(( MAX_EPOCH + MIN_BREAK_STALE ))
+  STALE_BY_MAX=0
+  STALE_BY_END=0
+
+  [ "$NOW" -ge "$STALE_LIMIT_MAX" ] && STALE_BY_MAX=1
+  if [ "$END_EPOCH" -gt 0 ] 2>/dev/null; then
+    STALE_LIMIT_END=$(( END_EPOCH + MIN_BREAK_STALE ))
+    [ "$NOW" -ge "$STALE_LIMIT_END" ] && STALE_BY_END=1
+  fi
+
+  { [ "$STALE_BY_MAX" -eq 1 ] || [ "$STALE_BY_END" -eq 1 ]; } && exit 0
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@guard/hook.sh` around lines 74 - 78, The stale-state check currently only
considers MAX_EPOCH + min_break (variables MAX_EPOCH, MIN_BREAK_STALE,
STALE_LIMIT) and ignores END_EPOCH, so states whose END_EPOCH has already passed
but MAX_EPOCH is still in the future remain treated as active; update the
predicate in the block containing NOW, MAX_EPOCH, START_EPOCH and STATE to also
read END_EPOCH from STATE (via jq) and treat the state as stale if NOW >=
END_EPOCH OR if NOW >= MAX_EPOCH + MIN_BREAK_STALE (i.e., include END_EPOCH
freshness in the stale detection), or alternatively enforce/document an
invariant that MAX_EPOCH <= END_EPOCH and validate that invariant early and fail
fast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@guard/hook.sh`:
- Around line 74-78: The stale-state check currently only considers MAX_EPOCH +
min_break (variables MAX_EPOCH, MIN_BREAK_STALE, STALE_LIMIT) and ignores
END_EPOCH, so states whose END_EPOCH has already passed but MAX_EPOCH is still
in the future remain treated as active; update the predicate in the block
containing NOW, MAX_EPOCH, START_EPOCH and STATE to also read END_EPOCH from
STATE (via jq) and treat the state as stale if NOW >= END_EPOCH OR if NOW >=
MAX_EPOCH + MIN_BREAK_STALE (i.e., include END_EPOCH freshness in the stale
detection), or alternatively enforce/document an invariant that MAX_EPOCH <=
END_EPOCH and validate that invariant early and fail fast.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6863261f-b9e3-4f83-9625-9bf71dda42f6

📥 Commits

Reviewing files that changed from the base of the PR and between 1ead71a and a94f1b7.

📒 Files selected for processing (2)
  • guard/hook.sh
  • tests/test_guard.py

@teseo

teseo commented Mar 16, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 16, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Two bugs caused "Llevas 2 horas y media" to spam every tool use when
Claude was launched outside the wrapper (IDE, command claude):

1. _notify_once had no dedup when HUMAN_GUARD_SESSION_ID was unset —
   now falls back to session_id from session-state.json.

2. No staleness check on session-state.json — old epochs from a
   previous session triggered session_limit immediately. Now skips
   informational notifications when past max_epoch + min_break.
@teseo
teseo force-pushed the fix/stale-session-state-and-sid-fallback branch from a94f1b7 to a995523 Compare March 16, 2026 22:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_guard.py (1)

2422-2489: Consider extracting shared hook test utilities to a module-level fixture/helper.

hook_env and _make_state here substantially overlap with the earlier hook integration section, increasing maintenance overhead when hook contract changes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_guard.py` around lines 2422 - 2489, The hook test utilities (the
pytest fixture hook_env and helper function _make_state) are duplicated and
should be extracted to a shared module-level helper so all hook tests reuse a
single implementation; refactor by moving the hook_env fixture and _make_state
function into a common test helper (e.g., a top-level conftest/shared test
utilities module), import or reference them from the current test file, and
update any tests to call the shared hook_env fixture and _make_state helper
instead of their local copies to ensure a single source of truth for hook
behavior and state construction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_guard.py`:
- Around line 2612-2631: Add an assertion to ensure the
test_stale_blocked_period_does_not_block path also enforces the silent output
contract: after calling hook_env["run_hook"](state, sid="active-sid") and
asserting rc == 0, assert that stdout (or stdout.strip()) is empty (no
notification text), matching the same silent-output checks used in the other
tests around Line 2506/2610; reference the stdout and rc variables and the
run_hook call to locate where to insert this assertion.
- Around line 2550-2591: The dedup tests call hook_env["run_hook"](state,
sid=...) but ignore the return code (they unpack as stdout, _), which lets
non-zero exits pass; update both
test_no_sid_notification_fires_once_not_every_call and
test_with_sid_env_still_works to capture the full return (e.g., stdout1, rc1 =
hook_env["run_hook"](…)) and add assertions that rc1 == 0 (and similarly for the
second call rc2 == 0) before asserting on stdout contents; reference the
run_hook calls and stdout1/stdout2 variables in the two tests when adding these
rc assertions.

---

Nitpick comments:
In `@tests/test_guard.py`:
- Around line 2422-2489: The hook test utilities (the pytest fixture hook_env
and helper function _make_state) are duplicated and should be extracted to a
shared module-level helper so all hook tests reuse a single implementation;
refactor by moving the hook_env fixture and _make_state function into a common
test helper (e.g., a top-level conftest/shared test utilities module), import or
reference them from the current test file, and update any tests to call the
shared hook_env fixture and _make_state helper instead of their local copies to
ensure a single source of truth for hook behavior and state construction.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 728403a0-5de7-4e52-a786-7d2885da8a0f

📥 Commits

Reviewing files that changed from the base of the PR and between a94f1b7 and a995523.

📒 Files selected for processing (2)
  • guard/hook.sh
  • tests/test_guard.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • guard/hook.sh

Comment thread tests/test_guard.py
Comment thread tests/test_guard.py
@teseo
teseo merged commit 5501713 into main Mar 17, 2026
10 checks passed
@teseo
teseo deleted the fix/stale-session-state-and-sid-fallback branch April 10, 2026 00:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant