Skip to content

feat: active hours + idle detection gates for session-guardian#413

Merged
affaan-m merged 3 commits into
affaan-m:mainfrom
ispaydeu:feat/active-hours-idle-guard
Mar 13, 2026
Merged

feat: active hours + idle detection gates for session-guardian#413
affaan-m merged 3 commits into
affaan-m:mainfrom
ispaydeu:feat/active-hours-idle-guard

Conversation

@ispaydeu

@ispaydeu ispaydeu commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Extends session-guardian.sh (introduced in #412) with two additional guard gates that prevent observer sessions from spawning when no human is present:

  • Gate 1 — Active Hours: blocks observer cycles outside a configurable time window (default 8 AM – 11 PM local time). Uses date +%k%M with base-10 arithmetic prefix to avoid octal crash on midnight minutes (00:08, 00:09, etc.). Works toollessly on BSD date (macOS) and GNU date (Linux). Set both vars to 0 to disable.
  • Gate 3 — Idle Detection: skips cycles when the user has been idle beyond a configurable threshold (default 30 min). Detects system idle time via OS-native APIs — no additional tools required on macOS or Windows:
    • macOS: /usr/sbin/ioreg + awk (built-in)
    • Linux: xprintidle if installed, else fail open
    • Windows (Git Bash/MSYS2): PowerShell GetLastInputInfo via Add-Type (Win32 API, pre-installed)
    • Headless/unknown: always returns 0 (fail open)
  • Gate 2 (per-project cooldown log, from feat: add project cooldown log to prevent observer re-spawn loops #412) is unchanged and still runs between Gates 1 and 3.

Execution order is cheapest-first: time window (~0 ms) → cooldown log (~1 ms) → idle detection (~5–50 ms).

Motivation: Observer sessions that spawn outside working hours (overnight, while the user is away) consume licensed usage with no benefit. This PR gives users a simple, zero-dependency way to ensure observers only run while they are actively present.

Note: This PR builds on #412. It can be reviewed independently but should be merged after #412 since it references session-guardian.sh.

Configuration

All settings are optional env vars with sensible defaults:

OBSERVER_ACTIVE_HOURS_START=800   # 8:00 AM local (set to 0 to disable)
OBSERVER_ACTIVE_HOURS_END=2300    # 11:00 PM local (set to 0 to disable)
OBSERVER_MAX_IDLE_SECONDS=1800    # 30 min idle threshold (set to 0 to disable)

Correctness fixes included

  • 10# base-10 prefix on all HHMM arithmetic to prevent octal crash on midnight minutes containing digits 8 or 9
  • PowerShell output stripped of \r via tr -d '\r' (Windows CRLF); uses [long] cast to prevent 32-bit TickCount overflow after 24.9 days uptime
  • mktemp uses log file's own directory (not $TMPDIR) so mv is always same-filesystem on Linux
  • mkdir -p failure exits 0 (fail open) rather than propagating a non-zero exit under set -e
  • Numeric validation on last_spawn guards against corrupt log entries causing arithmetic errors

Test plan

  • OBSERVER_ACTIVE_HOURS_START=0 OBSERVER_ACTIVE_HOURS_END=0 OBSERVER_MAX_IDLE_SECONDS=0 bash session-guardian.sh → exit 0 (all gates disabled)
  • OBSERVER_ACTIVE_HOURS_START=0 OBSERVER_ACTIVE_HOURS_END=1 OBSERVER_MAX_IDLE_SECONDS=0 bash session-guardian.sh → exit 1 with "outside active hours" on stderr
  • OBSERVER_MAX_IDLE_SECONDS=1 bash session-guardian.sh → exit 0 on macOS with ioreg idle < 1 s
  • No absolute paths containing usernames appear on stderr in any gate
  • bash -c 'x="008"; echo $(( 10#$x < 800 ))' → outputs 1 (octal fix)

Summary by cubic

Adds active-hours and idle-detection gates to session-guardian.sh and wires it into observer-loop.sh to skip observer cycles when no one is around. This cuts wasted usage by stopping spawns outside set hours, during cooldown, or while the user is idle.

  • New Features

    • Gate 1 — Active Hours: blocks cycles outside a local time window (default 08:00–23:00). Uses HHMM with base-10 math. Disable with OBSERVER_ACTIVE_HOURS_START=0 and OBSERVER_ACTIVE_HOURS_END=0.
    • Gate 3 — Idle Detection: skips when idle > threshold (default 30m). macOS via ioreg, Linux via xprintidle if available (else fail open), Windows via PowerShell GetLastInputInfo. Disable with OBSERVER_MAX_IDLE_SECONDS=0.
    • Integrated guard call in observer-loop.sh. Gate order: time window → per-project cooldown (unchanged, default OBSERVER_INTERVAL_SECONDS=300) → idle. Config vars: OBSERVER_INTERVAL_SECONDS, OBSERVER_LAST_RUN_LOG, OBSERVER_ACTIVE_HOURS_START/END, OBSERVER_MAX_IDLE_SECONDS.
  • Bug Fixes

    • Gate hardening: base-10 prefix for HHMM arithmetic; Windows CRLF stripped and [long] cast; same-filesystem mv for log updates; fail-open if mkdir -p cannot create the log dir; numeric validation of log entries.
    • Concurrency and privacy: mkdir lock now skips on contention to avoid double spawns; stderr avoids absolute paths (only basenames).

Written for commit a46e644. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Observer loop now runs a session-guardian gate before analysis: configurable active time windows (including overnight), per-project cooldown to avoid duplicate observations, and idle-time detection to skip cycles when the user is inactive. The gate can be configured or disabled and will skip an analysis cycle when conditions dictate.

Adds session-guardian.sh, called by observer-loop.sh before each Haiku
spawn. It reads ~/.claude/observer-last-run.log and blocks the cycle if
the same project was observed within OBSERVER_INTERVAL_SECONDS (default
300s).

Prevents self-referential loops where a spawned session triggers
observe.sh, which signals the observer before the cooldown has elapsed.

Uses a mkdir-based lock for safe concurrent access across multiple
simultaneously-observed projects. Log entries use tab-delimited format
to handle paths containing spaces. Fails open on lock contention.

Config:
  OBSERVER_INTERVAL_SECONDS   default: 300
  OBSERVER_LAST_RUN_LOG       default: ~/.claude/observer-last-run.log

No external dependencies. Works on macOS, Linux, Windows (Git Bash/MSYS2).
Adds Gate 1 (active hours check) and Gate 3 (system idle detection) to
session-guardian.sh, building on the per-project cooldown log from PR 1.

Gate 1 — Time Window:
- OBSERVER_ACTIVE_HOURS_START/END (default 800–2300 local time)
- Uses date +%k%M with 10# prefix to avoid octal crash at midnight
- Toolless on all platforms; set both vars to 0 to disable

Gate 3 — Idle Detection:
- macOS: ioreg + awk (built-in, no deps)
- Linux: xprintidle if available, else fail open
- Windows (Git Bash/MSYS2): PowerShell GetLastInputInfo via Add-Type
- Unknown/headless: always returns 0 (fail open)
- OBSERVER_MAX_IDLE_SECONDS=0 disables gate

Fixes in this commit:
- 10# base-10 prefix prevents octal arithmetic crash on midnight minutes
  containing digits 8 or 9 (e.g. 00:08 = "008" is invalid octal)
- PowerShell output piped through tr -d '\r' to strip Windows CRLF;
  also uses [long] cast to avoid TickCount 32-bit overflow after 24 days
- mktemp now uses log file directory instead of TMPDIR to ensure
  same-filesystem mv on Linux (atomic rename instead of copy+unlink)
- mkdir -p failure exits 0 (fail open) rather than crashing under set -e
- Numeric validation on last_spawn prevents arithmetic error on corrupt log

Gate execution order: 1 (time, ~0ms) → 2 (cooldown, ~1ms) → 3 (idle, ~50ms)
Copilot AI review requested due to automatic review settings March 12, 2026 15:41
@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2f00c311-14d2-4205-be25-099633dc0ae6

📥 Commits

Reviewing files that changed from the base of the PR and between 1b9cbe2 and a46e644.

📒 Files selected for processing (2)
  • skills/continuous-learning-v2/agents/observer-loop.sh
  • skills/continuous-learning-v2/agents/session-guardian.sh

📝 Walkthrough

Walkthrough

Adds a session-guardian gate to the observer loop: the loop invokes a new session-guardian.sh which runs time-window, project-cooldown, and idle-detection checks and returns non-zero to skip the observer cycle when any gate blocks.

Changes

Cohort / File(s) Summary
Observer Loop Integration
skills/continuous-learning-v2/agents/observer-loop.sh
Invokes session-guardian.sh before constructing the prompt / calling Claude; if the guardian exits non-zero, logs and returns, skipping the observation cycle.
Session Guardian Script
skills/continuous-learning-v2/agents/session-guardian.sh
New script implementing three gates: active-hours window (HHMM, supports overnight), per-project cooldown using a lock dir and atomic log updates, and idle detection (Darwin, Linux, Windows PowerShell fallback). Exits 0 to proceed or 1 to skip; configurable env vars and robust lock/cleanup handling.

Sequence Diagram(s)

sequenceDiagram
    participant Observer as Observer Loop
    participant Guardian as session-guardian.sh
    participant FS as Filesystem (log + lock)
    participant OS as Host OS (idle check)
    participant Claude as Claude API

    Observer->>Guardian: run session-guardian.sh
    Guardian->>Guardian: check active hours
    Guardian->>FS: try acquire project lock / read last-run log
    alt cooldown active
        FS-->>Guardian: cooldown -> deny
        Guardian-->>Observer: exit non-zero (skip)
    else cooldown ok
        FS-->>Guardian: lock acquired / update log
        Guardian->>OS: measure idle time
        alt idle below threshold
            Guardian-->>Observer: exit 0 (proceed)
        else idle too high
            Guardian-->>Observer: exit non-zero (skip)
        end
    end
    alt proceed
        Observer->>Claude: construct prompt & invoke
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I peered at the loop and hopped in to say,
Three tiny gates deciding whether to play.
Time, cooldown, and idle — each lends a paw,
Whispering "proceed" or "not now" with awe.
A soft hop of logic — observer sleeps or stays.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly describes the main additions to the PR: two new guard gates (active hours and idle detection) for the session-guardian component.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds additional gating logic to the Continuous Learning v2 observer so that analysis sessions are only spawned during configured active hours and when the user is not idle, reducing wasted/undesired observer runs. This extends the existing session-guardian.sh hook invoked by observer-loop.sh before starting Claude analysis.

Changes:

  • Introduces an “active hours” gate (HHMM window) to skip observer cycles outside working hours.
  • Adds cross-platform idle-time detection (macOS via ioreg, Linux via xprintidle if present, Windows via PowerShell P/Invoke) to skip cycles when the user has been idle too long.
  • Hooks session-guardian.sh into the observer loop so cycles can be skipped before spawning Claude.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
skills/continuous-learning-v2/agents/session-guardian.sh Implements active-hours + per-project cooldown + idle-detection gates before allowing observer sessions to proceed.
skills/continuous-learning-v2/agents/observer-loop.sh Calls session-guardian.sh before running claude to allow early skip of observer cycles.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

ACTIVE_START="${OBSERVER_ACTIVE_HOURS_START:-800}"
ACTIVE_END="${OBSERVER_ACTIVE_HOURS_END:-2300}"
MAX_IDLE="${OBSERVER_MAX_IDLE_SECONDS:-1800}"

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

These env vars are used in numeric test/arithmetic contexts later. If a user sets any of them to a non-integer value (or empty), [ will emit “integer expression expected”; with set -e, the script will exit non-zero, causing the observer cycle to be skipped (fail-closed) rather than proceeding (fail-open). Consider validating/sanitizing INTERVAL, ACTIVE_START, ACTIVE_END, and MAX_IDLE once here (e.g., [[ $var =~ ^[0-9]+$ ]] || var=<default>), or explicitly fail open on invalid values.

Suggested change
# Sanitize numeric env vars to avoid "integer expression expected" under set -e
if ! [[ "$INTERVAL" =~ ^[0-9]+$ ]]; then
INTERVAL=300
fi
if ! [[ "$ACTIVE_START" =~ ^[0-9]+$ ]]; then
ACTIVE_START=800
fi
if ! [[ "$ACTIVE_END" =~ ^[0-9]+$ ]]; then
ACTIVE_END=2300
fi
if ! [[ "$MAX_IDLE" =~ ^[0-9]+$ ]]; then
MAX_IDLE=1800
fi

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +33
if [ $(( 10#${current_hhmm:-0} )) -lt $(( 10#${ACTIVE_START:-800} )) ] || \
[ $(( 10#${current_hhmm:-0} )) -ge $(( 10#${ACTIVE_END:-2300} )) ]; then

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

The active-hours check assumes ACTIVE_START < ACTIVE_END (same-day window). If a user configures an overnight window (e.g., 2300–700), the current logic will treat all times as outside the window and permanently block observer cycles. Consider either supporting wrap-around windows (when start > end) or explicitly detecting that configuration and failing open / emitting a clear diagnostic.

Suggested change
if [ $(( 10#${current_hhmm:-0} )) -lt $(( 10#${ACTIVE_START:-800} )) ] || \
[ $(( 10#${current_hhmm:-0} )) -ge $(( 10#${ACTIVE_END:-2300} )) ]; then
current_val=$(( 10#${current_hhmm:-0} ))
start_val=$(( 10#${ACTIVE_START:-800} ))
end_val=$(( 10#${ACTIVE_END:-2300} ))
if [ "$start_val" -lt "$end_val" ]; then
# Same-day window (e.g., 0800–2300): outside if before start or after/end
if [ "$current_val" -lt "$start_val" ] || [ "$current_val" -ge "$end_val" ]; then
echo "session-guardian: outside active hours (${current_hhmm}, window ${ACTIVE_START}-${ACTIVE_END})" >&2
exit 1
fi
elif [ "$start_val" -gt "$end_val" ]; then
# Overnight window (e.g., 2300–0700): active if current >= start OR current < end
# So we are outside only if current < start AND current >= end
if [ "$current_val" -lt "$start_val" ] && [ "$current_val" -ge "$end_val" ]; then
echo "session-guardian: outside active hours (${current_hhmm}, window ${ACTIVE_START}-${ACTIVE_END})" >&2
exit 1
fi
else
# start_val == end_val: degenerate window, treat as always outside (matches prior behavior)

Copilot uses AI. Check for mistakes.
}

if [ "$MAX_IDLE" -gt 0 ]; then
idle_seconds=$(get_idle_seconds)

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

idle_seconds is used in a numeric -gt comparison, but get_idle_seconds can return a non-numeric string (e.g., unexpected PowerShell output, xprintidle returning something non-integer, or an empty line). With set -e, a non-integer here will abort the script and cause the observer cycle to be skipped unexpectedly. Consider normalizing/validating idle_seconds (digits-only) and defaulting to 0 (fail open) before the comparison.

Suggested change
idle_seconds=$(get_idle_seconds)
idle_seconds=$(get_idle_seconds)
case "$idle_seconds" in
''|*[!0-9]*)
# Non-numeric or empty idle time — fail open by treating as 0s
idle_seconds=0
;;
esac

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +112
Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool GetLastInputInfo(ref LASTINPUTINFO p); [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public int dwTime; }' -Name WinAPI -Namespace PInvoke; \
\$l = New-Object PInvoke.WinAPI+LASTINPUTINFO; \$l.cbSize = 8; \
[PInvoke.WinAPI]::GetLastInputInfo([ref]\$l) | Out-Null; \
[Math]::Max(0, [long]([Environment]::TickCount - [long]\$l.dwTime) / 1000) \

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

Windows idle detection uses [Environment]::TickCount (32-bit, wraps about every 24.9 days) and defines LASTINPUTINFO.dwTime as int. Casting to [long] does not prevent TickCount wrap-around; after long uptimes this can yield incorrect (often near-zero) idle times and defeat the idle gate. Prefer using TickCount64 when available and model dwTime as an unsigned 32-bit value (and compute the unsigned delta) to make the calculation robust.

Suggested change
Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool GetLastInputInfo(ref LASTINPUTINFO p); [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public int dwTime; }' -Name WinAPI -Namespace PInvoke; \
\$l = New-Object PInvoke.WinAPI+LASTINPUTINFO; \$l.cbSize = 8; \
[PInvoke.WinAPI]::GetLastInputInfo([ref]\$l) | Out-Null; \
[Math]::Max(0, [long]([Environment]::TickCount - [long]\$l.dwTime) / 1000) \
Add-Type -MemberDefinition '[DllImport(\"user32.dll\")] public static extern bool GetLastInputInfo(ref LASTINPUTINFO p); [StructLayout(LayoutKind.Sequential)] public struct LASTINPUTINFO { public uint cbSize; public uint dwTime; }' -Name WinAPI -Namespace PInvoke; \
\$l = New-Object PInvoke.WinAPI+LASTINPUTINFO; \$l.cbSize = 8; \
[PInvoke.WinAPI]::GetLastInputInfo([ref]\$l) | Out-Null; \
\$now = [uint][Environment]::TickCount; \
\$delta = \$now - \$l.dwTime; \
[Math]::Max(0, [long]\$delta / 1000) \

Copilot uses AI. Check for mistakes.
return
fi

# session-guardian: gate observer cycle (cooldown log — see session-guardian.sh)

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

This comment says the gate is only a “cooldown log”, but session-guardian.sh now enforces additional gates (active-hours + idle detection). Consider updating the comment to reflect the broader behavior so future readers don’t assume it’s only a cooldown check.

Suggested change
# session-guardian: gate observer cycle (cooldown log — see session-guardian.sh)
# session-guardian: gate observer cycle (cooldown, active-hours window, idle detection — see session-guardian.sh for full policy)

Copilot uses AI. Check for mistakes.
Comment on lines +61 to +63
last_spawn=0
last_spawn=$(grep -F "$project_root" "$LOG_PATH" 2>/dev/null | tail -n1 | awk '{print $NF}') || true
last_spawn="${last_spawn:-0}"

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

The cooldown log lookup uses grep -F "$project_root" which can match other projects whose paths merely contain the current project root as a substring, leading to an incorrect last_spawn and incorrect cooldown decisions. Consider parsing the tab-delimited log and matching the first field exactly (e.g., with awk -F '\t' comparing $1 to project_root). Also consider passing -- to grep/awk where relevant so paths starting with - can’t be misinterpreted as options.

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +77
tmp_log="$(mktemp "$(dirname "$LOG_PATH")/observer-last-run.XXXXXX")"
grep -vF "$project_root" "$LOG_PATH" > "$tmp_log" 2>/dev/null || true
printf '%s\t%s\n' "$project_root" "$now" >> "$tmp_log"

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

When rewriting the log, grep -vF "$project_root" will delete any line that contains the project root as a substring, which can unintentionally remove other projects’ entries. Prefer filtering by exact key (first tab-delimited column equals project_root) so only the intended project entry is removed/replaced.

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

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)
skills/continuous-learning-v2/agents/session-guardian.sh (1)

30-37: Overnight time windows not supported.

The current logic uses a simple range check (ACTIVE_START <= current < ACTIVE_END), which doesn't support wraparound overnight windows (e.g., 22:00–06:00). If a user configures ACTIVE_START=2200 and ACTIVE_END=600, the gate would block all hours.

This is likely acceptable for the default use case (daytime active hours), but consider documenting this limitation explicitly in the header comments.

📝 Suggested documentation addition
 #   OBSERVER_ACTIVE_HOURS_START  default: 800   (8:00 AM local, set to 0 to disable)
 #   OBSERVER_ACTIVE_HOURS_END    default: 2300  (11:00 PM local, set to 0 to disable)
+#   Note: Overnight windows (e.g., 2200-0600) are not supported; END must be > START.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/continuous-learning-v2/agents/session-guardian.sh` around lines 30 -
37, The time-range check in session-guardian.sh fails for overnight windows
(e.g., ACTIVE_START=2200, ACTIVE_END=600); update the conditional around
ACTIVE_START/ACTIVE_END to handle wraparound by detecting when ACTIVE_START >
ACTIVE_END and using an OR-range check (allow when current_hhmm >= ACTIVE_START
OR current_hhmm < ACTIVE_END) instead of the existing single contiguous-range
logic; reference the variables/current value names current_hhmm, ACTIVE_START,
ACTIVE_END and update the conditional that currently compares 10#${current_hhmm}
against 10#${ACTIVE_START} and 10#${ACTIVE_END} to include the wraparound
branch, or alternatively add a header comment documenting the limitation if you
choose not to implement wraparound.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@skills/continuous-learning-v2/agents/observer-loop.sh`:
- Around line 36-40: observer-loop.sh calls session-guardian.sh without ensuring
the correct project working directory, so session-guardian.sh's use of git
rev-parse --show-toplevel can resolve the wrong project; fix by either
exporting/passing PROJECT_DIR into session-guardian.sh (e.g., invoke
session-guardian.sh "$PROJECT_DIR" and update session-guardian.sh to prefer the
passed argument over git rev-parse) or ensure observer-loop.sh (and
start-observer.sh when launching nohup) cd "$PROJECT_DIR" before calling
session-guardian.sh; update the invocation in observer-loop.sh and
start-observer.sh and modify session-guardian.sh to accept and use a PROJECT_DIR
parameter (or environment variable) in place of running git rev-parse.

---

Nitpick comments:
In `@skills/continuous-learning-v2/agents/session-guardian.sh`:
- Around line 30-37: The time-range check in session-guardian.sh fails for
overnight windows (e.g., ACTIVE_START=2200, ACTIVE_END=600); update the
conditional around ACTIVE_START/ACTIVE_END to handle wraparound by detecting
when ACTIVE_START > ACTIVE_END and using an OR-range check (allow when
current_hhmm >= ACTIVE_START OR current_hhmm < ACTIVE_END) instead of the
existing single contiguous-range logic; reference the variables/current value
names current_hhmm, ACTIVE_START, ACTIVE_END and update the conditional that
currently compares 10#${current_hhmm} against 10#${ACTIVE_START} and
10#${ACTIVE_END} to include the wraparound branch, or alternatively add a header
comment documenting the limitation if you choose not to implement wraparound.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eb38b7d4-41b7-41c8-a629-df4d4cc75292

📥 Commits

Reviewing files that changed from the base of the PR and between 51eec12 and 1b9cbe2.

📒 Files selected for processing (2)
  • skills/continuous-learning-v2/agents/observer-loop.sh
  • skills/continuous-learning-v2/agents/session-guardian.sh

Comment thread skills/continuous-learning-v2/agents/observer-loop.sh Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

4 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="skills/continuous-learning-v2/agents/session-guardian.sh">

<violation number="1" location="skills/continuous-learning-v2/agents/session-guardian.sh:62">
P2: `grep -F` does substring matching, so a project root like `/home/user/app` will also match entries for `/home/user/app-v2`. Since the log is tab-delimited, anchor the match to include the tab separator to ensure exact key matching.

The same issue applies to the `grep -vF` on the write path (line 73).</violation>

<violation number="2" location="skills/continuous-learning-v2/agents/session-guardian.sh:112">
P1: PowerShell `/` performs floating-point division, so this expression can output decimals like `1800.123`, which then breaks the bash integer comparison `[ "$idle_seconds" -gt "$MAX_IDLE" ]` with "integer expression expected". Cast the final result to `[int]` to ensure integer output.</violation>
</file>

<file name="skills/continuous-learning-v2/agents/observer-loop.sh">

<violation number="1" location="skills/continuous-learning-v2/agents/observer-loop.sh:36">
P3: Comment is stale/misleading: session-guardian now enforces active hours, cooldown log, **and** idle detection — not just the cooldown log. Consider updating to reflect all three gates.</violation>

<violation number="2" location="skills/continuous-learning-v2/agents/observer-loop.sh:37">
P2: `session-guardian.sh` resolves the project root via `git rev-parse --show-toplevel`, which depends on `$PWD`. This invocation doesn't ensure the working directory is the target project before calling the guardian — if the observer process was launched from a different directory (e.g., `$HOME`), `git rev-parse` will resolve to the wrong repo (or fall back to `$PWD`), silently breaking per-project cooldown tracking. Either `cd "$PROJECT_DIR"` before this call or pass the project directory as an argument to `session-guardian.sh`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread skills/continuous-learning-v2/agents/session-guardian.sh Outdated
Comment thread skills/continuous-learning-v2/agents/session-guardian.sh Outdated
fi

# session-guardian: gate observer cycle (cooldown log — see session-guardian.sh)
if ! bash "$(dirname "$0")/session-guardian.sh"; then

@cubic-dev-ai cubic-dev-ai Bot Mar 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: session-guardian.sh resolves the project root via git rev-parse --show-toplevel, which depends on $PWD. This invocation doesn't ensure the working directory is the target project before calling the guardian — if the observer process was launched from a different directory (e.g., $HOME), git rev-parse will resolve to the wrong repo (or fall back to $PWD), silently breaking per-project cooldown tracking. Either cd "$PROJECT_DIR" before this call or pass the project directory as an argument to session-guardian.sh.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At skills/continuous-learning-v2/agents/observer-loop.sh, line 37:

<comment>`session-guardian.sh` resolves the project root via `git rev-parse --show-toplevel`, which depends on `$PWD`. This invocation doesn't ensure the working directory is the target project before calling the guardian — if the observer process was launched from a different directory (e.g., `$HOME`), `git rev-parse` will resolve to the wrong repo (or fall back to `$PWD`), silently breaking per-project cooldown tracking. Either `cd "$PROJECT_DIR"` before this call or pass the project directory as an argument to `session-guardian.sh`.</comment>

<file context>
@@ -33,6 +33,12 @@ analyze_observations() {
   fi
 
+  # session-guardian: gate observer cycle (cooldown log — see session-guardian.sh)
+  if ! bash "$(dirname "$0")/session-guardian.sh"; then
+    echo "[$(date)] Observer cycle skipped by session-guardian" >> "$LOG_FILE"
+    return
</file context>
Fix with Cubic

Comment thread skills/continuous-learning-v2/agents/observer-loop.sh Outdated
@affaan-m affaan-m merged commit a6f380f into affaan-m:main Mar 13, 2026
39 checks passed
floatingman pushed a commit to floatingman/everything-claude-code that referenced this pull request Mar 22, 2026
…n-m#413)

* feat: add project cooldown log to prevent rapid observer re-spawn

Adds session-guardian.sh, called by observer-loop.sh before each Haiku
spawn. It reads ~/.claude/observer-last-run.log and blocks the cycle if
the same project was observed within OBSERVER_INTERVAL_SECONDS (default
300s).

Prevents self-referential loops where a spawned session triggers
observe.sh, which signals the observer before the cooldown has elapsed.

Uses a mkdir-based lock for safe concurrent access across multiple
simultaneously-observed projects. Log entries use tab-delimited format
to handle paths containing spaces. Fails open on lock contention.

Config:
  OBSERVER_INTERVAL_SECONDS   default: 300
  OBSERVER_LAST_RUN_LOG       default: ~/.claude/observer-last-run.log

No external dependencies. Works on macOS, Linux, Windows (Git Bash/MSYS2).

* feat: extend session-guardian with time window and idle detection gates

Adds Gate 1 (active hours check) and Gate 3 (system idle detection) to
session-guardian.sh, building on the per-project cooldown log from PR 1.

Gate 1 — Time Window:
- OBSERVER_ACTIVE_HOURS_START/END (default 800–2300 local time)
- Uses date +%k%M with 10# prefix to avoid octal crash at midnight
- Toolless on all platforms; set both vars to 0 to disable

Gate 3 — Idle Detection:
- macOS: ioreg + awk (built-in, no deps)
- Linux: xprintidle if available, else fail open
- Windows (Git Bash/MSYS2): PowerShell GetLastInputInfo via Add-Type
- Unknown/headless: always returns 0 (fail open)
- OBSERVER_MAX_IDLE_SECONDS=0 disables gate

Fixes in this commit:
- 10# base-10 prefix prevents octal arithmetic crash on midnight minutes
  containing digits 8 or 9 (e.g. 00:08 = "008" is invalid octal)
- PowerShell output piped through tr -d '\r' to strip Windows CRLF;
  also uses [long] cast to avoid TickCount 32-bit overflow after 24 days
- mktemp now uses log file directory instead of TMPDIR to ensure
  same-filesystem mv on Linux (atomic rename instead of copy+unlink)
- mkdir -p failure exits 0 (fail open) rather than crashing under set -e
- Numeric validation on last_spawn prevents arithmetic error on corrupt log

Gate execution order: 1 (time, ~0ms) → 2 (cooldown, ~1ms) → 3 (idle, ~50ms)

* fix: harden session guardian gates

---------

Co-authored-by: Affaan Mustafa <affaan@dcube.ai>
FrancescoRosciano pushed a commit to FRosciano-Mambo/everything-claude-code that referenced this pull request Jun 1, 2026
…n-m#413)

* feat: add project cooldown log to prevent rapid observer re-spawn

Adds session-guardian.sh, called by observer-loop.sh before each Haiku
spawn. It reads ~/.claude/observer-last-run.log and blocks the cycle if
the same project was observed within OBSERVER_INTERVAL_SECONDS (default
300s).

Prevents self-referential loops where a spawned session triggers
observe.sh, which signals the observer before the cooldown has elapsed.

Uses a mkdir-based lock for safe concurrent access across multiple
simultaneously-observed projects. Log entries use tab-delimited format
to handle paths containing spaces. Fails open on lock contention.

Config:
  OBSERVER_INTERVAL_SECONDS   default: 300
  OBSERVER_LAST_RUN_LOG       default: ~/.claude/observer-last-run.log

No external dependencies. Works on macOS, Linux, Windows (Git Bash/MSYS2).

* feat: extend session-guardian with time window and idle detection gates

Adds Gate 1 (active hours check) and Gate 3 (system idle detection) to
session-guardian.sh, building on the per-project cooldown log from PR 1.

Gate 1 — Time Window:
- OBSERVER_ACTIVE_HOURS_START/END (default 800–2300 local time)
- Uses date +%k%M with 10# prefix to avoid octal crash at midnight
- Toolless on all platforms; set both vars to 0 to disable

Gate 3 — Idle Detection:
- macOS: ioreg + awk (built-in, no deps)
- Linux: xprintidle if available, else fail open
- Windows (Git Bash/MSYS2): PowerShell GetLastInputInfo via Add-Type
- Unknown/headless: always returns 0 (fail open)
- OBSERVER_MAX_IDLE_SECONDS=0 disables gate

Fixes in this commit:
- 10# base-10 prefix prevents octal arithmetic crash on midnight minutes
  containing digits 8 or 9 (e.g. 00:08 = "008" is invalid octal)
- PowerShell output piped through tr -d '\r' to strip Windows CRLF;
  also uses [long] cast to avoid TickCount 32-bit overflow after 24 days
- mktemp now uses log file directory instead of TMPDIR to ensure
  same-filesystem mv on Linux (atomic rename instead of copy+unlink)
- mkdir -p failure exits 0 (fail open) rather than crashing under set -e
- Numeric validation on last_spawn prevents arithmetic error on corrupt log

Gate execution order: 1 (time, ~0ms) → 2 (cooldown, ~1ms) → 3 (idle, ~50ms)

* fix: harden session guardian gates

---------

Co-authored-by: Affaan Mustafa <affaan@dcube.ai>
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.

3 participants