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.
@@ -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.0
12
- npx --yes github:blackspider-ops/relay#v0.5.0 --version
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.0",
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) }
@@ -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':<14}{'STATE':<23}{'MODEL':<23}PROJECT / NAME",
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', ''):<14}{str(row.get('state', 'unknown'))[:22]:<23}{str(row.get('model') or '?')[:22]:<23}{row.get('cwd') or row.get('name') or ''}"
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.0"
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.account.label)
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.account.label}.")
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.account.label}. Retry {episode['attempt']}/{self.retry_attempts} at {time.strftime('%H:%M:%S', time.localtime(target))}."
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.account.label}."
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)
@@ -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 = {