Skip to content

feat: add project cooldown log to prevent observer re-spawn loops#412

Closed
ispaydeu wants to merge 1 commit into
affaan-m:mainfrom
ispaydeu:feat/project-cooldown-log
Closed

feat: add project cooldown log to prevent observer re-spawn loops#412
ispaydeu wants to merge 1 commit into
affaan-m:mainfrom
ispaydeu:feat/project-cooldown-log

Conversation

@ispaydeu

@ispaydeu ispaydeu commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Problem

When ECC's observer loop spawns a Haiku session, that session can trigger
`observe.sh` hooks which signal the observer — causing a new analysis
cycle before the previous one has finished. Under some conditions this
creates a rapid re-spawn loop that consumes available licensed usage.

Solution

Adds `session-guardian.sh`, a lightweight gatekeeper called by
`observer-loop.sh` before each Haiku spawn. It maintains a per-project
log of last spawn times and blocks new spawns if the cooldown window
hasn't elapsed.

New file: `skills/continuous-learning-v2/agents/session-guardian.sh`

Env var Default Description
`OBSERVER_INTERVAL_SECONDS` `300` Per-project cooldown in seconds
`OBSERVER_LAST_RUN_LOG` `~/.claude/observer-last-run.log` Log path

How it works

  1. Acquires a `mkdir`-based lock on the log file (safe for concurrent projects)
  2. Reads the log for current project's last spawn timestamp (git root as key)
  3. If `(now - last_spawn) < OBSERVER_INTERVAL_SECONDS` → exits 1 (skip cycle)
  4. Otherwise → updates log with tab-delimited entry and exits 0 (proceed)

Debug output to stderr uses only the project basename — no full paths.
Fails open on lock contention (concurrent process holds the lock).

Platform support

No external dependencies. Uses only `date +%s`, `git`, `awk`, `grep`,
`mkdir` — all pre-installed on macOS, Linux, and Windows (Git Bash/MSYS2).

Testing

```bash

First call passes (exit 0)

OBSERVER_LAST_RUN_LOG=/tmp/test.log
./skills/continuous-learning-v2/agents/session-guardian.sh; echo $?

Immediate second call blocked (exit 1)

./skills/continuous-learning-v2/agents/session-guardian.sh; echo $?

Expired cooldown passes (exit 0)

project=$(git rev-parse --show-toplevel)
printf '%s\t%s\n' "$project" "$(( $(date +%s) - 400 ))" > /tmp/test.log
OBSERVER_LAST_RUN_LOG=/tmp/test.log
./skills/continuous-learning-v2/agents/session-guardian.sh; echo $?
```

Related

  • Companion PR: `feat/active-hours-idle-guard` adds time-window and idle-detection gates

Summary by cubic

Add a per-project cooldown to the observer to stop rapid re-spawn loops triggered by observe.sh hooks. A new session-guardian.sh gate blocks cycles if the same project ran within the cooldown window to reduce wasted sessions.

  • New Features
    • Added skills/continuous-learning-v2/agents/session-guardian.sh; called by observer-loop.sh before each spawn and logs skipped cycles.
    • Per-project last-run log at ~/.claude/observer-last-run.log (tab-delimited); default cooldown 300s via OBSERVER_INTERVAL_SECONDS.
    • Project key from git rev-parse --show-toplevel (fallback to PWD); exits 1 to skip, 0 to proceed; debug uses project basename only.
    • Concurrency-safe mkdir lock; fails open on lock contention. No external deps; works on macOS, Linux, and Windows (Git Bash/MSYS2).

Written for commit 332b8b4. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • Improvements
    • Observer runs now include a per-project cooldown mechanism (configurable, default 300 seconds).
    • Claude analysis is skipped when cooldown is active.
    • Lock-based synchronization ensures safe concurrent behavior.

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).
Copilot AI review requested due to automatic review settings March 12, 2026 15:25
@coderabbitai

coderabbitai Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This change adds a session guard mechanism to the observer loop. A new session-guardian.sh script enforces per-project cooldowns (default 300 seconds) using filesystem locks and timestamp tracking to prevent excessive Claude analysis invocations. The observer-loop.sh is modified to invoke this guard before analysis execution.

Changes

Cohort / File(s) Summary
Observer Loop Integration
skills/continuous-learning-v2/agents/observer-loop.sh
Added session-guardian gate that checks cooldown status before Claude analysis; logs skip message and returns early if guard fails.
Session Guardian
skills/continuous-learning-v2/agents/session-guardian.sh
New script implementing per-project cooldown mechanism with filesystem locking, timestamp tracking in log file, configurable intervals via environment variables, and atomic log updates for concurrent access handling.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A guardian hops through project lands,
With locks and timestamps in paws so grand,
No double-clicking the Claude machine,
Just peaceful pauses in between—
Cool yet quick, a rhythm so keen! 🏃‍♂️✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.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 'feat: add project cooldown log to prevent observer re-spawn loops' directly aligns with the main change: adding session-guardian.sh to implement a per-project cooldown mechanism that prevents rapid observer re-spawn loops.

✏️ 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 for PR comments
  • Generate 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.

❤️ 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 a “session guardian” gate to the Continuous Learning v2 observer loop to prevent spawning Claude analysis sessions too frequently for the same project by using a per-project cooldown log.

Changes:

  • Introduces session-guardian.sh to enforce a per-project cooldown using a timestamp log and a filesystem lock.
  • Hooks session-guardian.sh into observer-loop.sh so observer cycles can be skipped during cooldown.

Reviewed changes

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

File Description
skills/continuous-learning-v2/agents/session-guardian.sh New guard script that tracks last run per project root and enforces a cooldown before allowing analysis.
skills/continuous-learning-v2/agents/observer-loop.sh Calls the guard before spawning claude so the observer can skip cycles during cooldown.

💡 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.

# Key: git root path. Falls back to $PWD outside a git repo.
# stderr uses basename only — never prints the full absolute path.

project_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"

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.

git rev-parse is used unconditionally to derive project_root. If git isn’t installed/in PATH, bash typically emits a command not found diagnostic on stderr (not suppressed by 2>/dev/null), which can create noisy logs. Consider guarding with command -v git (and falling back to PWD) to keep the script quiet and predictable outside git environments.

Suggested change
project_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
if command -v git >/dev/null 2>&1; then
project_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
else
project_root="$PWD"
fi

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +38
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}"

elapsed=$(( now - last_spawn ))

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 lookup uses grep -F "$project_root" and then pulls the last field via awk. This can mis-associate entries when one project root is a substring of another, and it will also break (arithmetic error under set -e) if the log ever contains a non-numeric timestamp. Prefer matching the first column exactly (e.g., compare field 1) and validate/coerce the timestamp before doing arithmetic.

Copilot uses AI. Check for mistakes.
fi

# Update log atomically: remove old entry, append new timestamp
tmp_log="$(mktemp "${TMPDIR:-/tmp}/observer-last-run.XXXXXX")"

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.

tmp_log is created under ${TMPDIR:-/tmp} and then mv’d to $LOG_PATH. If $LOG_PATH is on a different filesystem (common when /tmp is separate), mv is not atomic (copy+delete), which can leave a partially-written log if interrupted. Create the temp file in the same directory as $LOG_PATH (and then rename) to preserve atomicity.

Suggested change
tmp_log="$(mktemp "${TMPDIR:-/tmp}/observer-last-run.XXXXXX")"
tmp_log="$(mktemp "$(dirname "$LOG_PATH")/observer-last-run.XXXXXX")"

Copilot uses AI. Check for mistakes.
# OBSERVER_LAST_RUN_LOG default: ~/.claude/observer-last-run.log
#
set -euo pipefail

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 script documents a narrow exit-code contract (0=proceed, 1=skip), but set -euo pipefail means unexpected conditions (e.g., unwritable log dir, mktemp failure) will also return non-zero. Since the caller treats any non-zero as “skip”, this can silently disable analysis. Consider catching internal errors and explicitly exit 0 (fail open), or use a distinct non-1 exit code and have the caller handle it separately.

Suggested change
# Fail open on unexpected internal errors: log and proceed (exit 0).
trap 'echo "session-guardian: internal error, proceeding (fail-open)" >&2; exit 0' ERR

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +39
if ! bash "$(dirname "$0")/session-guardian.sh"; then
echo "[$(date)] Observer cycle skipped by session-guardian" >> "$LOG_FILE"
return

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 treats any non-zero exit from session-guardian.sh as a cooldown skip. If session-guardian.sh fails for an unexpected reason (permissions, mktemp, etc.), the observer will silently stop analyzing. Consider checking the exit code explicitly (e.g., 1 => skip, other non-zero => log an error but proceed/fail open) so operational failures don’t disable the observer.

Suggested change
if ! bash "$(dirname "$0")/session-guardian.sh"; then
echo "[$(date)] Observer cycle skipped by session-guardian" >> "$LOG_FILE"
return
bash "$(dirname "$0")/session-guardian.sh"
guardian_status=$?
if [ "$guardian_status" -ne 0 ]; then
if [ "$guardian_status" -eq 1 ]; then
echo "[$(date)] Observer cycle skipped by session-guardian" >> "$LOG_FILE"
return
else
echo "[$(date)] session-guardian failed with exit code $guardian_status; proceeding with analysis (fail-open)" >> "$LOG_FILE"
fi

Copilot uses AI. Check for mistakes.

@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.

1 issue 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:35">
P1: `grep -F "$project_root"` does a substring match, so a project like `/home/dev/app` will also match `/home/dev/app-backend`. This causes both an incorrect cooldown read here and data loss on line 47 (`grep -vF` deletes both entries). Append a literal tab to the pattern so it matches only the exact key in the tab-delimited log.</violation>
</file>

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

trap 'rm -rf "$_lock_dir"' EXIT INT TERM

last_spawn=0
last_spawn=$(grep -F "$project_root" "$LOG_PATH" 2>/dev/null | tail -n1 | awk '{print $NF}') || true

@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.

P1: grep -F "$project_root" does a substring match, so a project like /home/dev/app will also match /home/dev/app-backend. This causes both an incorrect cooldown read here and data loss on line 47 (grep -vF deletes both entries). Append a literal tab to the pattern so it matches only the exact key in the tab-delimited log.

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/session-guardian.sh, line 35:

<comment>`grep -F "$project_root"` does a substring match, so a project like `/home/dev/app` will also match `/home/dev/app-backend`. This causes both an incorrect cooldown read here and data loss on line 47 (`grep -vF` deletes both entries). Append a literal tab to the pattern so it matches only the exact key in the tab-delimited log.</comment>

<file context>
@@ -0,0 +1,56 @@
+  trap 'rm -rf "$_lock_dir"' EXIT INT TERM
+
+  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}"
+
</file context>
Fix with Cubic

@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: 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 `@skills/continuous-learning-v2/agents/observer-loop.sh`:
- Around line 36-40: The session-guardian cooldown is recorded too early in
observer-loop.sh (it runs before the claude availability check), so move the
call that records the cooldown (the bash "$(dirname "$0")/session-guardian.sh"
invocation) to occur immediately after the claude launch attempt (the line that
runs claude --model haiku ...) or split session-guardian.sh into two functions
(e.g., session_guardian_check and session_guardian_record) and call the "check"
before the claude availability test and the "record" only when you actually
spawn a Haiku session; ensure you update references to LOG_FILE and preserve the
existing skip log message ("Observer cycle skipped by session-guardian") so the
cooldown is only consumed when the spawn succeeds.

In `@skills/continuous-learning-v2/agents/session-guardian.sh`:
- Around line 28-31: The lock contention path currently logs and lets the loser
continue; change it so a failed mkdir on _lock_dir causes the script to skip
this cycle instead of proceeding. In the session-guardian.sh block that checks
"if ! mkdir \"$_lock_dir\" ...", replace the current "proceed" behavior with an
early exit/return that stops further work (e.g., log the contention and exit 0
or return from the function). Keep the successful-creation cleanup/trap logic
for removing _lock_dir intact so the winner still releases the lock.
- Around line 35-50: The current use of grep -F with project_root matches
prefixes and can steal or remove other projects' entries; replace the grep
invocations that compute last_spawn and that filter out old entries (the lines
using grep -F "$project_root" and grep -vF "$project_root" writing to tmp_log)
with exact-key matching, e.g. use awk (or grep -Fx) to select/remove only lines
whose first field exactly equals project_root; ensure the last_spawn extraction
uses the same exact-key logic (the variable last_spawn calculation) and the
tmp_log rewrite (the removal of the old entry before appending the new
"${project_root}\t${now}") also uses exact-key matching so only the intended
project's timestamp is read/removed.
- Around line 20-21: The cooldown key currently derives
project_root/project_name by running git and basename in session-guardian.sh
(variables project_root and project_name); change it to use the provided
environment variable PROJECT_DIR (set by start-observer.sh) as the canonical
project directory/key. Replace uses of project_root/project_name when building
the cooldown identifier with PROJECT_DIR (or basename "$PROJECT_DIR" if you only
need the name) so cooldowns are tied to the observer-provided PROJECT_DIR rather
than the current working dir or git root.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4e24536f-22c1-485d-b69f-1fd7e2635796

📥 Commits

Reviewing files that changed from the base of the PR and between 51eec12 and 332b8b4.

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

Comment on lines +36 to +40
# 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
fi

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.

⚠️ Potential issue | 🟡 Minor

Record the cooldown at the actual spawn point.

This gate runs before the claude availability check on Lines 42-45, so a missing CLI still burns the cooldown window even though no Haiku session was launched. Move it as close as possible to the claude --model haiku ... line, or split session-guardian.sh into separate “check” and “record” phases.

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

In `@skills/continuous-learning-v2/agents/observer-loop.sh` around lines 36 - 40,
The session-guardian cooldown is recorded too early in observer-loop.sh (it runs
before the claude availability check), so move the call that records the
cooldown (the bash "$(dirname "$0")/session-guardian.sh" invocation) to occur
immediately after the claude launch attempt (the line that runs claude --model
haiku ...) or split session-guardian.sh into two functions (e.g.,
session_guardian_check and session_guardian_record) and call the "check" before
the claude availability test and the "record" only when you actually spawn a
Haiku session; ensure you update references to LOG_FILE and preserve the
existing skip log message ("Observer cycle skipped by session-guardian") so the
cooldown is only consumed when the spawn succeeds.

Comment on lines +20 to +21
project_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
project_name="$(basename "$project_root")"

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.

⚠️ Potential issue | 🟠 Major

Use PROJECT_DIR as the cooldown key.

start-observer.sh already provides PROJECT_DIR, but this code keys the log from the process working directory / current Git root instead. Since the observer is launched without a cd, starting it from another directory can make unrelated projects share a cooldown or bypass it entirely.

Suggested fix
-project_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
+project_root="${PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
 project_name="$(basename "$project_root")"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
project_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "$PWD")"
project_name="$(basename "$project_root")"
project_root="${PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
project_name="$(basename "$project_root")"
🤖 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 20 -
21, The cooldown key currently derives project_root/project_name by running git
and basename in session-guardian.sh (variables project_root and project_name);
change it to use the provided environment variable PROJECT_DIR (set by
start-observer.sh) as the canonical project directory/key. Replace uses of
project_root/project_name when building the cooldown identifier with PROJECT_DIR
(or basename "$PROJECT_DIR" if you only need the name) so cooldowns are tied to
the observer-provided PROJECT_DIR rather than the current working dir or git
root.

Comment on lines +28 to +31
if ! mkdir "$_lock_dir" 2>/dev/null; then
# Another observer holds the lock — fail open, let this cycle proceed
echo "session-guardian: log locked by concurrent process, proceeding" >&2
else

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.

⚠️ Potential issue | 🟠 Major

Lock contention should skip the cycle, not proceed.

If two observer triggers arrive together, the loser of this lock currently returns success and spawns anyway. That bypasses the serialization the guard is supposed to enforce and reintroduces duplicate Haiku launches during bursts.

Suggested fix
 if ! mkdir "$_lock_dir" 2>/dev/null; then
-  # Another observer holds the lock — fail open, let this cycle proceed
-  echo "session-guardian: log locked by concurrent process, proceeding" >&2
+  # Another observer is updating the cooldown log; don't bypass the gate.
+  echo "session-guardian: log locked by concurrent process, skipping cycle" >&2
+  exit 1
 else
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if ! mkdir "$_lock_dir" 2>/dev/null; then
# Another observer holds the lock — fail open, let this cycle proceed
echo "session-guardian: log locked by concurrent process, proceeding" >&2
else
if ! mkdir "$_lock_dir" 2>/dev/null; then
# Another observer is updating the cooldown log; don't bypass the gate.
echo "session-guardian: log locked by concurrent process, skipping cycle" >&2
exit 1
else
🤖 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 28 -
31, The lock contention path currently logs and lets the loser continue; change
it so a failed mkdir on _lock_dir causes the script to skip this cycle instead
of proceeding. In the session-guardian.sh block that checks "if ! mkdir
\"$_lock_dir\" ...", replace the current "proceed" behavior with an early
exit/return that stops further work (e.g., log the contention and exit 0 or
return from the function). Keep the successful-creation cleanup/trap logic for
removing _lock_dir intact so the winner still releases the lock.

Comment on lines +35 to +50
last_spawn=$(grep -F "$project_root" "$LOG_PATH" 2>/dev/null | tail -n1 | awk '{print $NF}') || true
last_spawn="${last_spawn:-0}"

elapsed=$(( now - last_spawn ))
if [ "$elapsed" -lt "$INTERVAL" ]; then
rm -rf "$_lock_dir"
trap - EXIT INT TERM
echo "session-guardian: cooldown active for '${project_name}' (last spawn ${elapsed}s ago, interval ${INTERVAL}s)" >&2
exit 1
fi

# Update log atomically: remove old entry, append new timestamp
tmp_log="$(mktemp "${TMPDIR:-/tmp}/observer-last-run.XXXXXX")"
grep -vF "$project_root" "$LOG_PATH" > "$tmp_log" 2>/dev/null || true
echo "${project_root} ${now}" >> "$tmp_log"
mv "$tmp_log" "$LOG_PATH"

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.

⚠️ Potential issue | 🟠 Major

Match log entries by exact key, not substring.

grep -F "$project_root" and grep -vF "$project_root" also match path prefixes, so /work/foo will read/remove /work/foo-bar entries in the shared log. That makes one project inherit or delete another project's cooldown timestamp.

Suggested fix
-  last_spawn=$(grep -F "$project_root" "$LOG_PATH" 2>/dev/null | tail -n1 | awk '{print $NF}') || true
+  last_spawn=$(awk -F '\t' -v key="$project_root" '$1 == key { ts = $2 } END { print ts }' "$LOG_PATH" 2>/dev/null) || true
   last_spawn="${last_spawn:-0}"
…
-  grep -vF "$project_root" "$LOG_PATH" > "$tmp_log" 2>/dev/null || true
+  awk -F '\t' -v OFS='\t' -v key="$project_root" '$1 != key' "$LOG_PATH" > "$tmp_log" 2>/dev/null || true
   echo "${project_root}	${now}" >> "$tmp_log"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
last_spawn=$(grep -F "$project_root" "$LOG_PATH" 2>/dev/null | tail -n1 | awk '{print $NF}') || true
last_spawn="${last_spawn:-0}"
elapsed=$(( now - last_spawn ))
if [ "$elapsed" -lt "$INTERVAL" ]; then
rm -rf "$_lock_dir"
trap - EXIT INT TERM
echo "session-guardian: cooldown active for '${project_name}' (last spawn ${elapsed}s ago, interval ${INTERVAL}s)" >&2
exit 1
fi
# Update log atomically: remove old entry, append new timestamp
tmp_log="$(mktemp "${TMPDIR:-/tmp}/observer-last-run.XXXXXX")"
grep -vF "$project_root" "$LOG_PATH" > "$tmp_log" 2>/dev/null || true
echo "${project_root} ${now}" >> "$tmp_log"
mv "$tmp_log" "$LOG_PATH"
last_spawn=$(awk -F '\t' -v key="$project_root" '$1 == key { ts = $2 } END { print ts }' "$LOG_PATH" 2>/dev/null) || true
last_spawn="${last_spawn:-0}"
elapsed=$(( now - last_spawn ))
if [ "$elapsed" -lt "$INTERVAL" ]; then
rm -rf "$_lock_dir"
trap - EXIT INT TERM
echo "session-guardian: cooldown active for '${project_name}' (last spawn ${elapsed}s ago, interval ${INTERVAL}s)" >&2
exit 1
fi
# Update log atomically: remove old entry, append new timestamp
tmp_log="$(mktemp "${TMPDIR:-/tmp}/observer-last-run.XXXXXX")"
awk -F '\t' -v OFS='\t' -v key="$project_root" '$1 != key' "$LOG_PATH" > "$tmp_log" 2>/dev/null || true
echo "${project_root} ${now}" >> "$tmp_log"
mv "$tmp_log" "$LOG_PATH"
🤖 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 35 -
50, The current use of grep -F with project_root matches prefixes and can steal
or remove other projects' entries; replace the grep invocations that compute
last_spawn and that filter out old entries (the lines using grep -F
"$project_root" and grep -vF "$project_root" writing to tmp_log) with exact-key
matching, e.g. use awk (or grep -Fx) to select/remove only lines whose first
field exactly equals project_root; ensure the last_spawn extraction uses the
same exact-key logic (the variable last_spawn calculation) and the tmp_log
rewrite (the removal of the old entry before appending the new
"${project_root}\t${now}") also uses exact-key matching so only the intended
project's timestamp is read/removed.

@affaan-m

Copy link
Copy Markdown
Owner

Superseded by #413, which carried the session-guardian lane forward with the active-hours, idle-detection, and cooldown fixes on top of the same observer path. Closing this one to keep the queue clean.

@affaan-m affaan-m closed this Mar 13, 2026
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