feat: active hours + idle detection gates for session-guardian#413
Conversation
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)
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds a session-guardian gate to the observer loop: the loop invokes a new Changes
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
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 viaxprintidleif present, Windows via PowerShell P/Invoke) to skip cycles when the user has been idle too long. - Hooks
session-guardian.shinto 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}" | ||
|
|
There was a problem hiding this comment.
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.
| # 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 |
| if [ $(( 10#${current_hhmm:-0} )) -lt $(( 10#${ACTIVE_START:-800} )) ] || \ | ||
| [ $(( 10#${current_hhmm:-0} )) -ge $(( 10#${ACTIVE_END:-2300} )) ]; then |
There was a problem hiding this comment.
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.
| 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) |
| } | ||
|
|
||
| if [ "$MAX_IDLE" -gt 0 ]; then | ||
| idle_seconds=$(get_idle_seconds) |
There was a problem hiding this comment.
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.
| 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 |
| 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) \ |
There was a problem hiding this comment.
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.
| 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) \ |
| return | ||
| fi | ||
|
|
||
| # session-guardian: gate observer cycle (cooldown log — see session-guardian.sh) |
There was a problem hiding this comment.
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.
| # 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) |
| 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}" |
There was a problem hiding this comment.
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.
| 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" |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 configuresACTIVE_START=2200andACTIVE_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
📒 Files selected for processing (2)
skills/continuous-learning-v2/agents/observer-loop.shskills/continuous-learning-v2/agents/session-guardian.sh
There was a problem hiding this comment.
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.
| fi | ||
|
|
||
| # session-guardian: gate observer cycle (cooldown log — see session-guardian.sh) | ||
| if ! bash "$(dirname "$0")/session-guardian.sh"; then |
There was a problem hiding this comment.
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>
…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>
…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>
Summary
Extends
session-guardian.sh(introduced in #412) with two additional guard gates that prevent observer sessions from spawning when no human is present:date +%k%Mwith 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 to0to disable./usr/sbin/ioreg+awk(built-in)xprintidleif installed, else fail openGetLastInputInfoviaAdd-Type(Win32 API, pre-installed)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.
Configuration
All settings are optional env vars with sensible defaults:
Correctness fixes included
10#base-10 prefix on all HHMM arithmetic to prevent octal crash on midnight minutes containing digits 8 or 9\rviatr -d '\r'(Windows CRLF); uses[long]cast to prevent 32-bitTickCountoverflow after 24.9 days uptimemktempuses log file's own directory (not$TMPDIR) somvis always same-filesystem on Linuxmkdir -pfailure exits 0 (fail open) rather than propagating a non-zero exit underset -elast_spawnguards against corrupt log entries causing arithmetic errorsTest 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 stderrOBSERVER_MAX_IDLE_SECONDS=1 bash session-guardian.sh→ exit 0 on macOS with ioreg idle < 1 sbash -c 'x="008"; echo $(( 10#$x < 800 ))'→ outputs1(octal fix)Summary by cubic
Adds active-hours and idle-detection gates to
session-guardian.shand wires it intoobserver-loop.shto 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
OBSERVER_ACTIVE_HOURS_START=0andOBSERVER_ACTIVE_HOURS_END=0.ioreg, Linux viaxprintidleif available (else fail open), Windows via PowerShellGetLastInputInfo. Disable withOBSERVER_MAX_IDLE_SECONDS=0.observer-loop.sh. Gate order: time window → per-project cooldown (unchanged, defaultOBSERVER_INTERVAL_SECONDS=300) → idle. Config vars:OBSERVER_INTERVAL_SECONDS,OBSERVER_LAST_RUN_LOG,OBSERVER_ACTIVE_HOURS_START/END,OBSERVER_MAX_IDLE_SECONDS.Bug Fixes
[long]cast; same-filesystemmvfor log updates; fail-open ifmkdir -pcannot create the log dir; numeric validation of log entries.Written for commit a46e644. Summary will update on new commits.
Summary by CodeRabbit