Skip to content

feat: engagement gap to filter autonomous agent tool calls - #10

Merged
teseo merged 6 commits into
mainfrom
feat/engagement-gap
Mar 11, 2026
Merged

feat: engagement gap to filter autonomous agent tool calls#10
teseo merged 6 commits into
mainfrom
feat/engagement-gap

Conversation

@teseo

@teseo teseo commented Mar 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #9. Adds min_activity_gap_seconds config option to distinguish human engagement from autonomous agent tool calls in work time tracking.

  • Engagement gap filter: rapid tool calls (gap < threshold) don't accumulate work_since_break. Only gaps at or above the threshold count as human engagement. Default 0 preserves current behavior.
  • Activity sentinel scoped to engagement: .activity file only updates on human engagement or break detection, not during autonomous sequences. This lets check_break give proper break credit when the agent runs while the human is away.
  • Validation & parity: min_activity_gap_seconds is coerced to int (via float() in Python for parity with parseInt in Node), clamped to [0, min_break_seconds), and negatives are normalized to 0.
  • Docs: new README sections for async-heavy users and productivity-focused blocked_periods.

Config example

sessions:
  max_continuous_minutes: 150
  min_break_minutes: 15
  min_activity_gap_seconds: 60   # ignore gaps under 60s (agent autonomy)

Test plan

  • 125 Python tests passing (17 new for engagement gap)
  • 112 JS tests passing (5 new for engagement gap)
  • Hook integration tests via subprocess (sentinel file verification)
  • Fresh-session, single-call, all-autonomous, and boundary cases covered
  • 4 rounds of adversarial review with Codex — each round found and fixed real bugs

Summary by CodeRabbit

  • New Features

    • Added min_activity_gap_seconds to session state/config to filter autonomous activity and refine session activity tracking.
  • Documentation

    • Added "Async-heavy users" section with YAML examples, defaults, and clamping rules.
    • Expanded "Blocked periods" guidance with examples and effects on session start.
  • Tests

    • Extensive tests for engagement-gap behavior, parsing/coercion, clamping, sentinel handling, and edge cases.

teseo added 4 commits March 11, 2026 15:21
Add min_activity_gap_seconds config option. When set, rapid tool calls
(gap < threshold) don't accumulate as human engagement in work_since_break.
Defaults to 0 (current behavior preserved).
Without it, end_session falls back to wall-clock duration,
defeating the engagement gap feature entirely.
- Create wsb sentinel on first tool call so end_session never
  falls back to wall-clock for engagement-gap sessions
- Clamp min_activity_gap_seconds below min_break_seconds
- Fix README wording (at or above, not above)
- Hook only updates .activity on human engagement or break detection,
  not during autonomous sequences. This lets check_break give proper
  break credit when the agent runs while the human is away.
- Coerce min_activity_gap_seconds via float() in Python (parity with
  parseInt in Node for values like "60.9").
- Clamp negative values to 0 in both runtimes.
@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e7b3d18-701a-4374-a622-5f1f59656290

📥 Commits

Reviewing files that changed from the base of the PR and between 4f57bf1 and 6271c3a.

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

📝 Walkthrough

Walkthrough

Adds configurable min_activity_gap_seconds: parsed and clamped in session state, exposed by computeSessionState, used by hook logic to treat short gaps as autonomous activity (preserve human timestamp); docs updated and extensive tests added.

Changes

Cohort / File(s) Summary
Documentation
README.md
Adds "Async-heavy users" and expanded "Blocked periods beyond health" sections with YAML examples and explanation of default/clamping behavior.
Core State Computation
guard/core.mjs, guard/core.py
Read, normalize/coerce and clamp min_activity_gap_seconds to [0, min_break_seconds-1]; include min_activity_gap_seconds in returned session state.
Activity Hook Logic
guard/hook.sh
Read min_activity_gap_seconds from state; treat gaps below threshold as autonomous agent activity (preserve last human activity timestamp), handle intra-session break resets, initialize/update sentinel files conditionally, and handle non-numeric sentinels.
Test Coverage
tests/test_guard.mjs, tests/test_guard.py
Add tests verifying config parsing, defaults, coercion (floats/strings/NaN/Inf), clamping relative to min_break_seconds, sentinel creation/handling, gap-edge behaviors, and parity between core state and hook behavior.

Sequence Diagram(s)

sequenceDiagram
    participant Config as Configuration
    participant State as computeSessionState
    participant Hook as hook.sh
    participant Sentinels as Sentinel Files (.activity, .work-since-break)

    Config->>State: supply min_activity_gap_seconds (raw)
    State->>State: normalize/coerce & clamp to [0, min_break_seconds-1]
    State->>Hook: emit session state (includes min_activity_gap_seconds)

    Hook->>Sentinels: read last activity timestamp, compute GAP
    alt GAP < min_activity_gap_seconds
        Note over Hook: treat as autonomous activity
        Hook->>Sentinels: ensure .activity exists (do not update timestamp)
    else GAP >= min_break_seconds
        Note over Hook: intra-session break detected
        Hook->>Sentinels: reset work-since-break, update .activity to NOW
    else Normal engagement
        Note over Hook: human engagement
        Hook->>Sentinels: add GAP to work-since-break, update .activity to NOW
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰
I nibble gaps while circuits hum,
Short hops stay theirs, the long ones come.
I guard the clock, I mind the crate,
Reset the count when breaks are great.
A little hop — the session’s neat.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: engagement gap to filter autonomous agent tool calls' accurately and specifically describes the main change: implementing a configuration feature to distinguish autonomous agent activity from human engagement.
Linked Issues check ✅ Passed The PR successfully implements all primary coding objectives from issue #9: a configurable min_activity_gap_seconds threshold to distinguish human engagement from autonomous tool calls, activity sentinel management tied to engagement, proper validation/clamping logic, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the engagement gap feature: configuration handling, session state computation, hook logic for activity tracking, documentation updates, and test coverage. No extraneous modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 94.29% 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/engagement-gap

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

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

1654-1694: Add the non-integer coercion parity cases here too.

This block covers integers, negatives, and upper-bound clamping, but it skips the 60.9 / "60.9" / invalid-string cases that the Python suite now exercises. Since cross-runtime parity is part of the feature, mirroring those cases here would make a JS-only regression much harder to miss.

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

In `@tests/test_guard.mjs` around lines 1654 - 1694, Add parity tests in the same
"Engagement gap (min_activity_gap_seconds)" describe block to cover non-integer
coercion: add cases that pass min_activity_gap_seconds as 60.9 (float) and
"60.9" (string) and assert that computeSessionState(config, now,
'Europe/London') returns the coerced integer (60) for both, and add a case with
an invalid string (e.g., "not-a-number") and assert it falls back to the
default/clamped value (0). Use the same helpers and fixtures as the other tests
(parseYaml, SAMPLE_YAML, SAMPLE_CONFIG, fakeNow) and mirror the existing test
patterns so these new cases sit alongside the integer/negative/clamping tests.
🤖 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.py`:
- Around line 659-663: The try/except around the coercion of raw_gap to
min_activity_gap currently only catches TypeError and ValueError but not
OverflowError (e.g., when raw_gap is "inf"/"-inf"); update the except clause for
the int(float(raw_gap)) conversion to also catch OverflowError so non-finite
floats fall back to the existing default (min_activity_gap = 0), keeping the
surrounding logic (clamping to valid range) unchanged; look for the conversion
site using the names raw_gap and min_activity_gap in guard/core.py to modify the
exception tuple.

In `@tests/test_guard.py`:
- Around line 1951-1957: The test captures subprocess.run into the variable
result but never verifies success; update the invocation around hook_path in
tests/test_guard.py so the test fails fast on hook errors by either passing
check=True to subprocess.run or immediately asserting result.returncode == 0
(include result.stdout/result.stderr in the assertion message for diagnostics);
ensure the result variable is used so the test stops if the hook subprocess
exits non‑zero before inspecting sentinel files.
- Line 1884: The test function test_check_writes_min_activity_gap_to_state_file
has an unused fixture parameter config_file; remove config_file from the
function signature so the test reads def
test_check_writes_min_activity_gap_to_state_file(self, session_state_path,
session_log_path, tmp_path): to eliminate the unused argument and reduce the
line length that is failing CI.

---

Nitpick comments:
In `@tests/test_guard.mjs`:
- Around line 1654-1694: Add parity tests in the same "Engagement gap
(min_activity_gap_seconds)" describe block to cover non-integer coercion: add
cases that pass min_activity_gap_seconds as 60.9 (float) and "60.9" (string) and
assert that computeSessionState(config, now, 'Europe/London') returns the
coerced integer (60) for both, and add a case with an invalid string (e.g.,
"not-a-number") and assert it falls back to the default/clamped value (0). Use
the same helpers and fixtures as the other tests (parseYaml, SAMPLE_YAML,
SAMPLE_CONFIG, fakeNow) and mirror the existing test patterns so these new cases
sit alongside the integer/negative/clamping tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46b380b2-e16e-452b-a858-1a6926699e1a

📥 Commits

Reviewing files that changed from the base of the PR and between 4714c83 and 6e64779.

📒 Files selected for processing (6)
  • README.md
  • guard/core.mjs
  • guard/core.py
  • guard/hook.sh
  • tests/test_guard.mjs
  • tests/test_guard.py

Comment thread guard/core.py
Comment thread guard/hook.sh
Comment thread tests/test_guard.py Outdated
Comment thread tests/test_guard.py
teseo added 2 commits March 11, 2026 16:37
- Catch OverflowError for int(float("inf")) in min_activity_gap coercion
- Handle legacy ISO-format activity sentinels in hook.sh (from touch_session)
- Assert hook subprocess returncode in all integration tests
- Remove unused config_file fixture parameter

@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

♻️ Duplicate comments (1)
tests/test_guard.py (1)

1952-1959: ⚠️ Potential issue | 🟠 Major

Fail fast if hook.sh exits non-zero.

These tests pre-seed sentinel files before the subprocess runs, so a broken hook.sh can still leave the later assertions passing against stale data. Please assert success here, or use check=True, before reading any files back.

🧪 Suggested fix
             import subprocess
-            subprocess.run(
+            proc = subprocess.run(
                 ["bash", str(hook_path)],
                 input="{}",  # stdin consumed by hook
                 capture_output=True,
                 text=True,
                 env=env,
             )
+            assert proc.returncode == 0, proc.stderr or proc.stdout
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_guard.py` around lines 1952 - 1959, The subprocess invocation
running the hook in tests/test_guard.py should fail the test immediately if
hook.sh exits non‑zero; update the call that currently uses
subprocess.run([...], input="{}", capture_output=True, text=True, env=env) to
either pass check=True to subprocess.run or capture the CompletedProcess and
assert proc.returncode == 0 before any file reads; reference the invocation in
tests/test_guard.py where the hook_path subprocess is executed to make the
change so stale sentinel files cannot mask a failing hook.
🤖 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 2148-2175: The tests currently only assert that
min_activity_gap_seconds is less than min_break_seconds; change both assertions
to assert the exact clamp target: compute_session_state(...) should set
state["min_activity_gap_seconds"] == state["min_break_seconds"] - 1, so update
the asserts in test_threshold_clamped_below_min_break and
test_threshold_above_min_break_also_clamped to compare equality against
state["min_break_seconds"] - 1 (referencing the compute_session_state function
and the state dict keys "min_activity_gap_seconds" and "min_break_seconds").

---

Duplicate comments:
In `@tests/test_guard.py`:
- Around line 1952-1959: The subprocess invocation running the hook in
tests/test_guard.py should fail the test immediately if hook.sh exits non‑zero;
update the call that currently uses subprocess.run([...], input="{}",
capture_output=True, text=True, env=env) to either pass check=True to
subprocess.run or capture the CompletedProcess and assert proc.returncode == 0
before any file reads; reference the invocation in tests/test_guard.py where the
hook_path subprocess is executed to make the change so stale sentinel files
cannot mask a failing hook.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 87978282-94c8-4dc6-b516-2345a0baa306

📥 Commits

Reviewing files that changed from the base of the PR and between 6e64779 and 4f57bf1.

📒 Files selected for processing (1)
  • tests/test_guard.py

Comment thread tests/test_guard.py
@teseo
teseo merged commit 307b9e8 into main Mar 11, 2026
10 checks passed
@teseo
teseo deleted the feat/engagement-gap branch March 11, 2026 18:40
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.

Power user use case: multi-surface, async-heavy workflow patterns

1 participant