Skip to content

fix: cumulative break enforcement + last_activity tracking - #6

Merged
teseo merged 3 commits into
mainfrom
feat/cumulative-break-enforcement
Feb 24, 2026
Merged

fix: cumulative break enforcement + last_activity tracking#6
teseo merged 3 commits into
mainfrom
feat/cumulative-break-enforcement

Conversation

@teseo

@teseo teseo commented Feb 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Break enforcement now uses cumulative work tracking: only requires break after max_continuous_minutes (default 150min) of total work across sessions without sufficient breaks between them
  • Previous behavior enforced break after every session regardless of duration — this caused false break enforcement after only 67 minutes of work
  • Added touch_session sentinel file mechanism for accurate last_activity tracking (written by hook.sh on each tool use, read by end_session)
  • Aligned check_break/checkBreak signatures across Python and JS (parameter order: log_path, min_break_minutes, now, max_continuous_minutes)
  • Added _parse_naive() helper in Python for Z-suffix timestamp safety net

Test plan

  • Python: 89 tests pass (pytest)
  • JS: 96 tests pass (node --test)
  • No break when work below max_continuous (67min < 150min)
  • Break required when cumulative exceeds max (80+70min with 5min gap)
  • Break resets after sufficient gap (120min + 32min break + 63min)
  • last_activity used over end_time when available
  • Fallback to end_time without last_activity
  • touch_session writes sentinel file
  • end_session reads sentinel and cleans up
  • Z-suffix timestamps parsed correctly
  • Malformed last_activity falls back to end_time

Summary by CodeRabbit

  • New Features

    • Added a CLI option to explicitly record session activity timestamps (allows external tools to signal user interaction).
  • Improvements

    • Break detection now uses actual last-activity timestamps (not just session end times).
    • More accurate accumulation of continuous work across sessions and respect for configurable max continuous minutes.
  • Chores

    • Updated author metadata.

- Break only required after max_continuous_minutes (150min) of cumulative work
- Walk backward through sessions, accumulate work time, stop at real breaks
- Add touch_session sentinel file for accurate last_activity tracking
- hook.sh writes local time to sentinel on each tool use
- end_session reads sentinel into last_activity field
- Align check_break/checkBreak signatures (now as 3rd param in both)
- Add _parse_naive() helper for Z-suffix safety net
@coderabbitai

coderabbitai Bot commented Feb 24, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Sentinel-based session activity tracking added: hooks write per-session .activity.{id} files; core exposes touchSession and reads/deletes sentinels in endSession. Break logic now uses last_activity, accumulates work across sessions, and accepts max_continuous_minutes from config.

Changes

Cohort / File(s) Summary
Core Implementation
guard/core.mjs, guard/core.py
Added touchSession/touch_session to write per-session sentinel files. endSession/end_session now reads sentinel last_activity, deletes it, and falls back to end_time if absent. checkBreak/check_break accepts max_continuous_minutes and accumulates work across sessions using last_activity. CLI wired to --touch-session.
Hook Integration
guard/hook.sh
When SID is set, writes timestamp to $HOME/.claude/human-guard/.activity.$SID to signal activity; creates notification dir if needed.
Tests
tests/test_guard.mjs, tests/test_guard.py
Added/updated tests for sentinel creation, consumption, cleanup, last_activity parsing/fallbacks, cumulative-work and break scenarios, and CLI touch-session behavior.
Project Metadata
package.json, pyproject.toml
Author metadata updated from "The LLM Whisperer" to "Javier Mellado (The LLM Whisperer)".

Sequence Diagram

sequenceDiagram
    participant Hook as Hook Script (hook.sh)
    participant Sentinel as Sentinel File (.activity.{id})
    participant Core as Guard Core (core.mjs / core.py)
    participant Config as Config/State Files

    Hook->>Sentinel: touchSession() / write timestamp
    activate Sentinel

    Hook->>Core: endSession(sessionId)
    activate Core
    Core->>Sentinel: read .activity.{id}
    Sentinel-->>Core: return last_activity
    Core->>Sentinel: delete sentinel file
    deactivate Sentinel
    Core->>Core: set s.last_activity or fallback to end_time
    deactivate Core

    Core->>Config: read historical sessions
    activate Core
    loop iterate sessions
        Core->>Core: use last_activity for elapsed calc
        Core->>Core: accumulate work until gap >= min_break_minutes
    end
    Core->>Core: compare cumulative work vs max_continuous_minutes
    deactivate Core
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I nudged a dot upon the ground,

it marks each hop and every sound.
When sessions end, I quietly say,
"Here's the last hop" — then hop away.
Rest now, human, nap is found.

🚥 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 accurately summarizes the main changes: implementing cumulative break enforcement (the primary fix) and adding last_activity tracking via sentinel files (the supporting mechanism).
Docstring Coverage ✅ Passed Docstring coverage is 80.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 docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/cumulative-break-enforcement

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: 4

🤖 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/core.mjs`:
- Around line 482-495: The loop currently skips trivial sessions with "if
(durationMin < minBreakMinutes) continue;" but doesn't update prevSessionStart,
leaving a stale prevSessionStart that can create a false break boundary; change
the logic so that before continuing you update prevSessionStart to the current
session's start (e.g., compute sessionStart = new Date(s.start) and, if valid,
set prevSessionStart = sessionStart) so subsequent gapMin calculations use the
latest session boundary (ensure you still validate the date the same way you do
for sessionInteraction).

In `@guard/core.py`:
- Around line 405-408: Replace the bare try/except/pass around reading
activity_file with contextlib.suppress(Exception): wrap the
activity_file.read_text().strip() assignment (the block that sets
s["last_activity"]) in a contextlib.suppress(Exception) context manager so
exceptions are suppressed explicitly; add an import for contextlib if not
already present and ensure the suppressed block only contains the read/assign
operation referencing activity_file and s["last_activity"].
- Around line 508-525: The current early "continue" on short sessions (when
duration_min < min_break_minutes) skips computing session_interaction, causing
prev_session_start vs session_interaction gap checks to see an inflated gap and
falsely stop accumulation; to fix, compute session_interaction (using
s.get("last_activity") and _parse_naive with the same fallback to end) before
deciding to skip adding the session's duration, then if duration_min is short,
skip adding its duration but still use its session_interaction for the gap
calculation (i.e., move the duration_min check after the session_interaction
logic or compute session_interaction prior to the continue), keeping references
to s, duration_min, min_break_minutes, session_interaction, prev_session_start
and _parse_naive unchanged.

In `@guard/hook.sh`:
- Around line 31-34: The sentinel write may fail if the NOTIFY_DIR doesn't
exist; before writing to "$NOTIFY_DIR/.activity.$SID" (the file created when SID
is set), ensure the directory exists by creating it (e.g., mkdir -p
"$NOTIFY_DIR") and handle potential errors/permissions, then perform the date
write to ".activity.$SID"; update the block that checks SID in hook.sh to create
the directory if missing prior to redirecting the date output.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e58c57 and 6a3b64c.

📒 Files selected for processing (7)
  • guard/core.mjs
  • guard/core.py
  • guard/hook.sh
  • package.json
  • pyproject.toml
  • tests/test_guard.mjs
  • tests/test_guard.py

Comment thread guard/core.mjs Outdated
Comment thread guard/core.py Outdated
Comment thread guard/core.py Outdated
Comment thread guard/hook.sh
…entinel dir

- Move short-session skip after gap detection so trivial sessions still
  interrupt gap chaining and update prev_session_start (Py + JS)
- Add mkdir -p for sentinel dir in hook.sh before writing activity file

@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 (3)
guard/core.py (2)

464-545: Cumulative break logic is sound and the trivial-session gap-chaining fix is correctly applied.

The prior issue — where an early continue on short sessions left prev_session_start stale and could create a false break boundary — is resolved: session_interaction is now computed before the gap check and before the trivial-skip branch, and prev_session_start is updated before the continue on Line 524. The accumulation, last_interaction capture, and final elapsed comparison are all correct.

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

In `@guard/core.py` around lines 464 - 545, check_break's previous bug left
prev_session_start stale causing false break detection; ensure in check_break
you always compute session_interaction (parse s["last_activity"] or fallback to
end) before any gap check, and when skipping trivial sessions (duration_min <
min_break_minutes) update prev_session_start = start before continuing; also
ensure last_interaction is set the first time you add to cumulative_work so
elapsed uses the correct timestamp.

395-413: LGTM — sentinel read and fallback are correct.

The prior SIM105 violation is resolved. The two-step pattern (suppress read, suppress unlink, then fall back to end_time) correctly handles all failure paths: file missing, read error, and delete error.

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

In `@guard/core.py` around lines 395 - 413, The review confirms no changes are
needed: the end_session function correctly reads the sentinel activity file
(activity_file = GUARD_DIR / f".activity.{session_id}") with exceptions
suppressed and falls back to setting s["last_activity"] = s["end_time"], and it
safely unlinks the file; leave end_session, the contextlib.suppress read/unlink
pattern, and the subsequent fallback as-is.
guard/core.mjs (1)

448-518: checkBreak correctly mirrors the Python cumulative logic with the trivial-session fix applied.

sessionInteraction is computed before the gap check and before the trivial-skip branch; prevSessionStart is updated on Line 497 before the continue, so gap chaining is not broken by short sessions. lastInteraction is captured only from the first (most recent) non-trivial session. Fully consistent with core.py.

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

In `@guard/core.mjs` around lines 448 - 518, The checkBreak implementation is
correct and needs no functional changes: sessionInteraction is set before the
gap check, trivial sessions update prevSessionStart before the continue so they
don't break gap chaining, and lastInteraction is only set from the first
non-trivial session; leave the checkBreak function (and variables
sessionInteraction, prevSessionStart, lastInteraction, cumulativeWork) as-is.
🧹 Nitpick comments (1)
guard/core.py (1)

416-424: log_path parameter is accepted but never used.

The sentinel path is always derived from the global GUARD_DIR, not from log_path. The parameter exists purely for API symmetry with start_session/end_session, which is understandable, but callers reading the signature may reasonably expect the log path to influence where the sentinel is written.

Consider either removing the parameter (breaking change if callers already pass it) or documenting that it is intentionally ignored.

♻️ Proposed nit
-def touch_session(log_path, session_id):
-    """Write last_activity sentinel file for an active session.
-
-    Uses a separate file to avoid racing with session-log.json writes.
-    The sentinel is read by end_session when the session closes.
-    """
+def touch_session(log_path, session_id):  # log_path unused; sentinel always in GUARD_DIR
+    """Write last_activity sentinel file for an active session.
+
+    The sentinel is stored in GUARD_DIR (not derived from log_path) to avoid
+    racing with session-log.json writes. log_path is accepted for API symmetry.
+    """
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@guard/core.py` around lines 416 - 424, The touch_session function accepts a
log_path parameter but never uses it—sentinel file path is built from GUARD_DIR;
either remove the unused log_path parameter from touch_session's signature (and
update all callers) or make the function use the provided log_path to derive the
activity file instead of GUARD_DIR (e.g., use log_path /
f".activity.{session_id}"), and update the docstring to state the chosen
behavior; locate the symbol touch_session and the global GUARD_DIR in the diff
to apply the change consistently.
🤖 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/core.mjs`:
- Around line 448-518: The checkBreak implementation is correct and needs no
functional changes: sessionInteraction is set before the gap check, trivial
sessions update prevSessionStart before the continue so they don't break gap
chaining, and lastInteraction is only set from the first non-trivial session;
leave the checkBreak function (and variables sessionInteraction,
prevSessionStart, lastInteraction, cumulativeWork) as-is.

In `@guard/core.py`:
- Around line 464-545: check_break's previous bug left prev_session_start stale
causing false break detection; ensure in check_break you always compute
session_interaction (parse s["last_activity"] or fallback to end) before any gap
check, and when skipping trivial sessions (duration_min < min_break_minutes)
update prev_session_start = start before continuing; also ensure
last_interaction is set the first time you add to cumulative_work so elapsed
uses the correct timestamp.
- Around line 395-413: The review confirms no changes are needed: the
end_session function correctly reads the sentinel activity file (activity_file =
GUARD_DIR / f".activity.{session_id}") with exceptions suppressed and falls back
to setting s["last_activity"] = s["end_time"], and it safely unlinks the file;
leave end_session, the contextlib.suppress read/unlink pattern, and the
subsequent fallback as-is.

---

Nitpick comments:
In `@guard/core.py`:
- Around line 416-424: The touch_session function accepts a log_path parameter
but never uses it—sentinel file path is built from GUARD_DIR; either remove the
unused log_path parameter from touch_session's signature (and update all
callers) or make the function use the provided log_path to derive the activity
file instead of GUARD_DIR (e.g., use log_path / f".activity.{session_id}"), and
update the docstring to state the chosen behavior; locate the symbol
touch_session and the global GUARD_DIR in the diff to apply the change
consistently.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a3b64c and dd280d4.

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

@teseo
teseo merged commit a32341a into main Feb 24, 2026
10 checks passed
@teseo
teseo deleted the feat/cumulative-break-enforcement branch February 24, 2026 23:47
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