Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ persisted queue row. Their existing separate identity contracts remain intact.
The broader cross-agent work in #180 remains open; this development feature is
not shipped in v1.0.4 and adds no automatic fallback or resubmission.

The 2026-10-15 review of the queued-input paths in native Codex [0.162.1](https://github.com/openai/codex/tree/092d3acd6bec3e3a14bdc7e7a2810ab628ab759d/codex-rs) did not identify a supported precondition that atomically checks the original writer incarnation. In the inspected source, the lock-acquisition path does not write an incarnation token, a thread's `sessionId` survives resume, and `thread/queue/add` accepts a thread, input and a client message ID rather than a generation precondition. The inspected `turn/steer` path checks `expectedTurnId` under the active-turn lock; session-peer does not use that turn-specific check as a writer generation. These findings are limited to the pinned paths, not a proof about every native API or future version. Writer PID, start-time and lock rechecks, queue IDs, `canAcceptDirectInput` and `sessionId` do not establish the required original-incarnation guarantee; session-peer continues to report Codex `targetGeneration` as `unsupported`. #180 remains open for a separately validated native contract.

`--message TEXT` (short form `-m`) names the text sent to the destination.
`--output-format text|json` selects the command result format, not the message
format. It is available on `list`, `send`, `doctor`, and `update`; the default is
Expand Down
2 changes: 1 addition & 1 deletion docs/i18n-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"docs/ai-assistant-guide.md": "9624ec96b92fb688fae3be3c94cb3564c77985091be451fbf1f056a1af9b9d9c",
"docs/antigravity.md": "f2b5122a0ce6e6fb5bd4d8ed50ce2c8bd2dc9c98ec8ce328405cf7c2cd24229d",
"docs/architecture/agent-transports.md": "01a4e3942e87acb476595859f2e37f9653ea1e3bc19ebc68dced1adf20a6966f",
"docs/cli-reference.md": "4f5e228e914ba1b1a7277e27688288a93cc9fa1dad261fa0511a895663dd6d5d",
"docs/cli-reference.md": "a1aae5b881f4aa939c995d7bc06819d04f808da3b31fd0d5da66c817e1b47e82",
"docs/compatibility-v1.md": "7e551aad37a369faff7e370728376763efb54c2c9e9cfd8556a0c19aabc3d103",
"docs/contracts/handoff-v1.md": "46a953010c49214bd8f969412f26713384fbe09a0ba78ebf0fef75da40d0d889",
"docs/diagnostics.md": "0949e53612eeab2cd7c7bbda498e14d71e9cb1b1189aa7722030cb8702cd8481",
Expand Down
2 changes: 2 additions & 0 deletions docs/ja/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Codex キュー、Antigravity とペアリング済み機器には、この受
Codex の書き込みプロセスを再確認しても、後からキューを消費するプロセスを原子的に制限できません。
#180 全体は未完了です。v1.0.4 の公開機能ではなく、自動フォールバックや再送も追加しません。

2026-10-15 に native Codex [0.162.1](https://github.com/openai/codex/tree/092d3acd6bec3e3a14bdc7e7a2810ab628ab759d/codex-rs) のキュー入力経路を確認しましたが、元の書き込みプロセスの世代を原子的に検査する、利用可能な前提条件は見つかりませんでした。確認したソースでは、ロック取得経路は世代トークンを記録せず、スレッドの `sessionId` は再開後も維持され、`thread/queue/add` は世代の前提条件ではなくスレッド・入力・クライアントメッセージ ID を受け取ります。確認した `turn/steer` 経路はアクティブターンのロック内で `expectedTurnId` を検査しますが、session-peer はこのターン固有の検査を書き込みプロセスの世代として使いません。これは固定した経路に限った調査であり、すべての native API や将来のバージョンで機能が存在しないことを証明しません。PID・開始時刻・ロックの再確認、キュー ID、`canAcceptDirectInput`、`sessionId` は必要な元世代の保証を確立しないため、Codex の `targetGeneration` は引き続き `unsupported` と報告します。#180 は別途検証された native 契約のため未完了のままです。

コマンドの詳細な動作、インストールオプション、トランスポートの制限、および過去の検証結果について以下に説明します。
機械consumerは[v1互換性契約](compatibility-v1.md)にも従ってください。

Expand Down
2 changes: 2 additions & 0 deletions docs/ko/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Codex 큐, Antigravity와 페어링 기기는 이 수신 프로세스 세대 보
Codex의 작성자 재확인만으로 나중에 큐를 소비하는 작성자를 원자적으로 제한할 수는 없습니다.
#180의 전체 작업은 아직 열려 있습니다. v1.0.4에 발행된 기능이 아니며 자동 대체 전송·재전송도 없습니다.

2026-10-15 native Codex [0.162.1](https://github.com/openai/codex/tree/092d3acd6bec3e3a14bdc7e7a2810ab628ab759d/codex-rs)의 큐 입력 경로를 검토했지만, 원래 쓰기 프로세스의 세대를 원자적으로 검사하는 지원 가능한 전제조건은 찾지 못했습니다. 확인한 소스에서 잠금 획득 경로는 세대 토큰을 기록하지 않고, 스레드의 `sessionId`는 재개 후에도 유지되며, `thread/queue/add`는 세대 전제조건 대신 스레드·입력·클라이언트 메시지 ID를 받습니다. 확인한 `turn/steer` 경로는 활성 턴 잠금 안에서 `expectedTurnId`를 검사하지만, session-peer는 이 턴 전용 검사를 쓰기 프로세스의 세대로 사용하지 않습니다. 이는 고정된 경로에 한정된 조사이며 모든 native API나 향후 버전의 기능 부재를 증명하지 않습니다. PID·시작 시각·잠금 재검사, 큐 ID, `canAcceptDirectInput`, `sessionId`로는 필요한 원본 세대 보장을 확립할 수 없으므로 Codex `targetGeneration`은 계속 `unsupported`로 보고합니다. #180은 별도로 검증된 native 계약을 위해 열린 상태로 유지합니다.

명령어의 상세 동작, 설치 옵션, 전송 한계 및 과거 검증 내역은 아래와 같습니다.
기계 소비자는 [v1 호환성 계약](compatibility-v1.md)도 따라야 합니다.

Expand Down
2 changes: 2 additions & 0 deletions docs/zh-CN/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Codex 队列、Antigravity 和配对设备不具备此接收进程代次保证
wake 前拒绝,并保留各自既有契约。重新核实 Codex 写入进程,不能原子地限制之后消费
持久队列的进程。#180 的完整工作仍未完成;该功能尚未在 v1.0.4 发布,不会自动回退或重发。

2026-10-15 对 native Codex [0.162.1](https://github.com/openai/codex/tree/092d3acd6bec3e3a14bdc7e7a2810ab628ab759d/codex-rs) 的队列输入路径进行了审查,但未找到可用的、原子检查原始写入进程代次的前置条件。在已检查的源码中,获取锁的路径不写入代次令牌,线程的 `sessionId` 在恢复后保持不变,而 `thread/queue/add` 接受线程、输入和客户端消息 ID,而非代次前置条件。已检查的 `turn/steer` 路径在活动轮次锁内检查 `expectedTurnId`,但 session-peer 不将这种轮次专用检查用作写入进程代次。这些结论仅限于固定的源码路径,并不证明所有 native API 或未来版本都不存在该功能。PID、启动时间和锁的复查、队列 ID、`canAcceptDirectInput`、`sessionId` 不能确立所需的原始代次保证,因此 Codex 的 `targetGeneration` 继续报告为 `unsupported`。#180 保持未完成,以等待另行验证的 native 契约。

以下是详细的命令行为、安装选项、传输限制和历史验证记录。
机器consumer还应遵循[v1兼容性契约](compatibility-v1.md)。

Expand Down
192 changes: 192 additions & 0 deletions tests/codex/test_generation_unsupported.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""Negative fixtures: weak Codex evidence is never promoted to a generation.

The reviewed queued-input paths in native Codex 0.162.1 (rust-v0.162.1,
092d3acd6bec) did not establish a supported atomic original-incarnation
precondition. This is not a proof about every native API or future version.
These offline tests prove that
session-peer keeps Codex generation `unsupported` whatever weak evidence is
present. They are not evidence of native delivery or consumption.
"""
import argparse
import contextlib
import io
import json
import os
from pathlib import Path
import sqlite3
import subprocess
import tempfile
import unittest
from unittest import mock

import session_peer as peer
from tests.codex.support import THREAD, isolate_native_layers

CLAUDE_SHAPED = "tg1:" + "a" * 64


class CodexGenerationUnsupported(unittest.TestCase):
def setUp(self):
temp = tempfile.TemporaryDirectory()
self.addCleanup(temp.cleanup)
self.root = Path(temp.name).resolve()
environment = mock.patch.dict(os.environ, {
"HOME": str(self.root), "USERPROFILE": str(self.root),
"CODEX_HOME": str(self.root)}, clear=True)
environment.start()
self.addCleanup(environment.stop)
# Isolate both the older file-backed and newer forced-preference
# readers so these generation fixtures never inspect host MDM state.
for name in ("codex_system_config_paths", "codex_managed_preference_paths",
"codex_managed_preference_values"):
if hasattr(peer, name):
layer = mock.patch.object(peer, name, return_value=[])
layer.start()
self.addCleanup(layer.stop)

def invoke(self, *argv):
out = io.StringIO()
with contextlib.redirect_stdout(out):
code = peer.main([*argv, "--json", "--no-update-notice"])
return code, json.loads(out.getvalue())

def strongest_weak_row(self, **extra):
# Every field a caller might mistake for an incarnation guarantee.
return {"agent": "codex", "id": THREAD, "name": "worker", "status": "active",
"archived": False, "cwd": "/fixture", "codexHome": str(self.root),
"activity": "live_writer", "writerLock": "held", "ownerPid": 4242,
"ownerStable": True, "ownerStartTime": 1700000000.25,
"queueId": "01900000-0000-7000-8000-0000000000aa",
"clientUserMessageId": "01900000-0000-7000-8000-0000000000bb",
"canAcceptDirectInput": True, "sessionId": THREAD,
"queueRevision": 7, "expectedTurnId": "01900000-0000-7000-8000-0000000000cc",
**extra}

def test_listing_never_mints_codex_generation_from_writer_queue_or_session_evidence(self):
row = self.strongest_weak_row()
with mock.patch.object(peer.CodexAdapter, "list", return_value={
"sessions": [row], "discovery": {"status": "ok"}}), \
mock.patch.object(peer, "claude_generation") as minted:
code, result = self.invoke("list", "--agent", "codex", "--with-target-generation")
self.assertEqual(code, 0)
listed = result["sessions"][0]
self.assertIsNone(listed["targetGeneration"])
self.assertEqual(listed["generationStatus"], "unsupported")
minted.assert_not_called()

def test_saved_discovery_with_arbitrary_lock_content_stays_unsupported(self):
with contextlib.closing(sqlite3.connect(self.root / "state_5.sqlite")) as conn:
conn.execute("CREATE TABLE threads (id TEXT, title TEXT, cwd TEXT, updated_at INTEGER, archived INTEGER, rollout_path TEXT)")
conn.execute("INSERT INTO threads VALUES (?,?,?,?,?,?)",
(THREAD, "fixture", str(self.root), 1, 0, "unused"))
conn.commit()
locks = self.root / "thread-writer-locks"
locks.mkdir()
# Native writers leave the lock empty; any content is not a token either.
for content in (b"", b"pid=4242 generation=" + b"f" * 64, CLAUDE_SHAPED.encode()):
with self.subTest(content=content[:16]):
(locks / f"{THREAD}.lock").write_bytes(content)
# Saved-row listing does not call inspect_codex_writer. This
# case tests lock content only; stable writer fields are covered
# by the explicit adapter-row fixture above.
with mock.patch.object(peer, "claude_generation") as minted, \
mock.patch.object(peer, "inspect_codex_writer", wraps=peer.inspect_codex_writer) as inspected:
code, result = self.invoke("list", "--agent", "codex", "--codex-home",
str(self.root), "--with-target-generation")
self.assertEqual(code, 0)
rows = [row for row in result["sessions"] if row.get("id") == THREAD]
self.assertEqual(len(rows), 1)
self.assertIsNone(rows[0]["targetGeneration"])
self.assertEqual(rows[0]["generationStatus"], "unsupported")
minted.assert_not_called()
inspected.assert_not_called()

def test_changed_writer_rows_stay_unsupported_and_arbitrary_pins_are_refused(self):
# Two synthetic listings and a separate pin refusal, not a live race.
for owner in (4242, 4243):
with mock.patch.object(peer.CodexAdapter, "list", return_value={
"sessions": [self.strongest_weak_row(ownerPid=owner)],
"discovery": {"status": "ok"}}):
code, result = self.invoke("list", "--agent", "codex", "--with-target-generation")
self.assertEqual(code, 0)
self.assertIsNone(result["sessions"][0]["targetGeneration"])
for token in (CLAUDE_SHAPED, "tg1:" + "0" * 64):
for options in ([], ["--dry-run"], ["--wake"], ["--host", "test-host"]):
with self.subTest(token=token[:8], options=options), \
mock.patch.object(peer, "queue_codex") as queue, \
mock.patch.object(peer, "run_codex_wake") as wake, \
mock.patch.object(peer.SshTransport, "execute") as remote, \
mock.patch.object(peer, "tailscale_status") as network_discovery, \
mock.patch.object(peer.subprocess, "run") as child:
code, result = self.invoke("send", "--to", "codex:" + THREAD,
"--target-generation", token, "--message=x",
"--no-from", "--no-reply-to", *options)
self.assertEqual(code, 1)
self.assertEqual(result["reason"], "unsupported_target_generation")
self.assertIs(result["submitted"], False)
self.assertIs(result["retryAllowed"], False)
self.assertNotIn("lastSeenTarget", result)
for effect in (queue, wake, remote, child, network_discovery):
effect.assert_not_called()

def test_successful_queue_result_never_carries_a_generation(self):
# Exercise the real CLI -> queue_codex -> _queue_codex result producer.
# Only native execution, writer evidence and fixture-local prerequisites
# are replaced; a prebuilt application result would make this vacuous.
isolate_native_layers(self, self.root)
rollout = self.root / "rollout.jsonl"
rollout.write_text("{}\n", encoding="utf-8")
database = self.root / "state_5.sqlite"
with contextlib.closing(sqlite3.connect(database)) as conn:
conn.execute("CREATE TABLE threads (id TEXT, title TEXT, cwd TEXT, updated_at INTEGER, archived INTEGER, rollout_path TEXT)")
conn.execute("INSERT INTO threads VALUES (?,?,?,?,?,?)",
(THREAD, "fixture", str(self.root), 1, 0, str(rollout)))
conn.commit()
original_database = database.read_bytes()
queue_id = "01900000-0000-7000-8000-0000000000aa"
executable = str(self.root / "fixture-codex")
writer = {"activity": "live_writer", "writerLock": "held", "ownerPid": 4242,
"ownerStable": True, "ownerStartTime": "fixture-start",
"reason": "stable_live_writer"}
completed = subprocess.CompletedProcess([], 0,
"Queued message " + queue_id + " for thread " + THREAD + ".\n", "")
with mock.patch.object(peer, "known_codex_homes", return_value=[self.root]), \
mock.patch.object(peer, "inspect_codex_writer", return_value=writer), \
mock.patch.object(peer, "codex_executable", return_value=executable), \
mock.patch.object(peer.subprocess, "run", return_value=completed) as child, \
mock.patch.object(peer.subprocess, "Popen") as other_child:
code, result = self.invoke("send", "--to", "codex:" + THREAD, "--message=x",
"--codex-home", str(self.root), "--no-from", "--no-reply-to")
self.assertEqual(code, 0)
child.assert_called_once()
framed = peer.peer_delivery_message("x", "codex")
self.assertEqual(child.call_args.args[0],
[executable, "queue", "--thread", THREAD, "--message=" + framed])
self.assertIn("\n| x\nEND QUOTED PEER BODY", child.call_args.args[0][-1])
self.assertEqual(child.call_args.kwargs["cwd"], str(self.root))
self.assertEqual(child.call_args.kwargs["env"]["CODEX_HOME"], str(self.root))
if not peer.IS_WINDOWS:
self.assertEqual(child.call_args.kwargs["env"]["PWD"], str(self.root))
other_child.assert_not_called()
self.assertIs(result["ok"], True)
self.assertEqual(result["target"], {"agent": "codex", "id": THREAD})
self.assertEqual(result["status"], "queued")
self.assertIs(result["submitted"], True)
self.assertEqual(result["queueId"], queue_id)
self.assertNotIn("targetGeneration", result)
self.assertIs(result["consumptionConfirmed"], False)
self.assertEqual(database.read_bytes(), original_database)

def test_codex_adapter_contract_has_no_generation_capability(self):
adapter = peer.AGENTS.for_target("codex:" + THREAD)
args = argparse.Namespace(to="codex:" + THREAD, target_generation=CLAUDE_SHAPED,
wake=False, host=[])
with self.assertRaises(peer.CcPeerError) as caught:
adapter.validate_send(args, "x")
self.assertEqual(caught.exception.details["reason"], "unsupported_target_generation")
args.target_generation = None
adapter.validate_send(args, "x")


if __name__ == "__main__":
unittest.main()
Loading