relay-codex 0.5.0 → 0.5.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.
- package/docs/publishing.md +2 -2
- package/package.json +2 -2
- package/python/Menu.swift +1 -1
- package/python/commands.py +9 -2
- package/python/router.py +18 -6
- package/tests/test_audit.py +25 -0
package/docs/publishing.md
CHANGED
|
@@ -8,8 +8,8 @@ Friends can run `npx relay-codex` or install it with `npm install -g relay-codex
|
|
|
8
8
|
The GitHub repository is enough for friends to install or try Relay:
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
npm install -g github:blackspider-ops/relay#v0.5.
|
|
12
|
-
npx --yes github:blackspider-ops/relay#v0.5.
|
|
11
|
+
npm install -g github:blackspider-ops/relay#v0.5.1
|
|
12
|
+
npx --yes github:blackspider-ops/relay#v0.5.1 --version
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
`npx` runs packages; it is not a separate publishing service. Publishing
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "relay-codex",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Keep your Codex sessions moving between your own accounts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "blackspider-ops",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/blackspider-ops/relay.git"
|
|
9
|
+
"url": "git+https://github.com/blackspider-ops/relay.git"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://github.com/blackspider-ops/relay#readme",
|
|
12
12
|
"bugs": {
|
package/python/Menu.swift
CHANGED
|
@@ -126,7 +126,7 @@ final class MenuController: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
|
|
126
126
|
let name = session["name"] as? String ?? String(id.prefix(10))
|
|
127
127
|
let status = (session["state"] as? String ?? "unknown").replacingOccurrences(of: "_", with: " ")
|
|
128
128
|
let submenu = NSMenu()
|
|
129
|
-
text("Account: \(session["account"] as? String ?? "unknown")", into: submenu)
|
|
129
|
+
text("Account: \(session["account_name"] as? String ?? session["account"] as? String ?? "unknown")", into: submenu)
|
|
130
130
|
text("Model: \(session["model"] as? String ?? "unknown")", into: submenu)
|
|
131
131
|
if let next = session["next_wake"] { countdown("Next action", value: next, into: submenu) }
|
|
132
132
|
if let reason = session["reason"] as? String { text(reason.replacingOccurrences(of: "_", with: " "), into: submenu) }
|
package/python/commands.py
CHANGED
|
@@ -110,6 +110,13 @@ def snapshot(state, include_inactive=False):
|
|
|
110
110
|
)
|
|
111
111
|
with pool.db() as db:
|
|
112
112
|
for session in sessions:
|
|
113
|
+
account = next(
|
|
114
|
+
(a for a in accounts if a["id"] == session.get("account")), {}
|
|
115
|
+
)
|
|
116
|
+
session["account_name"] = account.get(
|
|
117
|
+
"name", session.get("account", "")
|
|
118
|
+
)
|
|
119
|
+
session["account_email"] = account.get("email")
|
|
113
120
|
saved_intent = db.execute(
|
|
114
121
|
"SELECT details FROM intents WHERE owner=? AND thread=?",
|
|
115
122
|
(session["id"], session.get("thread", "")),
|
|
@@ -198,11 +205,11 @@ def render(value):
|
|
|
198
205
|
if value["sessions"]:
|
|
199
206
|
lines += [
|
|
200
207
|
"",
|
|
201
|
-
f"{'SESSION':<12}{'ACCOUNT':<
|
|
208
|
+
f"{'SESSION':<12}{'ACCOUNT':<32}{'STATE':<23}{'MODEL':<23}PROJECT / NAME",
|
|
202
209
|
]
|
|
203
210
|
for row in value["sessions"]:
|
|
204
211
|
lines.append(
|
|
205
|
-
f"{row['id'][:10]:<12}{row.get('account', ''):<
|
|
212
|
+
f"{row['id'][:10]:<12}{str(row.get('account_name') or row.get('account', '')):<32}{str(row.get('state', 'unknown'))[:22]:<23}{str(row.get('model') or '?')[:22]:<23}{row.get('cwd') or row.get('name') or ''}"
|
|
206
213
|
)
|
|
207
214
|
if row.get("next_wake"):
|
|
208
215
|
lines.append(
|
package/python/router.py
CHANGED
|
@@ -24,7 +24,7 @@ from pool import Pool, blocked, reset_time, failure_kind, file_lock
|
|
|
24
24
|
from launch import translate, RESUME_FIELDS
|
|
25
25
|
import settings
|
|
26
26
|
|
|
27
|
-
VERSION = "0.5.
|
|
27
|
+
VERSION = "0.5.1"
|
|
28
28
|
_support = Path.home() / "Library/Application Support"
|
|
29
29
|
_legacy = _support / "Codex Accounts"
|
|
30
30
|
_default_state = _legacy if (_legacy / "accounts").exists() else _support / "Relay"
|
|
@@ -435,6 +435,17 @@ class Controller:
|
|
|
435
435
|
self.awake_process = None
|
|
436
436
|
self.fallback_tried = {}
|
|
437
437
|
|
|
438
|
+
def account_name(self):
|
|
439
|
+
if self.pool:
|
|
440
|
+
from commands import account_identity
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
account_identity(self.pool.state, self.account.label).get("email")
|
|
444
|
+
or self.pool.options(self.account.label).get("name")
|
|
445
|
+
or self.account.label
|
|
446
|
+
)
|
|
447
|
+
return self.account.label
|
|
448
|
+
|
|
438
449
|
def attached(self):
|
|
439
450
|
return bool(self.client) or self.background or self.starting
|
|
440
451
|
|
|
@@ -457,7 +468,7 @@ class Controller:
|
|
|
457
468
|
if self.preferences.get("notifications"):
|
|
458
469
|
from notifications import notify
|
|
459
470
|
|
|
460
|
-
notify(kind, self.
|
|
471
|
+
notify(kind, self.account_name())
|
|
461
472
|
|
|
462
473
|
def merge_settings(self, thread, params):
|
|
463
474
|
allowed = {
|
|
@@ -792,6 +803,8 @@ class Controller:
|
|
|
792
803
|
self.note("switched", to=label)
|
|
793
804
|
elif op == "retry":
|
|
794
805
|
self.paused = False
|
|
806
|
+
if thread:
|
|
807
|
+
self.clear_review(thread)
|
|
795
808
|
self.wake.set()
|
|
796
809
|
if self.recovery and not self.recovery.done():
|
|
797
810
|
self.force_retry.set()
|
|
@@ -803,7 +816,6 @@ class Controller:
|
|
|
803
816
|
)
|
|
804
817
|
self.pool.invalidate(self.account.label)
|
|
805
818
|
if thread and (not self.recovery or self.recovery.done()):
|
|
806
|
-
self.clear_review(thread)
|
|
807
819
|
self.recovery = asyncio.create_task(
|
|
808
820
|
self.continue_thread(thread, self.user_generation)
|
|
809
821
|
)
|
|
@@ -1173,7 +1185,7 @@ class Controller:
|
|
|
1173
1185
|
raise
|
|
1174
1186
|
except (RouterError, asyncio.TimeoutError):
|
|
1175
1187
|
return await self.uncertain(thread, generation, baseline)
|
|
1176
|
-
self.report(f"Continued the saved thread with {self.
|
|
1188
|
+
self.report(f"Continued the saved thread with {self.account_name()}.")
|
|
1177
1189
|
|
|
1178
1190
|
async def restore_recovery(self, thread, persisted):
|
|
1179
1191
|
"""Honor an accepted retry's original deadline after a process restart."""
|
|
@@ -1383,7 +1395,7 @@ class Controller:
|
|
|
1383
1395
|
episode["attempt"] += 1
|
|
1384
1396
|
target = episode["start"] + episode["attempt"] * self.retry_interval
|
|
1385
1397
|
self.report(
|
|
1386
|
-
f"Service/authentication failure on {self.
|
|
1398
|
+
f"Service/authentication failure on {self.account_name()}. Retry {episode['attempt']}/{self.retry_attempts} at {time.strftime('%H:%M:%S', time.localtime(target))}."
|
|
1387
1399
|
)
|
|
1388
1400
|
self.note(
|
|
1389
1401
|
"retry_scheduled",
|
|
@@ -1416,7 +1428,7 @@ class Controller:
|
|
|
1416
1428
|
)
|
|
1417
1429
|
self.episodes.pop(thread, None)
|
|
1418
1430
|
self.report(
|
|
1419
|
-
f"Selecting another account after {kind} failure on {self.
|
|
1431
|
+
f"Selecting another account after {kind} failure on {self.account_name()}."
|
|
1420
1432
|
)
|
|
1421
1433
|
self.note("selecting_account", thread=thread, reason=kind)
|
|
1422
1434
|
await self.mark(self.index, model, kind)
|
package/tests/test_audit.py
CHANGED
|
@@ -45,6 +45,31 @@ class RecoveryRegressions(unittest.IsolatedAsyncioTestCase):
|
|
|
45
45
|
self.assertTrue(any(method == "turn/start" for method, _ in backend.calls))
|
|
46
46
|
pool.close()
|
|
47
47
|
|
|
48
|
+
async def test_explicit_retry_clears_a_review_hold_during_an_existing_timer(self):
|
|
49
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
50
|
+
pool = Pool(Path(folder))
|
|
51
|
+
backend = FakeBackend()
|
|
52
|
+
c = Controller(
|
|
53
|
+
[FakeAccount("one")], backend, pool=pool, report=lambda _: None
|
|
54
|
+
)
|
|
55
|
+
c.client = FakeClient()
|
|
56
|
+
c.last_turn["thread-1"] = {"threadId": "thread-1"}
|
|
57
|
+
c.recovery = asyncio.create_task(c.recover("thread-1", 0, "capacity"))
|
|
58
|
+
try:
|
|
59
|
+
while not c.episodes:
|
|
60
|
+
await asyncio.sleep(0)
|
|
61
|
+
c.hold_for_review("thread-1")
|
|
62
|
+
await c.control("retry", {"thread": "thread-1"})
|
|
63
|
+
await asyncio.wait_for(c.recovery, 1)
|
|
64
|
+
self.assertFalse(pool.intent("thread-1")["review_required"])
|
|
65
|
+
self.assertEqual(
|
|
66
|
+
sum(method == "turn/start" for method, _ in backend.calls), 1
|
|
67
|
+
)
|
|
68
|
+
finally:
|
|
69
|
+
c.recovery.cancel()
|
|
70
|
+
await asyncio.gather(c.recovery, return_exceptions=True)
|
|
71
|
+
pool.close()
|
|
72
|
+
|
|
48
73
|
async def test_picker_permission_control_applies_before_thread_selection(self):
|
|
49
74
|
c = Controller([FakeAccount("one")], FakeBackend(), report=lambda _: None)
|
|
50
75
|
c.launch_overrides = {
|