codex-telegram 0.4.9__tar.gz → 0.4.11__tar.gz

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.
Files changed (93) hide show
  1. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/PKG-INFO +14 -2
  2. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/README.md +13 -1
  3. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/app_runtime/bootstrap.py +11 -1
  4. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex_telegram/__init__.py +1 -1
  5. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex_telegram.egg-info/PKG-INFO +14 -2
  6. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex_telegram.egg-info/SOURCES.txt +3 -2
  7. codex-telegram-0.4.11/tests/test_bootstrap_update_check.py +53 -0
  8. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_web_server_local_command.py +100 -1
  9. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/web/routes.py +25 -6
  10. codex-telegram-0.4.11/web/static/dist/assets/index-BbpBsR8Z.js +53 -0
  11. codex-telegram-0.4.11/web/static/dist/assets/index-BfBVFmNi.css +1 -0
  12. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/web/static/dist/index.html +2 -2
  13. codex-telegram-0.4.9/web/static/dist/assets/index-B6IQl7AZ.js +0 -53
  14. codex-telegram-0.4.9/web/static/dist/assets/index-Czx9yv3Y.css +0 -1
  15. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/LICENSE +0 -0
  16. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/MANIFEST.in +0 -0
  17. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/TELEGRAM.md +0 -0
  18. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/WEB.md +0 -0
  19. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/app_runtime/__init__.py +0 -0
  20. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/app_runtime/telegram_app.py +0 -0
  21. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/app_runtime/web_server.py +0 -0
  22. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/bot/__init__.py +0 -0
  23. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/bot/callbacks.py +0 -0
  24. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/bot/features_ui.py +0 -0
  25. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/bot/handlers.py +0 -0
  26. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/bot/keyboard.py +0 -0
  27. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/bot/projects_ui.py +0 -0
  28. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/bot/skills_ui.py +0 -0
  29. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/bot/thread_ui.py +0 -0
  30. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/__init__.py +0 -0
  31. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/approval_flow.py +0 -0
  32. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/approval_guardian.py +0 -0
  33. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/client.py +0 -0
  34. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/collaboration_mode.py +0 -0
  35. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/command_router/__init__.py +0 -0
  36. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/command_router/common.py +0 -0
  37. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/command_router/context.py +0 -0
  38. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/command_router/contracts.py +0 -0
  39. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/command_router/core.py +0 -0
  40. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/command_router/projects.py +0 -0
  41. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/command_router/review.py +0 -0
  42. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/command_router/system.py +0 -0
  43. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/command_router/threads.py +0 -0
  44. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/commands.py +0 -0
  45. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/event_forwarding.py +0 -0
  46. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/events.py +0 -0
  47. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex/protocol.py +0 -0
  48. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex_telegram/cli.py +0 -0
  49. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex_telegram.egg-info/dependency_links.txt +0 -0
  50. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex_telegram.egg-info/entry_points.txt +0 -0
  51. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex_telegram.egg-info/requires.txt +0 -0
  52. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/codex_telegram.egg-info/top_level.txt +0 -0
  53. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/conf.toml.example +0 -0
  54. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/main.py +0 -0
  55. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/models/__init__.py +0 -0
  56. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/models/state.py +0 -0
  57. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/models/thread.py +0 -0
  58. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/models/user.py +0 -0
  59. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/pyproject.toml +0 -0
  60. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/setup.cfg +0 -0
  61. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_approval_guardian.py +0 -0
  62. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_approval_policy.py +0 -0
  63. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_callbacks_results.py +0 -0
  64. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_cli_entrypoint.py +0 -0
  65. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_codex_client.py +0 -0
  66. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_command_router.py +0 -0
  67. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_guardian_config.py +0 -0
  68. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_handlers_results.py +0 -0
  69. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_keyboard.py +0 -0
  70. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_main_approval_flow.py +0 -0
  71. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_normalize.py +0 -0
  72. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_pypi_check.py +0 -0
  73. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_single_instance.py +0 -0
  74. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_telegram_bot_config.py +0 -0
  75. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_thread_history.py +0 -0
  76. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/tests/test_web_runtime.py +0 -0
  77. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/utils/__init__.py +0 -0
  78. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/utils/approval_policy.py +0 -0
  79. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/utils/config.py +0 -0
  80. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/utils/local_command.py +0 -0
  81. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/utils/logger.py +0 -0
  82. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/utils/normalize.py +0 -0
  83. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/utils/pypi_check.py +0 -0
  84. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/utils/single_instance.py +0 -0
  85. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/utils/web_config.py +0 -0
  86. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/web/__init__.py +0 -0
  87. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/web/dependencies.py +0 -0
  88. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/web/runtime.py +0 -0
  89. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/web/server.py +0 -0
  90. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/web/static/dist/icons-tab-add.svg +0 -0
  91. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/web/static/dist/icons-tab-close.svg +0 -0
  92. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/web/thread_history.py +0 -0
  93. {codex-telegram-0.4.9 → codex-telegram-0.4.11}/web/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codex-telegram
3
- Version: 0.4.9
3
+ Version: 0.4.11
4
4
  Summary: Bridge for controlling the Codex App Server from Telegram and Web UI
5
5
  Author: heungtae
6
6
  License: Apache License
@@ -320,6 +320,18 @@ export CODEX_WEB_PASSWORD="your_password"
320
320
  codex-telegram
321
321
  ```
322
322
 
323
+ If you want to rebuild the web UI before launching the app, use:
324
+
325
+ ```bash
326
+ ./run_build_and_main.sh
327
+ ```
328
+
329
+ Or:
330
+
331
+ ```bash
332
+ make run
333
+ ```
334
+
323
335
  ### 4. Open Web UI
324
336
 
325
337
  ```text
@@ -335,7 +347,7 @@ pip install -e .
335
347
  cd web/frontend && npm install && npm run build && cd ../..
336
348
  cp conf.toml.example ~/.config/codex-telegram/conf.toml
337
349
  # Edit config, then run:
338
- python3 main.py
350
+ ./run_build_and_main.sh
339
351
  ```
340
352
 
341
353
  ## Documentation
@@ -99,6 +99,18 @@ export CODEX_WEB_PASSWORD="your_password"
99
99
  codex-telegram
100
100
  ```
101
101
 
102
+ If you want to rebuild the web UI before launching the app, use:
103
+
104
+ ```bash
105
+ ./run_build_and_main.sh
106
+ ```
107
+
108
+ Or:
109
+
110
+ ```bash
111
+ make run
112
+ ```
113
+
102
114
  ### 4. Open Web UI
103
115
 
104
116
  ```text
@@ -114,7 +126,7 @@ pip install -e .
114
126
  cd web/frontend && npm install && npm run build && cd ../..
115
127
  cp conf.toml.example ~/.config/codex-telegram/conf.toml
116
128
  # Edit config, then run:
117
- python3 main.py
129
+ ./run_build_and_main.sh
118
130
  ```
119
131
 
120
132
  ## Documentation
@@ -14,13 +14,21 @@ logger = logging.getLogger("codex-telegram")
14
14
  async def _check_update(*, verify_ssl: bool) -> None:
15
15
  try:
16
16
  version_info = await check_latest_version(verify_ssl=verify_ssl)
17
- if version_info is not None and version_info.is_outdated:
17
+ if version_info is None:
18
+ logger.info("PyPI update check did not return version information.")
19
+ return
20
+ if version_info.is_outdated:
18
21
  logger.warning(
19
22
  "A newer version of codex-telegram is available: %s (current: %s). "
20
23
  "Run: pip install --upgrade codex-telegram",
21
24
  version_info.latest,
22
25
  version_info.current,
23
26
  )
27
+ return
28
+ logger.info(
29
+ "codex-telegram is up to date at version %s.",
30
+ version_info.current,
31
+ )
24
32
  except Exception as e:
25
33
  logger.warning("Failed to check for updates: %s", e)
26
34
 
@@ -77,6 +85,7 @@ async def post_init(
77
85
  default=True,
78
86
  )
79
87
  if update_check_enabled:
88
+ logger.info("Checking PyPI for codex-telegram updates...")
80
89
  asyncio.create_task(_check_update(verify_ssl=update_check_verify_ssl))
81
90
  state.update_notified = True
82
91
 
@@ -94,6 +103,7 @@ async def post_shutdown(
94
103
  state_module.approval_guardian = None
95
104
  state_module.command_router = None
96
105
  state_module.codex_ready.clear()
106
+ state_module.update_notified = False
97
107
 
98
108
 
99
109
  async def run_without_telegram(
@@ -1,3 +1,3 @@
1
1
  """codex-telegram package."""
2
2
 
3
- __version__ = "0.4.9"
3
+ __version__ = "0.4.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: codex-telegram
3
- Version: 0.4.9
3
+ Version: 0.4.11
4
4
  Summary: Bridge for controlling the Codex App Server from Telegram and Web UI
5
5
  Author: heungtae
6
6
  License: Apache License
@@ -320,6 +320,18 @@ export CODEX_WEB_PASSWORD="your_password"
320
320
  codex-telegram
321
321
  ```
322
322
 
323
+ If you want to rebuild the web UI before launching the app, use:
324
+
325
+ ```bash
326
+ ./run_build_and_main.sh
327
+ ```
328
+
329
+ Or:
330
+
331
+ ```bash
332
+ make run
333
+ ```
334
+
323
335
  ### 4. Open Web UI
324
336
 
325
337
  ```text
@@ -335,7 +347,7 @@ pip install -e .
335
347
  cd web/frontend && npm install && npm run build && cd ../..
336
348
  cp conf.toml.example ~/.config/codex-telegram/conf.toml
337
349
  # Edit config, then run:
338
- python3 main.py
350
+ ./run_build_and_main.sh
339
351
  ```
340
352
 
341
353
  ## Documentation
@@ -50,6 +50,7 @@ models/thread.py
50
50
  models/user.py
51
51
  tests/test_approval_guardian.py
52
52
  tests/test_approval_policy.py
53
+ tests/test_bootstrap_update_check.py
53
54
  tests/test_callbacks_results.py
54
55
  tests/test_cli_entrypoint.py
55
56
  tests/test_codex_client.py
@@ -84,5 +85,5 @@ web/workspace.py
84
85
  web/static/dist/icons-tab-add.svg
85
86
  web/static/dist/icons-tab-close.svg
86
87
  web/static/dist/index.html
87
- web/static/dist/assets/index-B6IQl7AZ.js
88
- web/static/dist/assets/index-Czx9yv3Y.css
88
+ web/static/dist/assets/index-BbpBsR8Z.js
89
+ web/static/dist/assets/index-BfBVFmNi.css
@@ -0,0 +1,53 @@
1
+ import types
2
+ import unittest
3
+ from unittest.mock import AsyncMock, patch
4
+
5
+ from app_runtime import bootstrap
6
+ from utils.pypi_check import VersionInfo
7
+
8
+
9
+ class BootstrapUpdateCheckTests(unittest.IsolatedAsyncioTestCase):
10
+ @patch("app_runtime.bootstrap.check_latest_version", new_callable=AsyncMock)
11
+ async def test_check_update_logs_warning_when_outdated(self, mock_check_latest_version):
12
+ mock_check_latest_version.return_value = VersionInfo(
13
+ current="0.4.9",
14
+ latest="0.4.10",
15
+ is_outdated=True,
16
+ release_url="https://pypi.org/project/codex-telegram/0.4.10/",
17
+ )
18
+
19
+ with self.assertLogs("codex-telegram", level="WARNING") as logs:
20
+ await bootstrap._check_update(verify_ssl=True)
21
+
22
+ self.assertTrue(any("A newer version of codex-telegram is available" in entry for entry in logs.output))
23
+
24
+ @patch("app_runtime.bootstrap.check_latest_version", new_callable=AsyncMock)
25
+ async def test_check_update_logs_info_when_up_to_date(self, mock_check_latest_version):
26
+ mock_check_latest_version.return_value = VersionInfo(
27
+ current="0.4.10",
28
+ latest="0.4.10",
29
+ is_outdated=False,
30
+ release_url="https://pypi.org/project/codex-telegram/0.4.10/",
31
+ )
32
+
33
+ with self.assertLogs("codex-telegram", level="INFO") as logs:
34
+ await bootstrap._check_update(verify_ssl=True)
35
+
36
+ self.assertTrue(any("codex-telegram is up to date at version 0.4.10." in entry for entry in logs.output))
37
+
38
+ async def test_post_shutdown_resets_update_notified(self):
39
+ state_module = types.SimpleNamespace(
40
+ codex_client=None,
41
+ approval_guardian=None,
42
+ command_router=object(),
43
+ codex_ready=types.SimpleNamespace(clear=lambda: None),
44
+ update_notified=True,
45
+ )
46
+
47
+ await bootstrap.post_shutdown(None, state_module=state_module)
48
+
49
+ self.assertFalse(state_module.update_notified)
50
+
51
+
52
+ if __name__ == "__main__":
53
+ unittest.main()
@@ -4,7 +4,7 @@ import os
4
4
  import subprocess
5
5
  import tempfile
6
6
  from types import SimpleNamespace
7
- from unittest.mock import AsyncMock, patch
7
+ from unittest.mock import AsyncMock, call, patch
8
8
 
9
9
  from fastapi import HTTPException
10
10
 
@@ -660,6 +660,42 @@ path_glob_any = ["helm/**"]
660
660
  asyncio.run(endpoint(request, "thread-sub"))
661
661
 
662
662
  self.assertIn(self.session.user_id, user_manager.find_user_ids_by_thread("thread-sub"))
663
+ self.assertEqual("thread-sub", user_manager.get(self.session.user_id).active_thread_id)
664
+
665
+ def test_chat_messages_uses_active_thread_when_payload_thread_id_is_missing(self):
666
+ app = create_web_app()
667
+ endpoint = next(
668
+ route.endpoint
669
+ for route in app.routes
670
+ if getattr(route, "path", None) == "/api/chat/messages"
671
+ )
672
+ request = SimpleNamespace(cookies={COOKIE_NAME: self.session.token})
673
+ state_user = user_manager.get(self.session.user_id)
674
+ state_user.active_thread_id = "thread-active"
675
+ state_user.set_collaboration_mode_mask(
676
+ {"name": "build", "mode": "default", "model": "gpt-5.3-codex", "reasoning_effort": "medium"}
677
+ )
678
+ state.codex_client.call = AsyncMock(return_value={"turn": {"id": "turn-new"}})
679
+
680
+ body = asyncio.run(endpoint({"text": "hello"}, request))
681
+
682
+ self.assertTrue(body["ok"])
683
+ self.assertEqual("thread-active", body["thread_id"])
684
+ state.codex_client.call.assert_awaited_once_with(
685
+ "turn/start",
686
+ {
687
+ "threadId": "thread-active",
688
+ "collaborationMode": {
689
+ "mode": "default",
690
+ "settings": {
691
+ "model": "gpt-5.3-codex",
692
+ "reasoning_effort": "medium",
693
+ "developer_instructions": None,
694
+ },
695
+ },
696
+ "input": [{"type": "text", "text": "hello"}],
697
+ },
698
+ )
663
699
 
664
700
  def test_thread_read_marks_non_default_assistant_messages_as_subagent(self):
665
701
  app = create_web_app()
@@ -922,6 +958,7 @@ path_glob_any = ["helm/**"]
922
958
  )
923
959
  state.codex_client.call = AsyncMock(
924
960
  side_effect=[
961
+ CodexError(-32600, "thread not found: stale-thread"),
925
962
  CodexError(-32600, "thread not found: stale-thread"),
926
963
  {"thread": {"id": "thread-new"}},
927
964
  {"turn": {"id": "turn-new"}},
@@ -936,6 +973,68 @@ path_glob_any = ["helm/**"]
936
973
  self.assertEqual(self.session.user_id, user_manager.find_user_id_by_turn("turn-new"))
937
974
  self.assertEqual("thread-new", user_manager.get_turn_thread("turn-new"))
938
975
 
976
+ def test_chat_messages_retries_requested_thread_by_resuming_before_creating_new_thread(self):
977
+ app = create_web_app()
978
+ endpoint = next(
979
+ route.endpoint
980
+ for route in app.routes
981
+ if getattr(route, "path", None) == "/api/chat/messages"
982
+ )
983
+ request = SimpleNamespace(cookies={COOKIE_NAME: self.session.token})
984
+ state_user = user_manager.get(self.session.user_id)
985
+ state_user.set_collaboration_mode_mask(
986
+ {"name": "build", "mode": "default", "model": "gpt-5.3-codex", "reasoning_effort": "medium"}
987
+ )
988
+ state.codex_client.call = AsyncMock(
989
+ side_effect=[
990
+ CodexError(-32600, "thread not found: thread-existing"),
991
+ {},
992
+ {"turn": {"id": "turn-existing"}},
993
+ ]
994
+ )
995
+
996
+ body = asyncio.run(endpoint({"text": "hello", "thread_id": "thread-existing"}, request))
997
+
998
+ self.assertTrue(body["ok"])
999
+ self.assertEqual("thread-existing", body["thread_id"])
1000
+ self.assertEqual("turn-existing", body["turn_id"])
1001
+ self.assertEqual(
1002
+ [
1003
+ call(
1004
+ "turn/start",
1005
+ {
1006
+ "threadId": "thread-existing",
1007
+ "collaborationMode": {
1008
+ "mode": "default",
1009
+ "settings": {
1010
+ "model": "gpt-5.3-codex",
1011
+ "reasoning_effort": "medium",
1012
+ "developer_instructions": None,
1013
+ },
1014
+ },
1015
+ "input": [{"type": "text", "text": "hello"}],
1016
+ },
1017
+ ),
1018
+ call("thread/resume", {"threadId": "thread-existing"}),
1019
+ call(
1020
+ "turn/start",
1021
+ {
1022
+ "threadId": "thread-existing",
1023
+ "collaborationMode": {
1024
+ "mode": "default",
1025
+ "settings": {
1026
+ "model": "gpt-5.3-codex",
1027
+ "reasoning_effort": "medium",
1028
+ "developer_instructions": None,
1029
+ },
1030
+ },
1031
+ "input": [{"type": "text", "text": "hello"}],
1032
+ },
1033
+ ),
1034
+ ],
1035
+ state.codex_client.call.await_args_list,
1036
+ )
1037
+
939
1038
  def test_workspace_tree_status_file_and_diff_endpoints(self):
940
1039
  with tempfile.TemporaryDirectory(prefix="codex-web-workspace-") as workspace:
941
1040
  subprocess.run(["git", "init"], cwd=workspace, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
@@ -478,12 +478,26 @@ def register_thread_routes(app: FastAPI) -> None:
478
478
  and "thread not found" in str(exc.message or "").lower()
479
479
  and payload_thread_id
480
480
  ):
481
- thread_id = await start_thread_for_context()
482
- if payload_project_key:
483
- user_manager.bind_thread_project(thread_id, payload_project_key)
484
- user_manager.bind_thread_owner(session.user_id, thread_id)
485
- params["threadId"] = thread_id
486
- result = await state.codex_client.call("turn/start", params)
481
+ resumed_existing_thread = False
482
+ try:
483
+ await state.codex_client.call("thread/resume", {"threadId": payload_thread_id})
484
+ thread_id = payload_thread_id
485
+ user_manager.set_active_thread(session.user_id, thread_id, project_key=payload_project_key or None)
486
+ if payload_project_key:
487
+ user_manager.bind_thread_project(thread_id, payload_project_key)
488
+ user_manager.bind_thread_owner(session.user_id, thread_id)
489
+ params["threadId"] = thread_id
490
+ result = await state.codex_client.call("turn/start", params)
491
+ resumed_existing_thread = True
492
+ except CodexError:
493
+ resumed_existing_thread = False
494
+ if not resumed_existing_thread:
495
+ thread_id = await start_thread_for_context()
496
+ if payload_project_key:
497
+ user_manager.bind_thread_project(thread_id, payload_project_key)
498
+ user_manager.bind_thread_owner(session.user_id, thread_id)
499
+ params["threadId"] = thread_id
500
+ result = await state.codex_client.call("turn/start", params)
487
501
  else:
488
502
  raise
489
503
  turn = result.get("turn", {}) if isinstance(result, dict) else {}
@@ -513,6 +527,11 @@ def register_thread_routes(app: FastAPI) -> None:
513
527
  if not normalized_thread_id:
514
528
  raise HTTPException(status_code=400, detail="thread_id is required")
515
529
  user_manager.bind_thread_subscriber(session.user_id, normalized_thread_id)
530
+ user_manager.set_active_thread(
531
+ session.user_id,
532
+ normalized_thread_id,
533
+ project_key=user_manager.get_thread_project(normalized_thread_id),
534
+ )
516
535
  await wait_for_codex()
517
536
  try:
518
537
  result = await state.codex_client.call(