codex-ipc 0.1.2__tar.gz → 0.1.3__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 (51) hide show
  1. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/PKG-INFO +1 -1
  2. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/ipc/src/ipc/constants.py +8 -3
  3. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/ipc/src/ipc/owner_follower.py +83 -4
  4. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/pyproject.toml +1 -1
  5. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/tests/test_plan_mode_user_input.py +109 -0
  6. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/uv.lock +1 -1
  7. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/.github/workflows/publish.yml +0 -0
  8. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/.gitignore +0 -0
  9. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/.python-version +0 -0
  10. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/.vscode/launch.json +0 -0
  11. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/README.md +0 -0
  12. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/examples/demo.py +0 -0
  13. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/examples/tui.py +0 -0
  14. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/README.md +0 -0
  15. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/pyproject.toml +0 -0
  16. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/__init__.py +0 -0
  17. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/_inputs.py +0 -0
  18. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/_message_router.py +0 -0
  19. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/_run.py +0 -0
  20. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/_version.py +0 -0
  21. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/api.py +0 -0
  22. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/async_client.py +0 -0
  23. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/client.py +0 -0
  24. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/errors.py +0 -0
  25. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/generated/__init__.py +0 -0
  26. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/generated/notification_registry.py +0 -0
  27. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/generated/v2_all.py +0 -0
  28. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/models.py +0 -0
  29. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/py.typed +0 -0
  30. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/codex-sdk/src/codex_app_server/retry.py +0 -0
  31. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/ipc/README.md +0 -0
  32. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/ipc/pyproject.toml +0 -0
  33. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/ipc/src/ipc/__init__.py +0 -0
  34. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/ipc/src/ipc/client.py +0 -0
  35. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/ipc/src/ipc/conversation_state.py +0 -0
  36. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/ipc/src/ipc/frame_protocol.py +0 -0
  37. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/ipc/src/ipc/immer.py +0 -0
  38. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/ipc/src/ipc/py.typed +0 -0
  39. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/packages/ipc/src/ipc/router.py +0 -0
  40. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/src/codex_ipc/__init__.py +0 -0
  41. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/src/codex_ipc/codex/__init__.py +0 -0
  42. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/src/codex_ipc/codex/api.py +0 -0
  43. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/src/codex_ipc/codex/async_client.py +0 -0
  44. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/src/codex_ipc/codex/client.py +0 -0
  45. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/src/codex_ipc/codex/generated/__init__.py +0 -0
  46. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/src/codex_ipc/codex/generated/v2_all.py +0 -0
  47. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/src/codex_ipc/config.py +0 -0
  48. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/src/codex_ipc/session.py +0 -0
  49. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/src/codex_ipc/user_input.py +0 -0
  50. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/tests/__init__.py +0 -0
  51. {codex_ipc-0.1.2 → codex_ipc-0.1.3}/tests/test_ssh_websocket_transport.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-ipc
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: pydantic>=2.12
@@ -22,7 +22,10 @@ IPC_DEBUG_TEXT_LIMIT = 500
22
22
 
23
23
  IPC_METHOD_VERSIONS: dict[str, int] = {
24
24
  # broadcast methods
25
- "thread-stream-state-changed": 7,
25
+ "thread-stream-state-changed": 11,
26
+ "thread-stream-following-changed": 1,
27
+ "thread-stream-following-status-requested": 1,
28
+ "ipc-connection-reset": 1,
26
29
  "thread-read-state-changed": 1,
27
30
  "thread-archived": 2,
28
31
  "thread-unarchived": 1,
@@ -31,12 +34,14 @@ IPC_METHOD_VERSIONS: dict[str, int] = {
31
34
  "client-status-changed": 0,
32
35
  # request methods (thread-follower series)
33
36
  "thread-follower-start-turn": 1,
37
+ "thread-follower-load-complete-history": 1,
34
38
  "thread-follower-compact-thread": 1,
35
39
  "thread-follower-steer-turn": 1,
36
- "thread-follower-interrupt-turn": 1,
40
+ "thread-follower-interrupt-turn": 2,
41
+ "thread-follower-update-thread-settings": 1,
37
42
  "thread-follower-set-model-and-reasoning": 1,
38
43
  "thread-follower-set-collaboration-mode": 1,
39
- "thread-follower-edit-last-user-turn": 1,
44
+ "thread-follower-edit-last-user-turn": 2,
40
45
  "thread-follower-command-approval-decision": 1,
41
46
  "thread-follower-file-approval-decision": 1,
42
47
  "thread-follower-permissions-request-approval-response": 1,
@@ -149,7 +149,7 @@ class OwnerFollowerSession:
149
149
  self._state: JsonDict | None = None
150
150
  self._stream_role: JsonDict | None = None
151
151
  self._streaming_conversations: set[str] = set()
152
- # Monotonic per-conversation revision (matches Codex extension v7
152
+ # Monotonic per-conversation revision (matches Codex extension v11
153
153
  # streamRevisionByConversationId). Owner increments on every snapshot
154
154
  # and patch it emits; followers track it to guard out-of-order patches.
155
155
  self._stream_revision: int = 0
@@ -206,6 +206,11 @@ class OwnerFollowerSession:
206
206
  self._can_handle_owner_request,
207
207
  self._handle_follower_start_turn,
208
208
  )
209
+ self.ipc_client.add_request_handler(
210
+ "thread-follower-load-complete-history",
211
+ self._can_handle_owner_request,
212
+ self._handle_follower_load_complete_history,
213
+ )
209
214
  self.ipc_client.add_request_handler(
210
215
  "thread-follower-steer-turn",
211
216
  self._can_handle_owner_request,
@@ -216,6 +221,11 @@ class OwnerFollowerSession:
216
221
  self._can_handle_owner_request,
217
222
  self._handle_follower_set_queued_followups_state,
218
223
  )
224
+ self.ipc_client.add_request_handler(
225
+ "thread-follower-update-thread-settings",
226
+ self._can_handle_owner_request,
227
+ self._handle_follower_update_thread_settings,
228
+ )
219
229
  self.ipc_client.add_request_handler(
220
230
  "thread-follower-set-collaboration-mode",
221
231
  self._can_handle_owner_request,
@@ -333,17 +343,17 @@ class OwnerFollowerSession:
333
343
  owner_client_id = self._owner_client_id()
334
344
  if owner_client_id:
335
345
  response = await self.ipc_client.send_request(
336
- "thread-follower-set-collaboration-mode",
346
+ "thread-follower-update-thread-settings",
337
347
  {
338
348
  "conversationId": self.thread_id,
339
- "collaborationMode": collaboration_mode,
349
+ "threadSettings": {"collaborationMode": collaboration_mode},
340
350
  },
341
351
  target_client_id=owner_client_id,
342
352
  )
343
353
  if response.get("resultType") == "success":
344
354
  return
345
355
  raise RuntimeError(
346
- "thread-follower-set-collaboration-mode failed: "
356
+ "thread-follower-update-thread-settings failed: "
347
357
  f"{response.get('error', '')}"
348
358
  )
349
359
 
@@ -2992,6 +3002,19 @@ class OwnerFollowerSession:
2992
3002
  }
2993
3003
  }
2994
3004
 
3005
+ async def _handle_follower_load_complete_history(
3006
+ self,
3007
+ message: JsonDict,
3008
+ ) -> JsonDict:
3009
+ params = message.get("params")
3010
+ if not isinstance(params, dict):
3011
+ raise RuntimeError("invalid params")
3012
+ if params.get("conversationId") != self.thread_id:
3013
+ raise RuntimeError("invalid conversationId")
3014
+
3015
+ await self.read_thread(include_turns=True, broadcast_snapshot=True)
3016
+ return {"revision": self._stream_revision}
3017
+
2995
3018
  async def _handle_follower_steer_turn(self, message: JsonDict) -> JsonDict:
2996
3019
  params = message.get("params")
2997
3020
  if not isinstance(params, dict):
@@ -3028,6 +3051,62 @@ class OwnerFollowerSession:
3028
3051
  )
3029
3052
  return {"result": {"ok": True}}
3030
3053
 
3054
+ async def _handle_follower_update_thread_settings(
3055
+ self,
3056
+ message: JsonDict,
3057
+ ) -> JsonDict:
3058
+ params = message.get("params")
3059
+ if not isinstance(params, dict):
3060
+ raise RuntimeError("invalid params")
3061
+ if params.get("conversationId") != self.thread_id:
3062
+ raise RuntimeError("invalid conversationId")
3063
+
3064
+ thread_settings = params.get("threadSettings")
3065
+ if not isinstance(thread_settings, dict):
3066
+ raise RuntimeError("invalid threadSettings")
3067
+
3068
+ await self._update_state(
3069
+ lambda draft: self._apply_thread_settings(draft, thread_settings)
3070
+ )
3071
+ return {"result": {"ok": True}}
3072
+
3073
+ def _apply_thread_settings(
3074
+ self,
3075
+ draft: JsonDict,
3076
+ thread_settings: JsonDict,
3077
+ ) -> None:
3078
+ latest_settings = draft.get("latestThreadSettings")
3079
+ merged_settings = (
3080
+ dict(latest_settings) if isinstance(latest_settings, dict) else {}
3081
+ )
3082
+ merged_settings.update(thread_settings)
3083
+
3084
+ collaboration_mode = thread_settings.get("collaborationMode")
3085
+ if isinstance(collaboration_mode, dict):
3086
+ merged_settings["collaborationMode"] = dict(collaboration_mode)
3087
+ draft["latestCollaborationMode"] = dict(collaboration_mode)
3088
+ elif "collaborationMode" in thread_settings:
3089
+ collaboration_mode = default_collaboration_mode()
3090
+ merged_settings["collaborationMode"] = collaboration_mode
3091
+ draft["latestCollaborationMode"] = collaboration_mode
3092
+
3093
+ model = thread_settings.get("model")
3094
+ if not isinstance(model, str) and isinstance(collaboration_mode, dict):
3095
+ settings = collaboration_mode.get("settings")
3096
+ if isinstance(settings, dict) and isinstance(settings.get("model"), str):
3097
+ model = settings["model"]
3098
+ if isinstance(model, str):
3099
+ merged_settings["model"] = model
3100
+ draft["latestModel"] = model
3101
+
3102
+ if "effort" in thread_settings:
3103
+ draft["latestReasoningEffort"] = thread_settings.get("effort")
3104
+ cwd = thread_settings.get("cwd")
3105
+ if isinstance(cwd, str):
3106
+ draft["cwd"] = cwd
3107
+
3108
+ draft["latestThreadSettings"] = merged_settings
3109
+
3031
3110
  async def _handle_follower_submit_user_input(
3032
3111
  self,
3033
3112
  message: JsonDict,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "codex-ipc"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Add your description here"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -44,6 +44,7 @@ from ipc.conversation_state import (
44
44
  build_conversation_state,
45
45
  default_collaboration_mode,
46
46
  )
47
+ from ipc.constants import IPC_METHOD_VERSIONS
47
48
  from ipc.owner_follower import OwnerFollowerSession
48
49
 
49
50
 
@@ -570,6 +571,21 @@ class FacadeSessionTests(unittest.IsolatedAsyncioTestCase):
570
571
 
571
572
 
572
573
  class CoreImportTests(unittest.TestCase):
574
+ def test_ipc_versions_match_conversation_state_v11(self) -> None:
575
+ expected_versions = {
576
+ "thread-stream-state-changed": 11,
577
+ "thread-stream-following-changed": 1,
578
+ "thread-stream-following-status-requested": 1,
579
+ "ipc-connection-reset": 1,
580
+ "thread-follower-load-complete-history": 1,
581
+ "thread-follower-interrupt-turn": 2,
582
+ "thread-follower-update-thread-settings": 1,
583
+ "thread-follower-edit-last-user-turn": 2,
584
+ }
585
+
586
+ for method, version in expected_versions.items():
587
+ self.assertEqual(IPC_METHOD_VERSIONS[method], version)
588
+
573
589
  def test_core_import_does_not_require_tui_dependencies(self) -> None:
574
590
  script = """
575
591
  import builtins
@@ -597,6 +613,99 @@ print(codex_ipc.CodexIpcConfig.__name__)
597
613
 
598
614
 
599
615
  class PlanModeTests(unittest.IsolatedAsyncioTestCase):
616
+ async def test_follower_forwards_collaboration_mode_as_thread_settings(self) -> None:
617
+ session = make_session()
618
+ ipc_client = FakeIpcClient()
619
+ session.ipc_client = ipc_client
620
+ session._stream_role = {"role": "follower", "ownerClientId": "owner-1"}
621
+ collaboration_mode = {
622
+ "mode": "plan",
623
+ "settings": {
624
+ "model": "gpt-5.5",
625
+ "reasoning_effort": "high",
626
+ "developer_instructions": None,
627
+ },
628
+ }
629
+
630
+ await session.set_collaboration_mode(collaboration_mode)
631
+
632
+ self.assertEqual(
633
+ ipc_client.requests,
634
+ [
635
+ (
636
+ "thread-follower-update-thread-settings",
637
+ {
638
+ "conversationId": "thread-1",
639
+ "threadSettings": {"collaborationMode": collaboration_mode},
640
+ },
641
+ "owner-1",
642
+ )
643
+ ],
644
+ )
645
+
646
+ async def test_owner_applies_follower_thread_settings(self) -> None:
647
+ session = make_session()
648
+ session.ipc_client = FakeIpcClient()
649
+ session._stream_role = {"role": "owner"}
650
+ session._state = {
651
+ "id": "thread-1",
652
+ "latestModel": "gpt-5.4",
653
+ "latestReasoningEffort": "medium",
654
+ "latestCollaborationMode": default_collaboration_mode(),
655
+ "latestThreadSettings": {"model": "gpt-5.4"},
656
+ "updatedAt": 0,
657
+ }
658
+ collaboration_mode = {
659
+ "mode": "plan",
660
+ "settings": {
661
+ "model": "gpt-5.5",
662
+ "reasoning_effort": "high",
663
+ "developer_instructions": None,
664
+ },
665
+ }
666
+
667
+ result = await session._handle_follower_update_thread_settings(
668
+ {
669
+ "params": {
670
+ "conversationId": "thread-1",
671
+ "threadSettings": {
672
+ "collaborationMode": collaboration_mode,
673
+ "effort": "high",
674
+ "cwd": "/repo",
675
+ },
676
+ }
677
+ }
678
+ )
679
+
680
+ self.assertEqual(result, {"result": {"ok": True}})
681
+ self.assertEqual(session.state["latestCollaborationMode"], collaboration_mode)
682
+ self.assertEqual(session.state["latestModel"], "gpt-5.5")
683
+ self.assertEqual(session.state["latestReasoningEffort"], "high")
684
+ self.assertEqual(session.state["cwd"], "/repo")
685
+
686
+ async def test_owner_loads_complete_history_and_returns_snapshot_revision(self) -> None:
687
+ session = make_session()
688
+ calls: list[tuple[bool, bool]] = []
689
+
690
+ async def read_thread(
691
+ _thread_id: str | None = None,
692
+ *,
693
+ include_turns: bool = False,
694
+ broadcast_snapshot: bool = False,
695
+ ) -> object:
696
+ calls.append((include_turns, broadcast_snapshot))
697
+ session._stream_revision = 8
698
+ return object()
699
+
700
+ session.read_thread = read_thread # type: ignore[method-assign]
701
+
702
+ result = await session._handle_follower_load_complete_history(
703
+ {"params": {"conversationId": "thread-1"}}
704
+ )
705
+
706
+ self.assertEqual(calls, [(True, True)])
707
+ self.assertEqual(result, {"revision": 8})
708
+
600
709
  async def test_turn_start_passes_collaboration_mode(self) -> None:
601
710
  codex = FakeCodex()
602
711
  thread = AsyncThread(codex, "thread-1")
@@ -31,7 +31,7 @@ requires-dist = [{ name = "pydantic", specifier = ">=2.12" }]
31
31
 
32
32
  [[package]]
33
33
  name = "codex-ipc"
34
- version = "0.1.2"
34
+ version = "0.1.3"
35
35
  source = { editable = "." }
36
36
  dependencies = [
37
37
  { name = "pydantic" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes