Skip to content

fix: serialize tensors with per-channel transport mode in RPC pre-passes#6

Open
pollockjj wants to merge 29 commits into
developfrom
fix/per-channel-tensor-transport-mode
Open

fix: serialize tensors with per-channel transport mode in RPC pre-passes#6
pollockjj wants to merge 29 commits into
developfrom
fix/per-channel-tensor-transport-mode

Conversation

@pollockjj

Copy link
Copy Markdown
Owner

Summary

Outbound tensors could be encoded with the wrong transport mode when a host runs
multiple isolated extensions with different tensor-transport modes, crashing
torch-free sealed/conda workers mid-execution.

Root cause

_prepare_for_rpc_impl (RPC argument pre-pass) and _serialize_for_isolation_impl
both looked up the process-global SerializerRegistry "Tensor" serializer before
the torch.Tensor branch. register_tensor_serializer binds a single mode and runs
once per extension on the global singleton (last-writer-wins). When a shared_memory
(share_torch=True) extension is co-resident with a json (sealed/conda) extension,
the global Tensor mode can be pinned to shared_memory, so outbound tensors were
emitted as a shared-memory TensorRef even on json channels. A torch-free sealed
worker then raised KeyError('data') decoding the TensorRef, killing its RPC recv
thread (Socket closed or incomplete length header).

Fix

Handle torch.Tensor before the registry lookup in both pre-passes so tensors defer
to the per-channel transport (JSONSocketTransport._json_default), which already
serializes per channel via serialize_tensor(mode=self._tensor_transport). The
process-global registry mode no longer leaks across channels. No public API change;
no behavior change for single-mode hosts.

Verification

  • New tests/test_tensor_transport_mode_isolation.py covers both pre-passes — fails on develop, passes with the fix.
  • Targeted serialization / rpc / host unit tests pass (ruff lint + format clean).
  • End-to-end (real downstream): a conda/JAX sealed-worker workflow co-loaded with a share_torch=True extension fails before / completes after; downstream isolation suite 111/111 green.

Scope

Two-function reorder + one regression test. Internal change in _internal/.

pollockjj added 28 commits April 7, 2026 19:21
…l absent

When pyisolate is installed from PyPI (non-editable), _pyisolate_source_path()
resolves to site-packages/ which has no pyproject.toml, causing pixi install
to fail. Now checks for pyproject.toml existence and falls back to a
version-pinned PyPI dependency matching the uv code path pattern.
Remove 3 non-production ][ DIAG: log statements from the RPC
deserialization path that printed on every type-tagged object crossing
the wire.
Add _internal/pixi_provisioner.py that downloads, caches, and
integrity-verifies the pixi binary from prefix-dev GitHub releases.
Supports Linux x86_64/aarch64, macOS x86_64/arm64, Windows x86_64.
Version pinned to 0.67.0. 11 tests covering platform detection,
checksum verification, cache hit, and full download path.
Replace shutil.which("pixi") checks in environment.py and
environment_conda.py with ensure_pixi() from pixi_provisioner.
The conda backend now auto-downloads, caches, and verifies the pixi
binary on first use. Users never need to install pixi manually.
…ilable

_deserialize_json_tensor previously returned raw dicts when torch was
absent. Now falls back to numpy arrays. Also registers TensorValue
deserializer in the child bootstrap for sealed workers without torch.
Add TestPlatformCoverage class with parametrized tests for all 5
platform combinations, Windows binary name (pixi.exe), and URL
construction verification against GitHub release naming convention.
Sealed workers never run adapter.register_serializers() because the
adapter can't be rehydrated. This caused ndarray results to be wrapped
as RemoteObjectHandle instead of crossing the wire as inline data.

Register ndarray as a data_type serializer directly in
SealedNodeExtension.__init__. The serializer converts ndarray to
TensorValue dict format, which the host's existing TensorValue
fast-path in rpc_transports.py reconstructs as torch.Tensor.
Add extra_index_urls field to ExtensionConfig and plumb through
install_dependencies() as --extra-index-url args to uv pip install.
Enables packages from non-standard PyPI indexes (e.g., fbxsdkpy from
gitlab.inria.fr) to resolve during isolated venv provisioning.
Copilot AI review requested due to automatic review settings June 5, 2026 21:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Fixes a cross-extension isolation bug where outbound torch.Tensor arguments could be pre-serialized using a process-global, mode-bound "Tensor" serializer (last-writer-wins), causing JSON channels to accidentally emit shared-memory TensorRef payloads that torch-free sealed/conda workers can’t decode.

Changes:

  • Reorders _prepare_for_rpc_impl to handle torch.Tensor before registry serializer lookup, deferring tensor encoding to the per-channel JSON transport mode.
  • Reorders _serialize_for_isolation_impl similarly to prevent registry-pinned tensor modes from leaking across channels.
  • Adds a regression test covering both pre-pass paths with a registry whose Tensor serializer is pinned to shared_memory.

Reviewed changes

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

File Description
tests/test_tensor_transport_mode_isolation.py Adds regression coverage ensuring tensors are deferred to per-channel transport rather than registry mode.
pyisolate/_internal/rpc_serialization.py Moves torch tensor handling ahead of registry serializer resolution in the RPC pre-pass.
pyisolate/_internal/model_serialization.py Moves torch tensor handling ahead of registry serializer resolution in isolation serialization pre-pass.

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

Comment on lines +1 to +15
from __future__ import annotations

import pytest


def _shared_memory_registry():
"""A registry whose Tensor serializer is pinned to shared_memory, simulating a
co-resident share_torch extension that registered last on the process-global
SerializerRegistry singleton."""
from pyisolate._internal.serialization_registry import SerializerRegistry
from pyisolate._internal.tensor_serializer import register_tensor_serializer

registry = SerializerRegistry()
register_tensor_serializer(registry, mode="shared_memory")
return registry
Repository owner deleted a comment from qa-agent-seveneves Bot Jun 5, 2026
Repository owner deleted a comment from qa-agent-seveneves Bot Jun 5, 2026
@qa-agent-seveneves

Copy link
Copy Markdown

Codex Review -- Round 1 -- KICKOFF

Runner: scripts/run_codex_review.py
PR: #6
Reviewer model: codex review (configured codex CLI default)
Kickoff timestamp (UTC): 2026-06-05T23:40:16Z

Prompt sent to codex review

Review the latest state of https://github.com/pollockjj/pyisolate/pull/6. Output findings using strict P0-P3 priority tags ([P0], [P1], [P2], [P3]) at the start of each finding title. Surface P0/P1/P2 -- these block merge in this repo. Surface P3 with that tag explicitly. Do not surface style nits, formatting, typos, or pre-existing bugs. End your output with a single line either 'APPROVE' (no P0/P1/P2 found) or 'REQUEST_CHANGES' (one or more P0/P1/P2 found). Output nothing else after that line.

Awaiting codex output. The result will be posted as a separate comment on this PR when codex exits.

@qa-agent-seveneves

Copy link
Copy Markdown

Codex Review -- Round 1 -- RESULT

PR: #6
Reviewer model: codex review (configured codex CLI default)
Result timestamp (UTC): 2026-06-05T23:42:14Z
Codex exit code: 0
Duration (s): 116.0
Detected verdict: REQUEST_CHANGES

Prompt sent to codex review

Review the latest state of https://github.com/pollockjj/pyisolate/pull/6. Output findings using strict P0-P3 priority tags ([P0], [P1], [P2], [P3]) at the start of each finding title. Surface P0/P1/P2 -- these block merge in this repo. Surface P3 with that tag explicitly. Do not surface style nits, formatting, typos, or pre-existing bugs. End your output with a single line either 'APPROVE' (no P0/P1/P2 found) or 'REQUEST_CHANGES' (one or more P0/P1/P2 found). Output nothing else after that line.

Verbatim codex review output

This PR was reviewed using codex review with the prompt above and produced the following output:


The implementation change may be functionally sound, but the newly added test file introduces a mypy error in the repository's required CI type-check job, so the patch is not merge-ready.

Review comment:

  • [P2] Add the return type required by CI mypy — /tmp/mydev-prr/tests/test_tensor_transport_mode_isolation.py:6-6
    With this new helper present, the CI lint-and-type job runs mypy pyisolate tests under disallow_untyped_defs = true, so _shared_memory_registry fails with Function is missing a return type annotation [no-untyped-def] and blocks the PR before tests can complete. Add an explicit return annotation for the registry helper so the type-check job passes.

Codex runner stderr (non-evidence)

OpenAI Codex v0.130.0
--------
workdir: /tmp/mydev-prr
model: gpt-5.5
provider: openai
approval: never
sandbox: workspace-write [workdir, /tmp, /home/johnj/.codex/memories]
reasoning effort: high
reasoning summaries: none
session id: 019e9a28-b012-79b3-9ead-2c75f58bd906
--------
user
Review the latest state of https://github.com/pollockjj/pyisolate/pull/6. Output findings using strict P0-P3 priority tags ([P0], [P1], [P2], [P3]) at the start of each finding title. Surface P0/P1/P2 -- these block merge in this repo. Surface P3 with that tag explicitly. Do not surface style nits, formatting, typos, or pre-existing bugs. End your output with a single line either 'APPROVE' (no P0/P1/P2 found) or 'REQUEST_CHANGES' (one or more P0/P1/P2 found). Output nothing else after that line.
mcp: codex_apps/github_fetch_pr started
mcp: codex_apps/github_fetch_pr (completed)
exec
/bin/bash -lc pwd in /tmp/mydev-prr
 succeeded in 0ms:
/tmp/mydev-prr

exec
/bin/bash -lc ls in /tmp/mydev-prr
 succeeded in 0ms:
AGENTS.md
BCS043_HV GaN MMCI vs AccoTest KLM.xlsx
CLAUDE.md
GEMINI.md
MELIAN.md
Untitled.canvas
WHEELS_SELF_HOSTED_RUNNER_PASSDOWN.md
bcs_webform.html
claude
comfyui_manual_8188.log
debug
dev_blue.code-workspace
dev_yellow.code-workspace
equivalence_methods.md
github_issues
infineon_bcs
issue_87_2_p2_1_api.json
issue_87_2_p2_1_dup_api.json
mcp_servers.json
mymultigpu
myprocess_isolation
mywheels
orchestrator
orchestrator_requirements.md
persistent_btree.py
scripts
skills
tests
tools
tools_register.md
trellis2_final_workflow.json

exec
/bin/bash -lc 'find . -maxdepth 2 -type d -name pyisolate' in /tmp/mydev-prr
 succeeded in 0ms:
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (failed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: codex_apps/github_get_commit_combined_status started
mcp: codex_apps/github_get_commit_combined_status (completed)
mcp: codex_apps/github_fetch_commit_workflow_runs started
mcp: codex_apps/github_fetch_commit_workflow_runs (completed)
mcp: codex_apps/github_fetch_workflow_run_jobs started
mcp: codex_apps/github_fetch_workflow_run_jobs (completed)
mcp: codex_apps/github_fetch_workflow_job_logs started
mcp: codex_apps/github_fetch_workflow_job_logs (completed)
mcp: codex_apps/github_fetch_workflow_job_logs started
mcp: codex_apps/github_fetch_workflow_job_logs (completed)
mcp: github/search_code started
mcp: github/search_code (completed)
mcp: github/search_code started
mcp: github/search_code (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
codex
The implementation change may be functionally sound, but the newly added test file introduces a mypy error in the repository's required CI type-check job, so the patch is not merge-ready.

Review comment:

- [P2] Add the return type required by CI mypy — /tmp/mydev-prr/tests/test_tensor_transport_mode_isolation.py:6-6
  With this new helper present, the CI `lint-and-type` job runs `mypy pyisolate tests` under `disallow_untyped_defs = true`, so `_shared_memory_registry` fails with `Function is missing a return type annotation [no-untyped-def]` and blocks the PR before tests can complete. Add an explicit return annotation for the registry helper so the type-check job passes.

Both _prepare_for_rpc_impl (the RPC argument pre-pass) and
_serialize_for_isolation_impl looked up the process-global SerializerRegistry
"Tensor" serializer before the torch.Tensor branch. That serializer is bound to a
single mode at registration, and register_tensor_serializer runs once per
extension on the global singleton (last-writer-wins).

When a host runs a shared_memory (share_torch=True) extension alongside a json
(sealed/conda) extension, the global Tensor mode can be pinned to shared_memory,
so outbound tensors were encoded as a shared-memory TensorRef even on json
channels. A torch-free sealed worker then raised KeyError('data') decoding the
TensorRef, killing its RPC recv thread ("Socket closed or incomplete length
header").

Handle torch.Tensor before the registry lookup in both pre-passes so tensors are
deferred to the per-channel transport (JSONSocketTransport._json_default), which
already serializes per channel via serialize_tensor(mode=self._tensor_transport).
The global registry mode no longer leaks across channels.

Adds tests/test_tensor_transport_mode_isolation.py covering both pre-passes.
@pollockjj pollockjj force-pushed the fix/per-channel-tensor-transport-mode branch from c828a89 to f389b54 Compare June 5, 2026 23:45
@pollockjj pollockjj requested a review from Copilot June 5, 2026 23:45
@qa-agent-seveneves

Copy link
Copy Markdown

Codex Review -- Round 2 -- KICKOFF

Runner: scripts/run_codex_review.py
PR: #6
Reviewer model: codex review (configured codex CLI default)
Kickoff timestamp (UTC): 2026-06-05T23:45:40Z

Prompt sent to codex review

Review the latest state of https://github.com/pollockjj/pyisolate/pull/6. Output findings using strict P0-P3 priority tags ([P0], [P1], [P2], [P3]) at the start of each finding title. Surface P0/P1/P2 -- these block merge in this repo. Surface P3 with that tag explicitly. Do not surface style nits, formatting, typos, or pre-existing bugs. End your output with a single line either 'APPROVE' (no P0/P1/P2 found) or 'REQUEST_CHANGES' (one or more P0/P1/P2 found). Output nothing else after that line.

Awaiting codex output. The result will be posted as a separate comment on this PR when codex exits.

@qa-agent-seveneves

Copy link
Copy Markdown

Codex Review -- Round 2 -- RESULT

PR: #6
Reviewer model: codex review (configured codex CLI default)
Result timestamp (UTC): 2026-06-05T23:47:48Z
Codex exit code: 0
Duration (s): 126.3
Detected verdict: UNKNOWN

Prompt sent to codex review

Review the latest state of https://github.com/pollockjj/pyisolate/pull/6. Output findings using strict P0-P3 priority tags ([P0], [P1], [P2], [P3]) at the start of each finding title. Surface P0/P1/P2 -- these block merge in this repo. Surface P3 with that tag explicitly. Do not surface style nits, formatting, typos, or pre-existing bugs. End your output with a single line either 'APPROVE' (no P0/P1/P2 found) or 'REQUEST_CHANGES' (one or more P0/P1/P2 found). Output nothing else after that line.

Verbatim codex review output

This PR was reviewed using codex review with the prompt above and produced the following output:


No merge-blocking or advisory findings were identified in the latest PR diff. The earlier mypy issue is already fixed in the current head, and observed CI pytest failures are outside the changed code.


Codex runner stderr (non-evidence)

OpenAI Codex v0.130.0
--------
workdir: /tmp/mydev-prr
model: gpt-5.5
provider: openai
approval: never
sandbox: workspace-write [workdir, /tmp, /home/johnj/.codex/memories]
reasoning effort: high
reasoning summaries: none
session id: 019e9a2d-a1a1-7572-8240-dc256c2f3edd
--------
user
Review the latest state of https://github.com/pollockjj/pyisolate/pull/6. Output findings using strict P0-P3 priority tags ([P0], [P1], [P2], [P3]) at the start of each finding title. Surface P0/P1/P2 -- these block merge in this repo. Surface P3 with that tag explicitly. Do not surface style nits, formatting, typos, or pre-existing bugs. End your output with a single line either 'APPROVE' (no P0/P1/P2 found) or 'REQUEST_CHANGES' (one or more P0/P1/P2 found). Output nothing else after that line.
exec
/bin/bash -lc pwd in /tmp/mydev-prr
 succeeded in 0ms:
/tmp/mydev-prr

exec
/bin/bash -lc ls in /tmp/mydev-prr
 succeeded in 0ms:
AGENTS.md
BCS043_HV GaN MMCI vs AccoTest KLM.xlsx
CLAUDE.md
GEMINI.md
MELIAN.md
Untitled.canvas
WHEELS_SELF_HOSTED_RUNNER_PASSDOWN.md
bcs_webform.html
claude
comfyui_manual_8188.log
debug
dev_blue.code-workspace
dev_yellow.code-workspace
equivalence_methods.md
github_issues
infineon_bcs
issue_87_2_p2_1_api.json
issue_87_2_p2_1_dup_api.json
mcp_servers.json
mymultigpu
myprocess_isolation
mywheels
orchestrator
orchestrator_requirements.md
persistent_btree.py
scripts
skills
tests
tools
tools_register.md
trellis2_final_workflow.json

exec
/bin/bash -lc 'cat debug/reset_gpu.py' in /tmp/mydev-prr
 succeeded in 0ms:
#!python
from __future__ import annotations

import sys
from pathlib import Path

if __package__ in {None, ""}:
    sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from debug.harness_runtime import base_parser, detect_platform_name, render_launch_spec, run_render_only, run_reset_gpu


def main(argv: list[str] | None = None) -> int:
    parser = base_parser("Copy-exact port of debug/reset_gpu.sh")
    args = parser.parse_args(argv)
    spec = render_launch_spec(
        entrypoint="reset_gpu.py",
        mode="reset_gpu",
        platform_name=detect_platform_name(args.platform),
        workflows=[],
    )
    if args.render_spec:
        return run_render_only(spec)
    return run_reset_gpu(spec)


if __name__ == "__main__":
    raise SystemExit(main())

exec
/bin/bash -lc 'cat debug/run_debug.py' in /tmp/mydev-prr
 succeeded in 0ms:
#!python
from __future__ import annotations

import sys
from pathlib import Path

if __package__ in {None, ""}:
    sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from debug.harness_runtime import base_parser, detect_platform_name, render_launch_spec, run_debug_orchestrator, run_render_only


def main(argv: list[str] | None = None) -> int:
    parser = base_parser("Copy-exact port of debug/run_debug.sh")
    parser.add_argument("--workflow", action="append", default=[])
    parser.add_argument("--extra_arguments", default="")
    parser.add_argument("--copilot_prompt", default="")
    parser.add_argument("--forensic_shm", action="store_true")
    parser.add_argument("--legacy_stability_monitor", action="store_true")
    parser.add_argument("--sandbox_mode", action="store_true")
    args = parser.parse_args(argv)

    spec = render_launch_spec(
        entrypoint="run_debug.py",
        mode="run_debug",
        platform_name=detect_platform_name(args.platform),
        workflows=args.workflow,
        extra_arguments=args.extra_arguments,
        forensic_shm=args.forensic_shm,
        legacy_stability_monitor=args.legacy_stability_monitor,
        sandbox_mode=args.sandbox_mode,
    )
    if args.render_spec:
        return run_render_only(spec)
    return run_debug_orchestrator(spec)


if __name__ == "__main__":
    raise SystemExit(main())

exec
/bin/bash -lc 'cat debug/harness_runtime.py' in /tmp/mydev-prr
 succeeded in 0ms:
from __future__ import annotations

import argparse
import json
import os
import re
from dataclasses import asdict, dataclass
from pathlib import Path, PurePosixPath, PureWindowsPath
from typing import Iterable


WINDOWS = "windows"
LINUX = "linux"


def _path_string(platform_name: str, *parts: str) -> str:
    if platform_name == WINDOWS:
        return str(PureWindowsPath(*parts))
    return str(PurePosixPath(*parts))


def _platform_python(platform_name: str, comfy_root: str) -> str:
    if platform_name == WINDOWS:
        return _path_string(platform_name, comfy_root, ".venv", "Scripts", "python.exe")
    return _path_string(platform_name, comfy_root, ".venv", "bin", "python")


@dataclass(frozen=True)
class HarnessPaths:
    workspace_root: str
    comfy_root: str
    artifacts_root: str
    pre_launch_log: str
    post_launch_log: str
    delta_log: str
    launch_log: str
    comfy_python: str


@dataclass(frozen=True)
class LaunchSpec:
    entrypoint: str
    platform: str
    mode: str
    paths: HarnessPaths
    comfy_command: list[str]
    helper_commands: list[list[str]]
    workflow_files: list[str]
    env: dict[str, str]

    def to_json(self) -> str:
        return json.dumps(asdict(self), indent=2, sort_keys=True)


def detect_workspace_root() -> Path:
    return Path(__file__).resolve().parent.parent


def detect_comfy_root(workspace_root: Path) -> Path:
    override = os.environ.get("COMFY_ROOT")
    if override:
        return Path(override).resolve()
    return (workspace_root.parent / "ComfyUI").resolve()


def detect_cuda_slot(workspace_root: Path) -> int:
    match = re.search(r'dev_cuda_(\d+)', str(workspace_root))
    if match:
        return int(match.group(1))
    return 0


def slot_port(cuda_slot: int) -> int:
    """cuda_0 → 8188, cuda_1 → 8200, cuda_N → 8200 + (N-1)."""
    if cuda_slot == 0:
        return 8188
    return 8200 + (cuda_slot - 1)


def cuda_visible_devices(cuda_slot: int) -> str:
    override = os.environ.get("COMFY_CUDA_VISIBLE_DEVICES")
    if override:
        return override
    return str(cuda_slot)


def detect_platform_name(explicit: str | None = None) -> str:
    if explicit:
        return explicit.lower()
    return WINDOWS if os.name == "nt" else LINUX


def build_paths(platform_name: str, workspace_root: Path, comfy_root: Path) -> HarnessPaths:
    if platform_name == WINDOWS:
        workspace = str(PureWindowsPath(str(workspace_root)))
        comfy = str(PureWindowsPath(str(comfy_root)))
        artifacts = _path_string(platform_name, workspace, "temp_artifacts")
    else:
        workspace = str(PurePosixPath(*workspace_root.parts[1:])) if workspace_root.drive else str(PurePosixPath(str(workspace_root)))
        comfy = str(PurePosixPath(*comfy_root.parts[1:])) if comfy_root.drive else str(PurePosixPath(str(comfy_root)))
        workspace = f"/{workspace}" if workspace_root.drive else workspace
        comfy = f"/{comfy}" if comfy_root.drive else comfy
        artifacts = _path_string(platform_name, workspace, "temp_artifacts")

    if platform_name == WINDOWS:
        launch_log = _path_string(platform_name, artifacts, "comfyui_launch.log")
    else:
        launch_log = "/tmp/comfyui_launch.log"
    return HarnessPaths(
        workspace_root=workspace,
        comfy_root=comfy,
        artifacts_root=artifacts,
        pre_launch_log=_path_string(platform_name, artifacts, "comfyui_pre_launch.log"),
        post_launch_log=_path_string(platform_name, artifacts, "comfyui_post_launch.log"),
        delta_log=_path_string(platform_name, artifacts, "comfyui_run_delta.log"),
        launch_log=launch_log,
        comfy_python=_platform_python(platform_name, comfy),
    )


def parse_extra_arguments(extra_arguments: str) -> list[str]:
    if not extra_arguments:
        return []
    return [token for token in extra_arguments.split(" ") if token]


def normalize_workflows(workflows: Iterable[str]) -> list[str]:
    return list(workflows)


def render_launch_spec(
    *,
    entrypoint: str,
    mode: str,
    platform_name: str,
    workflows: Iterable[str],
    extra_arguments: str = "",
    forensic_shm: bool = False,
    legacy_stability_monitor: bool = False,
    sandbox_mode: bool = False,
) -> LaunchSpec:
    workspace_root = detect_workspace_root()
    comfy_root = detect_

...truncated 141925 bytes (full stderr in round2_codex_stderr.txt)...

lt: {codex_result_comment_url}
   Copilot review: {copilot_review_url}

   ### Codex findings ({len(codex_blocking)} blocking, {len(codex_advisory)} advisory)

   | Finding | Tag | Decision | Rationale |
   |:--|:--|:--|:--|
   | ... | ... | ... | ... |

   ### Copilot inline comments ({len(copilot_comments)})

   | Comment | Path:Line | Suggestion? | Decision | Rationale |
   |:--|:--|:--|:--|:--|
   | ... | ... | ... | ... | ... |

   ### Round outcome

   - Blocking codex findings to address: K
   - Copilot comments to ACCEPT: A
   - Copilot comments to MODIFY: M
   - Copilot comments to REJECT: R (rationale posted inline)
   - Disputed codex findings: D (rationale posted on PR)

   {one of:}
   - "Round N complete; transitioning to OWNER_ACTION to apply fixes."
   - "Round N complete; no blockers and no Copilot comments require code change. Transitioning to READY."

Post via scripts/run_tdd_post.py comment {OWNER/REPO} {ISSUE_NUMBER} {BODY_FILE}.
7. Determine next state:

  • If len(codex_blocking_addressed_with_commits) == len(codex_blocking_total) AND every Copilot comment has a recorded decision (ACCEPT applied, MODIFY applied, or REJECT replied inline) → READY.
  • Else → OWNER_ACTION.
  1. Update state file with evaluation_comment_id and the per-comment/per-finding decisions. Commit + push.

Exit: OWNER_ACTION or READY.


State OWNER_ACTION

Entry: Round N evaluation produced blockers requiring code change.

Actions:

  1. The slice LLM applies every ACCEPT and MODIFY decision: edits files, runs the slice's relevant tests locally to confirm the fix doesn't regress, commits each fix with the prescribed commit message format.
  2. The slice LLM posts inline replies for every REJECT and every DISPUTE.
  3. The slice LLM pushes all commits to the PR head branch.
  4. The push event is the trigger for the next round. The slice LLM re-invokes /pr-review with the same args. The INIT state will detect that the head SHA has changed, increment the round, and transition to KICKOFF.

This state never persists in the state file — it is the active-work state. The state file remains at EVALUATE until the slice LLM transitions back to KICKOFF via re-invocation. (Alternative implementations may persist a state=OWNER_ACTION flag if the loop is later lifted into a Python FSM that needs to track active work explicitly; for the LLM-driven version, the state file shows EVALUATE until the next KICKOFF transition.)

Exit: KICKOFF for round N+1 (after slice LLM re-invokes the skill post-push).


State READY

Entry: Round N evaluation produced zero codex blockers and every Copilot comment has a recorded decision.

Actions:

  1. Compose the READY notice:
    ## PR READY for human merge — Round N
    
    PR: {PR_URL}
    Head SHA: {head_sha_at_kickoff}
    Total rounds: N
    Final codex verdict: APPROVE
    Final Copilot verdict: clean (every comment addressed or rejected with rationale)
    
    This PR has been through the autonomous review loop and is ready for the user's final quality-gate review and merge decision. The skill does not auto-merge.
  2. Post the READY notice to the issue via scripts/run_tdd_post.py.
  3. Update state file: state=READY, record ready_comment_id. Commit + push.
  4. Exit skill with status: "READY at round N. User merge required."

Exit: Skill terminates. User performs final review and merge.


Skill Args

/pr-review {OWNER/REPO} {ISSUE_NUMBER} {PR_URL}

Or shorthand if the current branch is issue_{ISSUE_NUMBER} and a single open PR is associated:

/pr-review {ISSUE_NUMBER}

The skill resolves the rest from gh pr list --head issue_{ISSUE_NUMBER} and the issue's ## Affected Repositories table.


Loop Termination Conditions

  • Normal: State reaches READY → skill exits, user merges.
  • Forced exit: User invokes /pr-review --abort {ISSUE_NUMBER} — skill records state=ABORTED and exits without further action.
  • Round budget: The state file may include an optional max_rounds (default unset = unlimited). If round > max_rounds, the skill posts a budget-exceeded notice and exits without flipping to READY. User decides whether to extend the budget or take over.

Anti-Patterns

  1. Auto-applying Copilot suggestions — forbidden. Slice LLM judges per comment.
  2. Silent rejections — every REJECT must be posted as an inline reply with rationale.
  3. Bypassing the state file — every transition writes the state file before posting or pushing.
  4. Posting through gh issue comment — use the runners.
  5. Polling foreverWAIT_FOR_BOTH_REVIEWS exits reentrantly after a sensible deadline (default 30 minutes); the slice LLM resumes on next invocation.
  6. Auto-flipping draft PR to ready — forbidden. User flips after merge review.
  7. Skipping the kickoff post — the kickoff comment is mandatory for transparency. The codex runner posts it before spawning codex.
  8. Disputing codex without inline rationale — DISPUTE requires a public reply on the PR thread referencing a concrete project fact.

Quick State Transition Diagram

INIT
  └→ (load state file or initialize)
  └→ (head_sha changed? round++)
  └→ KICKOFF

KICKOFF
  └→ post codex kickoff
  └→ run codex review
  └→ post codex result
  └→ request Copilot review
  └→ WAIT_FOR_BOTH_REVIEWS

WAIT_FOR_BOTH_REVIEWS
  └→ (Copilot review present at head_sha?)
       NO  → (poll or exit reentrantly)
       YES → EVALUATE

EVALUATE
  └→ parse codex findings (P0/P1/P2/P3 + APPROVE/REQUEST_CHANGES)
  └→ parse Copilot inline comments + suggestions
  └→ slice LLM records per-finding + per-comment decisions
  └→ post evaluation report
  └→ (any blockers OR comments needing code change?)
       YES → OWNER_ACTION
       NO  → READY

OWNER_ACTION
  └→ slice LLM commits ACCEPTs + MODIFYs
  └→ slice LLM posts inline replies for REJECTs + DISPUTEs
  └→ slice LLM pushes
  └→ slice LLM re-invokes /pr-review
  └→ INIT (round N+1)

READY
  └→ post READY notice
  └→ exit
  └→ user performs final merge

exec
/bin/bash -lc 'ls pyisolate' in /tmp/mydev-prr
exited 2 in 0ms:
ls: cannot access 'pyisolate': No such file or directory

mcp: codex_apps/github_fetch_pr started
mcp: codex_apps/github_fetch_pr (completed)
mcp: codex_apps/github_fetch_file started
mcp: codex_apps/github_fetch_file (completed)
mcp: codex_apps/github_fetch_file started
mcp: codex_apps/github_fetch_file (completed)
mcp: codex_apps/github_fetch_file started
mcp: codex_apps/github_fetch_file (completed)
mcp: codex_apps/github_fetch_file started
mcp: codex_apps/github_fetch_file (completed)
mcp: codex_apps/github_fetch_file started
mcp: codex_apps/github_fetch_file (completed)
mcp: codex_apps/github_fetch_file started
mcp: codex_apps/github_fetch_file (failed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: codex_apps/github_fetch_file started
mcp: codex_apps/github_fetch_file (completed)
mcp: codex_apps/github_fetch_file started
mcp: codex_apps/github_fetch_file (completed)
mcp: codex_apps/github_get_commit_combined_status started
mcp: codex_apps/github_get_commit_combined_status (completed)
mcp: codex_apps/github_fetch_commit_workflow_runs started
mcp: codex_apps/github_fetch_commit_workflow_runs (completed)
mcp: codex_apps/github_fetch_workflow_run_jobs started
mcp: codex_apps/github_fetch_workflow_run_jobs (completed)
mcp: codex_apps/github_fetch_workflow_run_jobs started
mcp: codex_apps/github_fetch_workflow_run_jobs (completed)
mcp: codex_apps/github_fetch_workflow_run_jobs started
mcp: codex_apps/github_fetch_workflow_run_jobs (completed)
mcp: codex_apps/github_fetch_workflow_job_logs started
mcp: codex_apps/github_fetch_workflow_job_logs (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
mcp: github/get_file_contents started
mcp: github/get_file_contents (completed)
codex
No merge-blocking or advisory findings were identified in the latest PR diff. The earlier mypy issue is already fixed in the current head, and observed CI pytest failures are outside the changed code.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

@pollockjj pollockjj force-pushed the develop branch 2 times, most recently from 0e15c5f to 2fda457 Compare June 7, 2026 13:25
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