relay-codex 0.0.0-stage → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,109 @@
1
+ """Validated local preferences; no automatic model fallback or extra billing."""
2
+
3
+ from pathlib import Path
4
+ import copy
5
+ import json
6
+ import os
7
+ import tomllib
8
+
9
+ DEFAULTS = {
10
+ "retry_interval": 300,
11
+ "retry_attempts": 3,
12
+ "unknown_reset_interval": 300,
13
+ "watchdog_interval": 30,
14
+ "idle_warning_after": 120,
15
+ "routing": "balanced",
16
+ "notifications": False,
17
+ "fallback_models": [],
18
+ "preferred_accounts": [],
19
+ "max_sessions_per_account": 0,
20
+ "reservation_seconds": 120,
21
+ "history_days": 30,
22
+ "event_limit": 2000,
23
+ "offline_retry": 30,
24
+ "background_on_disconnect": False,
25
+ "broker_idle_seconds": 600,
26
+ }
27
+
28
+
29
+ def validate(values):
30
+ unknown = set(values) - set(DEFAULTS)
31
+ if unknown:
32
+ raise ValueError("Unknown settings: " + ", ".join(sorted(unknown)))
33
+ for key, value in values.items():
34
+ expected = DEFAULTS[key]
35
+ if isinstance(expected, bool):
36
+ if not isinstance(value, bool):
37
+ raise ValueError(f"{key} must be true or false")
38
+ elif isinstance(expected, int):
39
+ if isinstance(value, bool) or not isinstance(value, int) or value < 0:
40
+ raise ValueError(f"{key} must be a nonnegative integer")
41
+ if key not in {"max_sessions_per_account", "retry_attempts"} and value < 1:
42
+ raise ValueError(f"{key} must be positive")
43
+ elif isinstance(expected, list):
44
+ if not isinstance(value, list) or not all(
45
+ isinstance(v, str) for v in value
46
+ ):
47
+ raise ValueError(f"{key} must be a list of strings")
48
+ if values.get("routing", "balanced") not in {
49
+ "balanced",
50
+ "quota",
51
+ "priority",
52
+ "round-robin",
53
+ }:
54
+ raise ValueError("routing must be balanced, quota, priority, or round-robin")
55
+ if values.get("reservation_seconds", 120) < 30:
56
+ raise ValueError(
57
+ "reservation_seconds must be at least 30 to allow heartbeat renewal"
58
+ )
59
+ return values
60
+
61
+
62
+ def load(state, project=None, profile=None):
63
+ result = copy.deepcopy(DEFAULTS)
64
+ for path in [
65
+ state / "settings.json",
66
+ Path(project) / ".codex-accounts.toml" if project else None,
67
+ Path(project) / ".relay.toml" if project else None,
68
+ ]:
69
+ if not path or not path.exists():
70
+ continue
71
+ data = (
72
+ json.loads(path.read_text())
73
+ if path.suffix == ".json"
74
+ else tomllib.loads(path.read_text())
75
+ )
76
+ result.update(validate(data.get("settings", {})))
77
+ if profile:
78
+ result.update(validate(data.get("profiles", {}).get(profile, {})))
79
+ return result
80
+
81
+
82
+ def edit(state, key, raw, profile=None):
83
+ try:
84
+ value = json.loads(raw)
85
+ except ValueError:
86
+ value = raw
87
+ validate({key: value})
88
+ path = state / "settings.json"
89
+ data = json.loads(path.read_text()) if path.exists() else {}
90
+ target = (
91
+ data.setdefault("profiles", {}).setdefault(profile, {})
92
+ if profile
93
+ else data.setdefault("settings", {})
94
+ )
95
+ target[key] = value
96
+ atomic_json(path, data)
97
+
98
+
99
+ def atomic_json(path, data):
100
+ path = Path(path)
101
+ path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
102
+ temporary = path.with_name(path.name + f".{os.getpid()}.tmp")
103
+ fd = os.open(temporary, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
104
+ with os.fdopen(fd, "w") as stream:
105
+ json.dump(data, stream, indent=2)
106
+ stream.write("\n")
107
+ stream.flush()
108
+ os.fsync(stream.fileno())
109
+ temporary.replace(path)
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env python3
2
+ """Offline test double for native JSONL transport. Never contacts OpenAI."""
3
+
4
+ import json
5
+ import os
6
+ from pathlib import Path
7
+ import sys
8
+ import threading
9
+ import time
10
+ import uuid
11
+ import fcntl
12
+
13
+ home = Path(os.environ.get("CODEX_HOME", "."))
14
+ home.mkdir(parents=True, exist_ok=True)
15
+ state_file = home / "fake-history.json"
16
+ guard = threading.RLock()
17
+ out = threading.Lock()
18
+
19
+
20
+ def send(value):
21
+ with out:
22
+ print(json.dumps(value), flush=True)
23
+
24
+
25
+ def change(operation):
26
+ with guard:
27
+ with (home / "fake-history.lock").open("a") as lock:
28
+ fcntl.flock(lock, fcntl.LOCK_EX)
29
+ data = (
30
+ json.loads(state_file.read_text())
31
+ if state_file.exists()
32
+ else {"threads": {}, "calls": []}
33
+ )
34
+ result = operation(data)
35
+ temporary = state_file.with_suffix(".tmp")
36
+ temporary.write_text(json.dumps(data))
37
+ temporary.replace(state_file)
38
+ return result
39
+
40
+
41
+ def finish(thread, turn, text, mode):
42
+ status = "failed" if mode in {"quota", "capacity"} else "completed"
43
+ error = (
44
+ {"message": "synthetic limit", "codexErrorInfo": "usageLimitExceeded"}
45
+ if mode == "quota"
46
+ else None
47
+ )
48
+ if mode == "capacity":
49
+ error = {
50
+ "message": "Selected model is at capacity. Please try a different model.",
51
+ "codexErrorInfo": "policyCapacityUnavailable",
52
+ }
53
+
54
+ def apply(data):
55
+ value = data["threads"][thread]
56
+ value["status"] = {"type": "idle"}
57
+ value["turns"][-1].update(status=status, error=error)
58
+ return value["turns"][-1].copy()
59
+
60
+ value = change(apply)
61
+ if status == "completed":
62
+ send(
63
+ {
64
+ "method": "item/completed",
65
+ "params": {
66
+ "threadId": thread,
67
+ "item": {
68
+ "id": "reply-" + turn,
69
+ "type": "agentMessage",
70
+ "text": "FAKE_OK",
71
+ },
72
+ },
73
+ }
74
+ )
75
+ if mode == "capacity":
76
+ send(
77
+ {
78
+ "method": "error",
79
+ "params": {
80
+ "threadId": thread,
81
+ "turnId": turn,
82
+ "error": error,
83
+ "willRetry": False,
84
+ },
85
+ }
86
+ )
87
+ send({"method": "turn/completed", "params": {"threadId": thread, "turn": value}})
88
+ send(
89
+ {
90
+ "method": "thread/status/changed",
91
+ "params": {"threadId": thread, "status": {"type": "idle"}},
92
+ }
93
+ )
94
+
95
+
96
+ if "--version" in sys.argv:
97
+ print("codex-cli 0.166.0")
98
+ raise SystemExit()
99
+ if "--help" in sys.argv:
100
+ print("--remote --remote-auth-token-env")
101
+ raise SystemExit()
102
+
103
+ for raw in sys.stdin:
104
+ try:
105
+ request = json.loads(raw)
106
+ method = request.get("method")
107
+ params = request.get("params") or {}
108
+ ident = request.get("id")
109
+ if ident is None:
110
+ continue
111
+ change(
112
+ lambda d: d["calls"].append(
113
+ {"method": method, "thread": params.get("threadId")}
114
+ )
115
+ )
116
+ result = {}
117
+ if method == "initialize":
118
+ result = {
119
+ "userAgent": "fake/1",
120
+ "codexHome": str(home),
121
+ "platformFamily": "unix",
122
+ "platformOs": "macos",
123
+ }
124
+ elif method == "account/read":
125
+ result = {"account": {"type": "chatgpt", "planType": "plus"}}
126
+ elif method == "account/login/start":
127
+ result = {"type": params["type"]}
128
+ elif method == "account/rateLimits/read":
129
+ result = {
130
+ "ordinaryUsageAllowed": True,
131
+ "rateLimits": {
132
+ "primary": {"usedPercent": 20, "resetsAt": int(time.time()) + 3600}
133
+ },
134
+ }
135
+ elif method in ("thread/start", "thread/fork"):
136
+ thread = str(uuid.uuid4())
137
+ value = {
138
+ "id": thread,
139
+ "cwd": params.get("cwd", str(home)),
140
+ "model": params.get("model", "fake-model"),
141
+ "canAcceptDirectInput": True,
142
+ "status": {"type": "idle"},
143
+ "turns": [],
144
+ }
145
+ change(lambda d: d["threads"].update({thread: value}))
146
+ result = {
147
+ "thread": value,
148
+ "model": value["model"],
149
+ "cwd": value["cwd"],
150
+ "approvalPolicy": params.get("approvalPolicy", "never"),
151
+ "sandbox": params.get("sandbox", "read-only"),
152
+ }
153
+ send({"method": "thread/started", "params": {"thread": value}})
154
+ elif method == "thread/resume":
155
+ value = change(lambda d: d["threads"].get(params["threadId"]))
156
+ if not value:
157
+ raise ValueError("no thread")
158
+ if (
159
+ value.get("runtime") != os.getpid()
160
+ and value.get("status", {}).get("type") == "active"
161
+ ):
162
+ value["status"] = {"type": "idle"}
163
+ if value["turns"]:
164
+ value["turns"][-1].update(status="interrupted", error=None)
165
+ value.update({k: v for k, v in params.items() if k in {"model", "cwd"}})
166
+ change(lambda d: d["threads"].update({value["id"]: value}))
167
+ result = {"thread": value, "model": value["model"], "cwd": value["cwd"]}
168
+ elif method == "thread/read":
169
+ result = {"thread": change(lambda d: d["threads"][params["threadId"]])}
170
+ elif method == "thread/turns/list":
171
+ result = {
172
+ "data": change(
173
+ lambda d: list(reversed(d["threads"][params["threadId"]]["turns"]))[
174
+ : params.get("limit", 1)
175
+ ]
176
+ )
177
+ }
178
+ elif method == "thread/loaded/list":
179
+ result = {"data": change(lambda d: list(d["threads"]))}
180
+ elif method == "turn/start":
181
+ thread = params["threadId"]
182
+ turn = str(uuid.uuid4())
183
+ value = {"id": turn, "status": "inProgress", "items": [], "error": None}
184
+
185
+ def begin(data):
186
+ data["threads"][thread]["turns"].append(value)
187
+ data["threads"][thread]["status"] = {
188
+ "type": "active",
189
+ "activeFlags": [],
190
+ }
191
+ data["threads"][thread]["runtime"] = os.getpid()
192
+ return sum(c["method"] == "turn/start" for c in data["calls"])
193
+
194
+ count = change(begin)
195
+ fault = (
196
+ json.loads((home / "fault.json").read_text())
197
+ if (home / "fault.json").exists()
198
+ else {}
199
+ )
200
+ mode = "quota" if count <= fault.get("quota_turns", 0) else "ok"
201
+ if count <= fault.get("capacity_turns", 0):
202
+ mode = "capacity"
203
+ if not fault.get("drop_ack"):
204
+ send({"id": ident, "result": {"turn": value}})
205
+ send(
206
+ {
207
+ "method": "turn/started",
208
+ "params": {"threadId": thread, "turn": value},
209
+ }
210
+ )
211
+ if fault.get("external"):
212
+ send(
213
+ {
214
+ "method": "item/started",
215
+ "params": {
216
+ "threadId": thread,
217
+ "item": {"id": "external1", "type": "commandExecution"},
218
+ },
219
+ }
220
+ )
221
+ if fault.get("die_once") and count == 1:
222
+ os._exit(11)
223
+ timer = threading.Timer(
224
+ float(fault.get("delay", 0.02)), finish, args=(thread, turn, "", mode)
225
+ )
226
+ timer.daemon = True
227
+ timer.start()
228
+ continue
229
+ elif method == "turn/interrupt":
230
+ result = {}
231
+ send({"id": ident, "result": result})
232
+ except Exception:
233
+ send(
234
+ {
235
+ "id": request.get("id"),
236
+ "error": {"code": -32600, "message": "Synthetic request rejected"},
237
+ }
238
+ )
@@ -0,0 +1,217 @@
1
+ """Capacity recovery and audit regressions, without real accounts or requests."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import io
6
+ from pathlib import Path
7
+ import tempfile
8
+ import time
9
+ from types import SimpleNamespace
10
+ import unittest
11
+ from unittest.mock import patch
12
+
13
+ from pool import CAPACITY_MESSAGE, Pool, failure_kind
14
+ from router import Controller
15
+ from test_router import FakeAccount, FakeBackend, FakeClient, completed
16
+ from test_pool import Clock
17
+ from test_services import wait_for
18
+
19
+
20
+ class CapacityClassifierTests(unittest.TestCase):
21
+ def test_structured_and_exact_legacy_capacity_errors(self):
22
+ self.assertEqual(failure_kind("policyCapacityUnavailable"), "capacity")
23
+ for code in (
24
+ None,
25
+ "other",
26
+ "policyCapacityUnavailable",
27
+ "serverOverloaded",
28
+ "flexUnavailable",
29
+ ):
30
+ for message in (CAPACITY_MESSAGE, "■ " + CAPACITY_MESSAGE):
31
+ self.assertEqual(failure_kind(code, message), "capacity")
32
+
33
+ def test_policy_denials_are_not_reclassified_by_message(self):
34
+ for code in (
35
+ "cyberPolicy",
36
+ "misalignmentPolicyViolation",
37
+ "badRequest",
38
+ "sandboxError",
39
+ ):
40
+ self.assertIsNone(failure_kind(code, CAPACITY_MESSAGE))
41
+ self.assertIsNone(failure_kind("other", "This request mentions capacity"))
42
+
43
+
44
+ class CapacityRecoveryTests(unittest.IsolatedAsyncioTestCase):
45
+ async def capacity(self, turn):
46
+ message = completed(code="policyCapacityUnavailable", turn=turn)
47
+ message["params"]["turn"]["error"]["message"] = CAPACITY_MESSAGE
48
+ await self.c.event(message)
49
+
50
+ async def asyncSetUp(self):
51
+ self.tmp = tempfile.TemporaryDirectory()
52
+ self.clock = Clock()
53
+ self.pool = Pool(Path(self.tmp.name), clock=self.clock)
54
+ self.backend = FakeBackend()
55
+ self.c = Controller(
56
+ [FakeAccount("one"), FakeAccount("two")],
57
+ self.backend,
58
+ pool=self.pool,
59
+ clock=self.clock,
60
+ sleep=self.clock.sleep,
61
+ report=lambda _: None,
62
+ )
63
+ self.c.client = FakeClient()
64
+ self.c.last_turn["thread-1"] = {"threadId": "thread-1", "model": "chosen-model"}
65
+
66
+ async def asyncTearDown(self):
67
+ if self.c.recovery:
68
+ self.c.recovery.cancel()
69
+ await asyncio.gather(self.c.recovery, return_exceptions=True)
70
+ self.pool.close()
71
+ self.tmp.cleanup()
72
+
73
+ async def test_capacity_retries_every_five_minutes_without_cycling_accounts_or_models(
74
+ self,
75
+ ):
76
+ for i in range(4):
77
+ await self.capacity(str(i))
78
+ await self.c.recovery
79
+ self.assertEqual(self.clock.waits, [300, 300, 300, 300])
80
+ self.assertEqual(self.c.index, 0)
81
+ self.assertEqual(self.c.model_for("thread-1"), "chosen-model")
82
+ starts = [p for m, p in self.backend.calls if m == "turn/start"]
83
+ self.assertEqual(len(starts), 4)
84
+ self.assertTrue(all(p["threadId"] == "thread-1" for p in starts))
85
+ self.assertFalse(
86
+ any(
87
+ m in {"thread/resume", "account/login/start"}
88
+ for m, _ in self.backend.calls
89
+ )
90
+ )
91
+ with self.pool.db() as db:
92
+ reasons = [r[0] for r in db.execute("SELECT reason FROM blocks")]
93
+ self.assertFalse(reasons)
94
+
95
+ async def test_legacy_message_continues_saved_thread(self):
96
+ message = completed(code="other")
97
+ message["params"]["turn"]["error"]["message"] = CAPACITY_MESSAGE
98
+ await self.c.event(message)
99
+ await self.c.recovery
100
+ self.assertEqual(self.clock.waits, [300])
101
+ self.assertTrue(any(m == "turn/start" for m, _ in self.backend.calls))
102
+
103
+ async def test_final_policy_failure_cancels_an_earlier_capacity_retry(self):
104
+ await self.c.event(
105
+ {
106
+ "method": "error",
107
+ "params": {
108
+ "threadId": "thread-1",
109
+ "turnId": "turn-1",
110
+ "willRetry": False,
111
+ "error": {
112
+ "codexErrorInfo": "policyCapacityUnavailable",
113
+ "message": CAPACITY_MESSAGE,
114
+ },
115
+ },
116
+ }
117
+ )
118
+ await self.c.event(completed(code="cyberPolicy"))
119
+ await asyncio.gather(self.c.recovery, return_exceptions=True)
120
+ self.assertFalse(any(m == "turn/start" for m, _ in self.backend.calls))
121
+
122
+ async def test_native_error_and_final_turn_do_not_schedule_two_retries(self):
123
+ params = {
124
+ "threadId": "thread-1",
125
+ "turnId": "turn-1",
126
+ "error": {
127
+ "codexErrorInfo": "policyCapacityUnavailable",
128
+ "message": CAPACITY_MESSAGE,
129
+ },
130
+ }
131
+ await self.c.event({"method": "error", "params": {**params, "willRetry": True}})
132
+ self.assertIsNone(self.c.recovery)
133
+ await self.c.event(
134
+ {"method": "error", "params": {**params, "willRetry": False}}
135
+ )
136
+ await self.capacity("turn-1")
137
+ await self.c.recovery
138
+ self.assertEqual(self.clock.waits, [300])
139
+ self.assertEqual(sum(m == "turn/start" for m, _ in self.backend.calls), 1)
140
+
141
+ async def test_capacity_fallback_only_uses_explicitly_configured_model(self):
142
+ self.c.preferences["fallback_models"] = ["chosen-fallback"]
143
+ for i in range(4):
144
+ await self.capacity(str(i))
145
+ await self.c.recovery
146
+ self.assertEqual(self.c.model_for("thread-1"), "chosen-fallback")
147
+
148
+ async def test_restored_capacity_retry_retains_its_deadline(self):
149
+ await self.c.restore_recovery(
150
+ "thread-1",
151
+ {
152
+ "accepted": True,
153
+ "kind": "capacity",
154
+ "next_wake": 1500,
155
+ "account_label": "one",
156
+ "episode": {"account": 0, "start": 1000, "attempt": 1},
157
+ },
158
+ )
159
+ self.assertEqual(self.clock.waits, [500])
160
+ self.assertEqual(sum(m == "turn/start" for m, _ in self.backend.calls), 1)
161
+
162
+ async def test_missing_continuation_baseline_cannot_accept_old_completed_turn_as_new(
163
+ self,
164
+ ):
165
+ async def lost(method, params=None, **kwargs):
166
+ if method == "turn/start":
167
+ raise asyncio.TimeoutError()
168
+ if method == "thread/turns/list":
169
+ return {"data": [{"id": "old", "status": "completed"}]}
170
+ if method == "thread/read":
171
+ return {"thread": {"status": {"type": "idle"}}}
172
+ return {}
173
+
174
+ self.backend.call = lost
175
+ await self.c.continue_thread("thread-1", 0)
176
+ self.assertIn("thread-1", self.c.paused_uncertain)
177
+ self.assertFalse(self.c.pending_intents["thread-1"]["accepted"])
178
+
179
+ async def test_unrelated_wake_does_not_shorten_service_retry(self):
180
+ self.c.clock = time.time
181
+ self.c.sleep = asyncio.sleep
182
+ self.c.recovery = asyncio.create_task(self.c.recover("thread-1", 0, "capacity"))
183
+ await wait_for(lambda: self.c.episodes.get("thread-1", {}).get("attempt") == 1)
184
+ self.c.wake.set()
185
+ await asyncio.sleep(0.03)
186
+ self.assertFalse(self.c.recovery.done())
187
+ self.assertFalse(any(m == "turn/start" for m, _ in self.backend.calls))
188
+ # Explicit Retry is allowed to override the saved timer.
189
+ await self.c.control("retry", {})
190
+ await asyncio.wait_for(self.c.recovery, 1)
191
+ self.assertTrue(any(m == "turn/start" for m, _ in self.backend.calls))
192
+
193
+
194
+ class DoctorRegressionTests(unittest.IsolatedAsyncioTestCase):
195
+ async def test_incompatible_native_runtime_is_failure_exit(self):
196
+ import commands
197
+
198
+ with tempfile.TemporaryDirectory() as folder:
199
+ state = Path(folder)
200
+ Pool(state).close()
201
+
202
+ async def absent(*args, **kw):
203
+ raise OSError()
204
+
205
+ with (
206
+ patch("compatibility.check", side_effect=ValueError()),
207
+ patch("broker.exchange", absent),
208
+ contextlib.redirect_stdout(io.StringIO()),
209
+ ):
210
+ code = await commands.doctor(
211
+ SimpleNamespace(
212
+ restore_backup=False, self_test=False, backup=False
213
+ ),
214
+ state,
215
+ "fake",
216
+ )
217
+ self.assertEqual(code, 1)