Skip to content

feat: stateful monitor contract for scheduler (change-detection core) - #3845

Open
praisonai-triage-agent[bot] wants to merge 2 commits into
mainfrom
claude/issue-3844-20260810-0946
Open

feat: stateful monitor contract for scheduler (change-detection core)#3845
praisonai-triage-agent[bot] wants to merge 2 commits into
mainfrom
claude/issue-3844-20260810-0946

Conversation

@praisonai-triage-agent

@praisonai-triage-agent praisonai-triage-agent Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #3844

Summary

Adds the core contract increment that makes the scheduler's condition gates stateful and change-aware, so unattended monitor automations ("wake only when a watched source changed, and tell me only what changed") can be expressed across CLI + YAML + Python β€” without burning a full model turn on every tick against an unchanged source.

Scoped to core protocols/models only per AGENTS.md (lightweight & powerful; protocols in core, heavy impl in wrapper). Fully additive and backward-compatible: jobs that set neither monitor nor per-job state behave exactly as today.

Changes (praisonaiagents/scheduler)

  • GateResult (protocols.py): add no_change (a distinct silent-suppress "monitor mode" outcome, separate from a generic run=False skip) and state_updates (bounded KV the gate wants persisted for the next tick β€” last-seen hash, watermark).
  • JobConditionProtocol.should_run: optional keyword-only state param so a stateful monitor gate can compare against prior per-job state. Existing stateless gates (should_run(self, job)) satisfy the protocol unchanged.
  • JobStateStoreProtocol (new): bounded per-job get_state/set_state/clear_state scratchpad β€” a job's durable memory across wake-ups. Contract only; concrete store lives in the wrapper.
  • RunRecord.status (models.py): add no_change alongside succeeded/failed/skipped.
  • ScheduleJob: add optional monitor source spec ({"command": ...} / {"url": ...}) with to_dict/from_dict round-trip (omitted from serialization when unset).
  • Exports wired through scheduler/__init__.py.

Out of scope (follow-up, wrapper/bot layer)

The heavy MonitorGate (shell/URL probe + hashing + bounded diff), executor persistence of state + the no_change record, and standalone-sender suppression β€” deliberately kept out of core.

Test plan

  • Existing scheduler suite green (115 passed): history, integration, tools, claim, provider, principal.
  • New contract verified: monitor round-trips and is omitted when unset; GateResult(no_change, state_updates); RunRecord(status="no_change") round-trips; stateless gate still satisfies JobConditionProtocol; a get/set/clear store satisfies JobStateStoreProtocol.

Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for stateful job monitoring and change detection.
    • Jobs can now track monitoring state between runs.
    • Added a new β€œno change” execution status for jobs whose monitored data has not changed.
    • Monitoring settings and state updates can be saved and restored automatically.
  • Improvements
    • Scheduler conditions can use previously stored state when deciding whether a job should run.

Extend the core scheduler contract so change-detection monitor
automations can be expressed across CLI + YAML + Python, keeping heavy
implementation (shell/URL probe, hashing, diffing, persistence) in the
wrapper. Additive and backward-compatible: jobs that set neither
`monitor` nor per-job state behave exactly as today.

- GateResult: add `no_change` (distinct silent-suppress outcome) and
  `state_updates` (bounded KV a gate persists for the next tick).
- JobConditionProtocol.should_run: optional keyword-only `state` so a
  stateful monitor gate can compare against prior state; existing
  stateless gates satisfy the protocol unchanged.
- New JobStateStoreProtocol: bounded per-job get/set/clear scratchpad.
- RunRecord.status: add `no_change` alongside succeeded/failed/skipped.
- ScheduleJob: add optional `monitor` source spec with round-trip.

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
@MervinPraison

Copy link
Copy Markdown
Owner

@coderabbitai review

@MervinPraison

Copy link
Copy Markdown
Owner

/review

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor
βœ… Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more β†’

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account β†’

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us β†’

@MervinPraison MervinPraison added pipeline/blocked:ci Blocked: CI not green on HEAD pipeline/blocked:manual-review Blocked: requires manual review pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet pipeline/final-claude-pending Reviews done; waiting for FINAL @claude labels Aug 10, 2026
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 81d84a9b-3b1c-48b4-ab1f-1bcc33809cb0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • πŸ” Trigger review
πŸ“ Walkthrough

Walkthrough

The scheduler now supports state-aware condition gates, per-job state storage contracts, monitor specifications on scheduled jobs, persisted monitor configuration, and a distinct no_change run status.

Changes

Stateful scheduler monitoring

Layer / File(s) Summary
Stateful gate contracts
src/praisonai-agents/praisonaiagents/scheduler/protocols.py, src/praisonai-agents/praisonaiagents/scheduler/__init__.py
GateResult now carries no_change and optional state_updates. JobConditionProtocol.should_run accepts optional persisted state. JobStateStoreProtocol is lazily resolved and publicly exported.
Monitor job model and persistence
src/praisonai-agents/praisonaiagents/scheduler/models.py
RunRecord supports the no_change status. ScheduleJob supports optional monitor specifications and serializes and restores them.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

πŸš₯ Pre-merge checks | βœ… 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers core contracts and models but does not implement the linked issue's CLI, persistence, wrapper gating, executor, and sender requirements. Implement the remaining #3844 requirements or link this PR to a narrower issue that covers only the core contract and model changes.
βœ… Passed checks (4 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title clearly identifies the stateful scheduler monitor contract, which is the primary change in the pull request.
Out of Scope Changes check βœ… Passed The changes remain within the stated core scheduler contract and model scope and do not introduce unrelated functionality.
Docstring Coverage βœ… Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
πŸ“ Generate docstrings
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-3844-20260810-0946

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.

@greptile-apps

greptile-apps Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds scheduler contracts and serialized model fields for stateful change detection while leaving concrete monitor execution and persistence to the wrapper layer.

  • Adds no-change and state-update gate outcomes.
  • Adds a per-job state-store protocol and optional gate state input.
  • Adds monitor configuration and no-change run status serialization.
  • Exposes the new state-store protocol through the scheduler package.

Confidence Score: 4/5

The PR should not merge until the outstanding callable-contract incompatibility for existing stateless gates is resolved.

Existing should_run(self, job) gates still lack the state keyword declared by JobConditionProtocol, so runtime protocol checks pass while static structural conformance fails.

Files Needing Attention: src/praisonai-agents/praisonaiagents/scheduler/protocols.py and src/praisonai-bot/praisonai_bot/scheduler/condition_gate.py

Important Files Changed

Filename Overview
src/praisonai-agents/praisonaiagents/scheduler/protocols.py Adds stateful gate and state-store contracts, but the expanded gate signature remains incompatible with existing stateless implementations under static structural typing.
src/praisonai-agents/praisonaiagents/scheduler/models.py Adds monitor configuration and no-change status fields with additive dictionary serialization.
src/praisonai-agents/praisonaiagents/scheduler/init.py Lazily exports JobStateStoreProtocol consistently through type-checking imports, runtime lookup, caching, and all.

Sequence Diagram

sequenceDiagram
  participant Executor
  participant Store as JobStateStore
  participant Gate as JobCondition
  participant Agent
  Executor->>Store: get_state(job_id)
  Store-->>Executor: prior state
  Executor->>Gate: "should_run(job, state=prior)"
  Gate-->>Executor: GateResult
  alt source changed
    Executor->>Store: set_state(job_id, state_updates)
    Executor->>Agent: run with change context
  else no change
    Executor->>Store: set_state(job_id, state_updates)
    Executor-->>Executor: record no_change
  end
Loading

Reviews (2): Last reviewed commit: "fix: enforce no_change invariant and pre..." | Re-trigger Greptile

Comment on lines 101 to +103
"""

def should_run(self, job: Any) -> "GateResult":
"""Return a :class:`GateResult` deciding whether ``job`` should run."""
def should_run(

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 Legacy gate signatures no longer conform

When an existing stateless gate implements should_run(self, job), it cannot satisfy the expanded callable contract or accept a state-aware call using state=, causing downstream type-checking failures or a runtime TypeError despite the documented backward-compatibility guarantee.

@MervinPraison

Copy link
Copy Markdown
Owner

@claude You are the FINAL architecture reviewer. If the branch is under MervinPraison/PraisonAI (not a fork), you are able to make modifications to this branch and push directly. SCOPE: Focus ONLY on Python packages (praisonaiagents, praisonai). Do NOT modify praisonai-rust or praisonai-ts. Read ALL comments above from Gemini, Qodo, CodeRabbit, and Copilot carefully before responding.

Phase 1: Review per AGENTS.md

  1. Protocol-driven: check heavy implementations vs core SDK
  2. Backward compatible: ensure zero feature regressions
  3. Performance: no hot-path regressions
  4. SDK value: review in depth whether the change genuinely adds value to the SDK β€” never add features for the sake of adding them. It must strengthen the SDK (simpler, more user-friendly, robust, world-class, secure). If it does not clearly add value, request changes or recommend rejecting/closing rather than merging scope creep
  5. Do not bloat the Agent class with additional params β€” only if absolutely required; we already support many params.
  6. Repo routing: agent-callable tools β†’ PraisonAI-Tools; lifecycle plugins β†’ PraisonAI-Plugins; optional sandbox backends β†’ PraisonAI-Plugins (praisonai.sandbox entry point) β€” request changes if wrongly added to praisonaiagents/

Phase 2: FIX Valid Issues
7. For any VALID bugs or architectural flaws found by Gemini, CodeRabbit, Qodo, Copilot, or any other reviewer: implement the fix
8. Also independently identify and fix any gaps or issues you find in the changed code β€” do not rely only on prior reviewer feedback
9. Push all code fixes directly to THIS branch (do NOT create a new PR)
10. Comment a summary of exact files modified and what you skipped

Phase 3: Final Verdict
11. If all issues are resolved, approve the PR / close the Issue
12. If blocking issues remain, request changes / leave clear action items

@MervinPraison MervinPraison added pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:cooldown Blocked: post-push or @claude cooldown and removed pipeline/final-claude-pending Reviews done; waiting for FINAL @claude pipeline/blocked:no-final Blocked: no FINAL @claude trigger yet labels Aug 10, 2026

@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: 2

🧹 Nitpick comments (1)
src/praisonai-agents/praisonaiagents/scheduler/protocols.py (1)

50-79: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Split the optional per-job state operations into capability protocols.

JobStateStoreProtocol currently requires get_state, set_state, and clear_state, so a get_state-only store does not satisfy the protocol. Documented optional methods and hasattr() capability checks conflict with this single protocol.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/scheduler/protocols.py` around lines 50
- 79, Split JobStateStoreProtocol into separate capability protocols for
reading, writing, and clearing state, so stores can implement only the
operations they support. Update each protocol to expose only its corresponding
method, and retain the existing job-state documentation and method signatures
while making capability detection via hasattr() consistent with the protocol
definitions.

Source: Coding guidelines

πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/praisonai-agents/praisonaiagents/scheduler/models.py`:
- Around line 397-400: Update the monitor serialization condition in the model’s
to_dict logic to persist self.monitor whenever it is not None, including an
explicitly configured empty mapping; preserve omission only when monitor is
unset.

In `@src/praisonai-agents/praisonaiagents/scheduler/protocols.py`:
- Around line 43-47: Update the GateResult definition so no_change=True cannot
coexist with the default run=True: force run=False when no_change is set, or
validate and reject the conflicting combination. Preserve normal execution for
results where no_change is false.

---

Nitpick comments:
In `@src/praisonai-agents/praisonaiagents/scheduler/protocols.py`:
- Around line 50-79: Split JobStateStoreProtocol into separate capability
protocols for reading, writing, and clearing state, so stores can implement only
the operations they support. Update each protocol to expose only its
corresponding method, and retain the existing job-state documentation and method
signatures while making capability detection via hasattr() consistent with the
protocol definitions.
πŸͺ„ Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b3c1ee43-c932-4fd5-aad0-c7011e3303cb

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 5fbdd1e and f331e1e.

πŸ“’ Files selected for processing (3)
  • src/praisonai-agents/praisonaiagents/scheduler/__init__.py
  • src/praisonai-agents/praisonaiagents/scheduler/models.py
  • src/praisonai-agents/praisonaiagents/scheduler/protocols.py

Comment on lines +397 to +400
# Monitor source spec. Only persist when configured so stateless jobs
# stay byte-for-byte unchanged; the shape is opaque to the core.
if self.monitor:
d["monitor"] = self.monitor

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.

πŸ—„οΈ Data Integrity & Integration | 🟑 Minor | ⚑ Quick win

Preserve an explicitly configured empty monitor mapping.

from_dict({"monitor": {}}) restores monitor={}, but to_dict() omits it because {} is falsy. A load/save cycle then changes the value to None and silently changes the job to stateless behavior. Persist the field when it is not None.

Proposed fix
-        if self.monitor:
+        if self.monitor is not None:
             d["monitor"] = self.monitor
πŸ“ 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
# Monitor source spec. Only persist when configured so stateless jobs
# stay byte-for-byte unchanged; the shape is opaque to the core.
if self.monitor:
d["monitor"] = self.monitor
# Monitor source spec. Only persist when configured so stateless jobs
# stay byte-for-byte unchanged; the shape is opaque to the core.
if self.monitor is not None:
d["monitor"] = self.monitor
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/scheduler/models.py` around lines 397 -
400, Update the monitor serialization condition in the model’s to_dict logic to
persist self.monitor whenever it is not None, including an explicitly configured
empty mapping; preserve omission only when monitor is unset.

Comment on lines 43 to +47
run: bool = True
context: Optional[str] = None
reason: Optional[str] = None
no_change: bool = False
state_updates: Optional[Dict[str, Any]] = None

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.

🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Enforce the no_change invariant.

GateResult(no_change=True) keeps run=True from its default value. A runner can then execute and deliver a tick that the contract defines as suppressed. Force run=False when no_change is set, or reject the conflicting combination.

Proposed fix
 class GateResult:
+    def __post_init__(self) -> None:
+        if self.no_change:
+            self.run = False
+
     run: bool = True
πŸ“ 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
run: bool = True
context: Optional[str] = None
reason: Optional[str] = None
no_change: bool = False
state_updates: Optional[Dict[str, Any]] = None
def __post_init__(self) -> None:
if self.no_change:
self.run = False
run: bool = True
context: Optional[str] = None
reason: Optional[str] = None
no_change: bool = False
state_updates: Optional[Dict[str, Any]] = None
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/praisonai-agents/praisonaiagents/scheduler/protocols.py` around lines 43
- 47, Update the GateResult definition so no_change=True cannot coexist with the
default run=True: force run=False when no_change is set, or validate and reject
the conflicting combination. Preserve normal execution for results where
no_change is false.

@praisonai-triage-agent

praisonai-triage-agent Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Claude finished @MervinPraison's task β€”β€” View job


Final Architecture Review β€” PR #3845

  • Read AGENTS.md + changed files
  • Phase 1: Architecture review (protocol-driven, backward-compat, value)
  • Phase 2: Fix valid reviewer issues
  • Phase 3: Test + verdict

Phase 1 β€” Architecture verdict βœ…

Criterion Result
Protocol-driven βœ… Core holds contracts only (GateResult, JobStateStoreProtocol, should_run(state=), monitor spec). Heavy MonitorGate/persistence correctly deferred to wrapper.
Backward compatible βœ… Fully additive. Jobs setting neither monitor nor state behave exactly as today; monitor omitted from serialization when unset.
Performance βœ… No hot-path change β€” pure dataclass/protocol additions.
SDK value βœ… Genuine: enables "wake only when a watched source changed" without burning a model turn per tick. no_change is a meaningful outcome distinct from skipped. No Agent param bloat.
Repo routing βœ… Correct β€” contract in praisonaiagents/scheduler; probe/hash/diff stays in wrapper per AGENTS.md.

Phase 2 β€” Fixes pushed (commit 6b00f1d0)

scheduler/protocols.py

  • βœ… [CodeRabbit β€” Major] no_change invariant: added GateResult.__post_init__ forcing run=False when no_change=True. A gate that sets only no_change can no longer leave run=True and let a runner fire a suppressed tick.
  • βœ… [Greptile β€” P1] legacy gate signatures: runtime_checkable matches on method name, so stateless should_run(self, job) gates DO still satisfy the protocol (no regression there). Added an explicit caller contract to the docstring: callers must detect state capability and not unconditionally pass state=, so a legacy one-arg gate never raises TypeError.

scheduler/models.py

  • βœ… [CodeRabbit β€” Minor] empty monitor mapping: to_dict() now persists monitor when is not None (incl. {}), so an explicitly configured empty spec round-trips instead of silently reverting to stateless on a load/save cycle.

Skipped (with reasons)

  • [CodeRabbit nitpick] Split JobStateStoreProtocol into 3 capability protocols β€” Skipped as scope creep. The docstring already declares methods optional and directs callers to hasattr() detection (matching the existing claim_due/log_run optional-method pattern in this file). Adding 3 protocols expands surface area with no live consumer, against the lightweight mandate.

Verification

  • βœ… Full scheduler suite: 115 passed.
  • βœ… New invariants asserted: GateResult(no_change=True).run is False; monitor={} round-trips; unset monitor omitted.

Phase 3 β€” Verdict: βœ… Approve

Core contract is minimal, protocol-driven, backward-compatible, and adds clear SDK value. All valid reviewer findings resolved. The linked-issue "partial scope" warning is by design β€” the PR body explicitly scopes to core contracts with the heavy wrapper impl as documented follow-up.


Job run Β· Branch claude/issue-3844-20260810-0946

- GateResult.__post_init__ forces run=False when no_change is set so a
  silent-suppress outcome can never let a runner fire a suppressed tick.
- ScheduleJob.to_dict persists monitor when not None (incl. {}) so an
  explicitly configured empty spec round-trips instead of reverting to
  stateless.
- Document caller contract: detect state capability before passing
  state= so legacy stateless gates never raise TypeError.

Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
@MervinPraison MervinPraison added the pipeline/blocked:stale-final Blocked: FINAL stale after new commits label Aug 10, 2026
@praisonai-triage-agent praisonai-triage-agent Bot removed pipeline/blocked:stale-final Blocked: FINAL stale after new commits pipeline/blocked:ci Blocked: CI not green on HEAD pipeline/blocked:cooldown Blocked: post-push or @claude cooldown labels Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pipeline/awaiting-merge-gate FINAL done; waiting for merge gate / CI pipeline/blocked:manual-review Blocked: requires manual review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scheduler condition gates are stateless: no change-detection or per-job memory for unattended monitor automations

1 participant