Skip to content

fix: guard supervisor stale-job reconciliation#5

Open
Boulea7 wants to merge 5 commits into
mainfrom
codex/supervisor-owner-stale-guard
Open

fix: guard supervisor stale-job reconciliation#5
Boulea7 wants to merge 5 commits into
mainfrom
codex/supervisor-owner-stale-guard

Conversation

@Boulea7
Copy link
Copy Markdown
Owner

@Boulea7 Boulea7 commented May 27, 2026

Summary

  • persist supervisor owner metadata on running job records
  • keep records owned by another live supervisor process in running state instead of reconciling them to failed
  • still reconcile foreign records when the recorded owner process is dead or missing

Validation

  • env UV_CACHE_DIR=/private/tmp/agy-mcp-uv-cache uv run --default-index https://pypi.org/simple pytest tests/test_supervisor.py::test_status_keeps_foreign_live_supervisor_job_running tests/test_supervisor.py::test_status_reconciles_foreign_dead_supervisor_job tests/test_session_store.py::test_create_and_get_job_round_trip -q
  • env UV_CACHE_DIR=/private/tmp/agy-mcp-uv-cache uv run --default-index https://pypi.org/simple pytest tests/test_supervisor.py tests/test_session_store.py -q
  • env UV_CACHE_DIR=/private/tmp/agy-mcp-uv-cache uv run --default-index https://pypi.org/simple ruff check src/agy_mcp/supervisor.py src/agy_mcp/session_store.py tests/test_supervisor.py tests/test_session_store.py
  • git diff --check

Summary by Sourcery

Guard supervisor stale-job reconciliation by tracking job ownership per supervisor instance and respecting live foreign owners.

New Features:

  • Record supervisor instance metadata with created jobs to identify their owning process.

Bug Fixes:

  • Prevent a supervisor from incorrectly reconciling running jobs owned by another live supervisor instance as failed.
  • Still reconcile stale running jobs as failed when their recorded owner process is no longer alive.

Enhancements:

  • Extend session store job records to persist process ID and arbitrary extra metadata for each job.

Tests:

  • Add supervisor tests covering behavior for foreign live and dead supervisor-owned jobs during status checks.
  • Extend session store round-trip test to validate persistence of new PID and supervisor metadata fields.

Summary by cubic

Prevents the supervisor from marking another live supervisor’s running job as failed. We now verify ownership with a high‑resolution, timezone-stable process signature and respect foreign live owners.

  • Bug Fixes
    • Persist owner metadata on running jobs (pid, extra.supervisor.instance_id, extra.supervisor.process_start_signature); extend SessionStore.create_job to accept pid and extra.
    • On start, record PID, instance_id, and a process start signature (Linux /proc boot-id + start ticks; fallback to ps lstart with LC_ALL=C, TZ=UTC).
    • Supervisor.status respects foreign live owners (verifies PID liveness and signature; reconciles only when dead or mismatched) and uses a non-destructive Windows PID probe.

Written for commit 7d57758. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Jobs now track ownership information, including supervisor instance and process identity, enabling accurate status reconciliation across multiple supervisor instances.
    • Job status no longer incorrectly marks jobs as failed when they are still running under another active supervisor.
  • Tests

    • Expanded test coverage for supervisor ownership tracking and cross-instance job status reconciliation.

Review Change Stack

Change-Id: I12283d663097ff6a6b0afb6447511bbab73dd404
Copilot AI review requested due to automatic review settings May 27, 2026 07:48
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 27, 2026

Reviewer's Guide

Adds per-supervisor instance ownership metadata to job records and updates supervisor status reconciliation so that running jobs owned by another live supervisor process are not incorrectly marked as failed, including persistence in the session store and tests for the new behaviour.

Sequence diagram for updated supervisor status reconciliation

sequenceDiagram
    participant Supervisor
    participant SessionStore
    participant Helper as _owned_by_foreign_live_supervisor
    participant PidCheck as _pid_exists
    participant OS

    Supervisor->>SessionStore: get_job(job_id)
    SessionStore-->>Supervisor: JobRecord(record)

    alt record is None
        Supervisor-->>Supervisor: return None
    else record.status != running
        Supervisor-->>Supervisor: return _public_record(record)
    else record.status == running
        Supervisor->>Helper: _owned_by_foreign_live_supervisor(record, _instance_id)
        Helper->>PidCheck: _pid_exists(owner.pid)
        PidCheck->>OS: os.kill(pid, 0)
        OS-->>PidCheck: result
        PidCheck-->>Helper: bool
        Helper-->>Supervisor: bool

        alt owned by foreign live supervisor
            Supervisor-->>Supervisor: return _public_record(record)
        else not owned or owner dead
            Supervisor->>SessionStore: finalize_job(job_id, status=failed, error="reconciled by supervisor")
            SessionStore-->>Supervisor: JobRecord(finalised)
            Supervisor-->>Supervisor: return _public_record(finalised)
        end
    end
Loading

File-Level Changes

Change Details Files
Track supervisor instance ownership on job records and propagate it from Supervisor.start into persisted metadata.
  • Introduce a per-supervisor _instance_id generated at construction time.
  • Include current process pid and supervisor instance_id in the job record extra["supervisor"] metadata when starting a job.
  • Plumb optional pid and extra fields through job creation so they are stored on disk.
src/agy_mcp/supervisor.py
src/agy_mcp/session_store.py
Guard stale-job reconciliation so that running jobs owned by another live supervisor are preserved, while jobs for dead supervisors are still failed.
  • Extend Supervisor.status to skip reconciliation when a running job record is owned by a different live supervisor instance.
  • Add _owned_by_foreign_live_supervisor helper to evaluate ownership based on stored supervisor instance_id and pid.
  • Add _pid_exists helper that checks liveness of a PID via os.kill(…, 0).
src/agy_mcp/supervisor.py
Add tests covering new supervisor ownership semantics and extended job metadata.
  • Add tests ensuring status keeps foreign live supervisor jobs running and reconciles foreign dead supervisor jobs to failed.
  • Update session store round-trip test to assert pid and supervisor extra metadata persistence.
tests/test_supervisor.py
tests/test_session_store.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

JobRecord storage is extended to accept and persist pid and extra fields. Supervisors now record ownership metadata (instance_id, process signature) when creating jobs, enabling cross-instance reconciliation. New helpers detect live foreign supervisors by validating process identity and liveness. Status reconciliation checks for foreign owners before failing jobs.

Changes

Cross-instance supervisor job ownership and reconciliation

Layer / File(s) Summary
Job storage schema for pid and extra
src/agy_mcp/session_store.py, tests/test_session_store.py
SessionStore.create_job signature extended with optional pid and extra parameters; JobRecord wires pid and defaults extra to empty dict. Test validates round-trip persistence of pid and extra fields.
Supervisor owner tracking and metadata recording
src/agy_mcp/supervisor.py, tests/test_supervisor.py
Supervisor init captures instance_id and process start signature. When creating a job, supervisor records owner metadata (pid, instance_id, process_start_signature) into extra["supervisor"]. Test verifies owner signature is stored.
Foreign supervisor detection helpers
src/agy_mcp/supervisor.py, tests/test_supervisor.py
Helper functions check PID liveness (POSIX signal-0, Windows OpenProcess), compute process start signature from Linux /proc boot-id + stat ticks or ps lstart, and determine if recorded supervisor metadata matches a live foreign owner. Unit tests validate each helper.
Status reconciliation with foreign owner awareness
src/agy_mcp/supervisor.py, tests/test_supervisor.py
status() method now checks if a running job is owned by a live foreign supervisor before marking it failed; if foreign supervisor is live and signature matches, returns record unchanged. Tests validate live foreign jobs remain running, reused PIDs with mismatched signatures are failed, and dead foreign supervisors reconcile to failed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A supervisor now knows who's in charge,
With instance_ids and signatures at large,
Foreign friends are checked with care so fine,
No orphaned jobs marked failed by line,
Cross-instance peace, a rabbit's delight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.72% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 'fix: guard supervisor stale-job reconciliation' directly describes the primary change: adding safety guards to prevent incorrect reconciliation of jobs still owned by live foreign supervisors.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ 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 codex/supervisor-owner-stale-guard

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.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In Supervisor.start, you call os.getpid() twice when persisting the job record; consider capturing it once in a local variable and reusing it to avoid tiny inconsistencies and make the intent clearer.
  • The liveness check in _owned_by_foreign_live_supervisor relies solely on PID and instance ID; you may want to consider mitigating PID reuse issues (e.g., by also recording a start timestamp or monotonic counter) so that very long-lived records are not incorrectly treated as owned by a new, unrelated process that happens to reuse the same PID.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `Supervisor.start`, you call `os.getpid()` twice when persisting the job record; consider capturing it once in a local variable and reusing it to avoid tiny inconsistencies and make the intent clearer.
- The liveness check in `_owned_by_foreign_live_supervisor` relies solely on PID and instance ID; you may want to consider mitigating PID reuse issues (e.g., by also recording a start timestamp or monotonic counter) so that very long-lived records are not incorrectly treated as owned by a new, unrelated process that happens to reuse the same PID.

## Individual Comments

### Comment 1
<location path="src/agy_mcp/supervisor.py" line_range="350-354" />
<code_context>
                 cwd=self._response_cwd(effective_request.cwd),
                 request=_serialise_request(effective_request, self.safety),
                 backend=backend_name,
+                pid=os.getpid(),
+                extra={
+                    "supervisor": {
+                        "pid": os.getpid(),
+                        "instance_id": self._instance_id,
+                    }
+                },
</code_context>
<issue_to_address>
**question (bug_risk):** Clarify whether the stored `pid` is meant to be the worker/job pid or the supervisor process pid.

In `start()`, both the `pid` passed to `create_job` and `extra["supervisor"]["pid"]` use `os.getpid()`, so both currently store the supervisor’s pid. If any code later treats `record.pid` as the job/worker subprocess pid (e.g. for monitoring or cleanup), this mismatch could cause subtle bugs. Please either rename the field to make ownership explicit (e.g. `supervisor_pid`) or ensure `pid` consistently refers to the same process type across the codebase.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/agy_mcp/supervisor.py Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52c6eb9114

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agy_mcp/supervisor.py Outdated
if owner.get("instance_id") == current_instance_id:
return False
pid = owner.get("pid")
return isinstance(pid, int) and _pid_exists(pid)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard against PID reuse before trusting foreign owners

When a supervisor exits before finalizing a job, the OS can later reuse its PID for an unrelated process; with this check, a different supervisor will see os.kill(pid, 0) succeed and keep the stale record in running forever instead of reconciling it. This affects stores that are polled after a supervisor crash/restart on a busy host, so the owner metadata needs an additional identity check such as process start time/boot id (or another non-reusable token) before treating the recorded owner as live.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds supervisor ownership metadata to persisted job records so stale-job reconciliation does not incorrectly fail jobs owned by another live supervisor.

Changes:

  • Persists supervisor PID and instance ID when creating jobs.
  • Skips reconciliation for running jobs owned by a foreign live supervisor.
  • Adds tests for owner-aware reconciliation and session-store metadata round-tripping.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/agy_mcp/supervisor.py Adds supervisor instance IDs, persists owner metadata, and guards reconciliation with a PID liveness check.
src/agy_mcp/session_store.py Extends create_job to persist optional pid and extra metadata.
tests/test_supervisor.py Adds tests for live/dead foreign supervisor ownership behavior.
tests/test_session_store.py Verifies pid and supervisor metadata persist through create/get round trip.

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

Comment thread src/agy_mcp/supervisor.py Outdated
Comment on lines +737 to +739
return isinstance(pid, int) and _pid_exists(pid)


Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

No issues found across 4 files

Re-trigger cubic

Change-Id: Id1eb24c1664d0cab79965179dab1cbbbbea010de
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb4477543b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agy_mcp/supervisor.py
return None
try:
result = subprocess.run(
["ps", "-o", "lstart=", "-p", str(pid)],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use a timezone-stable process signature

When two supervisors share a store but run with different TZ environments, ps -o lstart= formats the same process start time in the caller's local timezone; I checked locally that TZ=UTC ps -o lstart= -p $$ and TZ=America/Los_Angeles ... produce different hours for the same PID. In that case a second supervisor polling a live foreign job compares a different string, treats the owner PID as reused, and finalizes the still-running job as failed.

Useful? React with 👍 / 👎.

Change-Id: Iaf6394ae5ea5a148f13cdb3059112a418ff8c01d
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c854b3ce8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agy_mcp/supervisor.py
return None
try:
result = subprocess.run(
["ps", "-o", "lstart=", "-p", str(pid)],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use a higher-resolution process identity

When a stale supervisor PID is recycled within the same second, this signature still matches because ps -o lstart contains only whole-second precision (I checked ps -o lstart= -p $$, which prints a value like Wed May 27 09:14:40 2026). The new evidence relative to the earlier PID-reuse comment is that this commit’s identity token is that second-resolution lstart string, so a fast PID reuse in a busy container/host can still make _pid_matches_start_signature return true and leave the stale job running indefinitely; use a non-reusable or higher-resolution identity such as /proc/<pid>/stat start ticks plus boot id where available.

Useful? React with 👍 / 👎.

Change-Id: I6827605a7d0b0c779999ca63f7711dfb2d44061b
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73161a7adf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agy_mcp/supervisor.py
if pid <= 0:
return False
try:
os.kill(pid, 0)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Stop using os.kill(pid, 0) on Windows

When this package runs on Windows (which the docs advertise as supported), a second supervisor polling a foreign running job without a usable start signature reaches this liveness probe; Python’s Windows os.kill implementation treats any signal other than CTRL_C/CTRL_BREAK as TerminateProcess, so os.kill(pid, 0) can terminate the live owner supervisor instead of just checking whether it exists. Use a non-destructive Windows liveness check such as opening the process and querying its exit code (or otherwise gate this POSIX probe to POSIX platforms).

Useful? React with 👍 / 👎.

Change-Id: If2e16fbde1cabae8e05bfdbad6701a54d79ec7fc
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7d57758b63

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agy_mcp/supervisor.py
return False
try:
kernel32 = ctypes.windll.kernel32
handle = kernel32.OpenProcess(0x1000, False, pid)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve HANDLE width in Windows PID probe

On 64-bit Windows, ctypes.windll defaults function return types to 32-bit c_int, but OpenProcess returns a pointer-sized HANDLE. When a valid handle has high bits set, this truncates the value passed to GetExitCodeProcess/CloseHandle, so _windows_pid_exists() can report a live foreign supervisor as missing and status() will finalize that still-running job as failed; set the OpenProcess restype/argtypes with ctypes.wintypes.HANDLE/DWORD before using the handle.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/agy_mcp/supervisor.py (1)

470-485: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Avoid blocking foreign-owner probes while holding the supervisor lock.

Line [470] holds self._lock, and Line [484] can trigger process probes that may block (including subprocess.run(..., timeout=1)). This can stall start()/cancel() and other status calls behind a single global lock under load.

🤖 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/agy_mcp/supervisor.py` around lines 470 - 485, The supervisor currently
holds self._lock while calling a potentially blocking foreign-owner probe via
_owned_by_foreign_live_supervisor(fresh, self._instance_id), which can stall
other operations; fix by minimizing lock scope: inside the with self._lock
block, read and cache handle, fresh = self.store.get_job(job_id) and any simple
flags (e.g., fresh.status and an indicator that ownership must be probed) then
release the lock and only then call the blocking probe function
_owned_by_foreign_live_supervisor(fresh, self._instance_id); after the probe
re-acquire the lock if you need to make a final decision or return a consistent
_public_record(fresh) (or re-read via store.get_job if necessary) so no
long-running blocking call happens while holding self._lock.
🤖 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/agy_mcp/supervisor.py`:
- Around line 829-849: _windows_pid_exists currently calls
kernel32.OpenProcess/GetExitCodeProcess/CloseHandle without declaring ctypes
signatures, which can truncate HANDLEs on 64-bit Windows; fix by importing
ctypes.wintypes and explicitly setting kernel32.OpenProcess.argtypes =
(wintypes.DWORD, wintypes.BOOL, wintypes.DWORD) and kernel32.OpenProcess.restype
= wintypes.HANDLE, kernel32.GetExitCodeProcess.argtypes = (wintypes.HANDLE,
ctypes.POINTER(wintypes.DWORD)) and restype = wintypes.BOOL, and
kernel32.CloseHandle.argtypes = (wintypes.HANDLE,) and restype = wintypes.BOOL
in the _windows_pid_exists function before calling these APIs (also treat a
returned handle of 0/NULL as failure and use a wintypes.DWORD for exit_code).

---

Outside diff comments:
In `@src/agy_mcp/supervisor.py`:
- Around line 470-485: The supervisor currently holds self._lock while calling a
potentially blocking foreign-owner probe via
_owned_by_foreign_live_supervisor(fresh, self._instance_id), which can stall
other operations; fix by minimizing lock scope: inside the with self._lock
block, read and cache handle, fresh = self.store.get_job(job_id) and any simple
flags (e.g., fresh.status and an indicator that ownership must be probed) then
release the lock and only then call the blocking probe function
_owned_by_foreign_live_supervisor(fresh, self._instance_id); after the probe
re-acquire the lock if you need to make a final decision or return a consistent
_public_record(fresh) (or re-read via store.get_job if necessary) so no
long-running blocking call happens while holding self._lock.
🪄 Autofix (Beta)

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

Run ID: 35dd33d0-caed-4dcf-8bcc-83c69d6f01ca

📥 Commits

Reviewing files that changed from the base of the PR and between 6d619b0 and 7d57758.

📒 Files selected for processing (4)
  • src/agy_mcp/session_store.py
  • src/agy_mcp/supervisor.py
  • tests/test_session_store.py
  • tests/test_supervisor.py

Comment thread src/agy_mcp/supervisor.py
Comment on lines +829 to +849
def _windows_pid_exists(pid: int) -> bool:
"""Return whether ``pid`` exists on Windows without signalling it."""

try:
import ctypes
except ImportError:
return False
try:
kernel32 = ctypes.windll.kernel32
handle = kernel32.OpenProcess(0x1000, False, pid)
if not handle:
return False
try:
exit_code = ctypes.c_ulong()
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
return False
return exit_code.value == 259
finally:
kernel32.CloseHandle(handle)
except (AttributeError, OSError):
return False
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify current Windows PID probe interop setup.
# Expected: WinAPI calls appear without explicit argtypes/restype declarations.
rg -n -C4 'def _windows_pid_exists|OpenProcess|GetExitCodeProcess|CloseHandle|ctypes\.windll|ctypes\.WinDLL|argtypes|restype' src/agy_mcp/supervisor.py

Repository: Boulea7/agy-mcp

Length of output: 882


🏁 Script executed:

#!/bin/bash
rg -n 'argtypes|restype|ctypes\.WinDLL|wintypes' src/agy_mcp/supervisor.py

Repository: Boulea7/agy-mcp

Length of output: 41


Define explicit ctypes WinAPI signatures to avoid HANDLE truncation on Windows.

_windows_pid_exists calls OpenProcess / GetExitCodeProcess / CloseHandle via ctypes.windll.kernel32 without setting argtypes/restype. On 64-bit Windows, ctypes’ default restype/argument conversions can truncate HANDLE values, causing false negatives or invalid handle use.

Suggested fix
 def _windows_pid_exists(pid: int) -> bool:
     """Return whether ``pid`` exists on Windows without signalling it."""
 
     try:
-        import ctypes
+        import ctypes
+        from ctypes import wintypes
     except ImportError:
         return False
     try:
-        kernel32 = ctypes.windll.kernel32
-        handle = kernel32.OpenProcess(0x1000, False, pid)
+        kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
+        kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
+        kernel32.OpenProcess.restype = wintypes.HANDLE
+        kernel32.GetExitCodeProcess.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]
+        kernel32.GetExitCodeProcess.restype = wintypes.BOOL
+        kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
+        kernel32.CloseHandle.restype = wintypes.BOOL
+
+        handle = kernel32.OpenProcess(0x1000, False, pid)
         if not handle:
             return False
         try:
-            exit_code = ctypes.c_ulong()
+            exit_code = wintypes.DWORD()
             if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
                 return False
             return exit_code.value == 259
         finally:
             kernel32.CloseHandle(handle)
📝 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
def _windows_pid_exists(pid: int) -> bool:
"""Return whether ``pid`` exists on Windows without signalling it."""
try:
import ctypes
except ImportError:
return False
try:
kernel32 = ctypes.windll.kernel32
handle = kernel32.OpenProcess(0x1000, False, pid)
if not handle:
return False
try:
exit_code = ctypes.c_ulong()
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
return False
return exit_code.value == 259
finally:
kernel32.CloseHandle(handle)
except (AttributeError, OSError):
return False
def _windows_pid_exists(pid: int) -> bool:
"""Return whether ``pid`` exists on Windows without signalling it."""
try:
import ctypes
from ctypes import wintypes
except ImportError:
return False
try:
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
kernel32.OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
kernel32.OpenProcess.restype = wintypes.HANDLE
kernel32.GetExitCodeProcess.argtypes = [wintypes.HANDLE, ctypes.POINTER(wintypes.DWORD)]
kernel32.GetExitCodeProcess.restype = wintypes.BOOL
kernel32.CloseHandle.argtypes = [wintypes.HANDLE]
kernel32.CloseHandle.restype = wintypes.BOOL
handle = kernel32.OpenProcess(0x1000, False, pid)
if not handle:
return False
try:
exit_code = wintypes.DWORD()
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
return False
return exit_code.value == 259
finally:
kernel32.CloseHandle(handle)
except (AttributeError, OSError):
return False
🤖 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/agy_mcp/supervisor.py` around lines 829 - 849, _windows_pid_exists
currently calls kernel32.OpenProcess/GetExitCodeProcess/CloseHandle without
declaring ctypes signatures, which can truncate HANDLEs on 64-bit Windows; fix
by importing ctypes.wintypes and explicitly setting
kernel32.OpenProcess.argtypes = (wintypes.DWORD, wintypes.BOOL, wintypes.DWORD)
and kernel32.OpenProcess.restype = wintypes.HANDLE,
kernel32.GetExitCodeProcess.argtypes = (wintypes.HANDLE,
ctypes.POINTER(wintypes.DWORD)) and restype = wintypes.BOOL, and
kernel32.CloseHandle.argtypes = (wintypes.HANDLE,) and restype = wintypes.BOOL
in the _windows_pid_exists function before calling these APIs (also treat a
returned handle of 0/NULL as failure and use a wintypes.DWORD for exit_code).

Copy link
Copy Markdown

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

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 (changes from recent commits).

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="src/agy_mcp/supervisor.py">

<violation number="1" location="src/agy_mcp/supervisor.py:839">
P1: `_windows_pid_exists` conflates permission failures with non-existent PIDs, which can misclassify live processes as dead during stale-job reconciliation on Windows.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/agy_mcp/supervisor.py
try:
kernel32 = ctypes.windll.kernel32
handle = kernel32.OpenProcess(0x1000, False, pid)
if not handle:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: _windows_pid_exists conflates permission failures with non-existent PIDs, which can misclassify live processes as dead during stale-job reconciliation on Windows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/agy_mcp/supervisor.py, line 839:

<comment>`_windows_pid_exists` conflates permission failures with non-existent PIDs, which can misclassify live processes as dead during stale-job reconciliation on Windows.</comment>

<file context>
@@ -818,6 +826,29 @@ def _pid_exists(pid: int) -> bool:
+    try:
+        kernel32 = ctypes.windll.kernel32
+        handle = kernel32.OpenProcess(0x1000, False, pid)
+        if not handle:
+            return False
+        try:
</file context>

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.

2 participants