codex-a2a 0.6.0__tar.gz → 0.6.1__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 (202) hide show
  1. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/.gitignore +2 -0
  2. {codex_a2a-0.6.0/src/codex_a2a.egg-info → codex_a2a-0.6.1}/PKG-INFO +1 -1
  3. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/stream_interrupts.py +13 -5
  4. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/session_control.py +16 -0
  5. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/session_query.py +16 -0
  6. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/conversation_facade.py +115 -4
  7. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/interrupts.py +17 -0
  8. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/models.py +4 -0
  9. {codex_a2a-0.6.0 → codex_a2a-0.6.1/src/codex_a2a.egg-info}/PKG-INFO +1 -1
  10. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_stream_interrupts.py +31 -2
  11. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_codex_session_extension.py +77 -0
  12. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/upstream/test_codex_client_params.py +105 -1
  13. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/upstream/test_modular_upstream_components.py +3 -0
  14. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/.github/workflows/ci.yml +0 -0
  15. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/.github/workflows/publish.yml +0 -0
  16. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/.pre-commit-config.yaml +0 -0
  17. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/.secrets.baseline +0 -0
  18. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/AGENTS.md +0 -0
  19. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/CONTRIBUTING.md +0 -0
  20. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/LICENSE +0 -0
  21. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/README.md +0 -0
  22. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/SECURITY.md +0 -0
  23. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/docs/architecture.md +0 -0
  24. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/docs/compatibility.md +0 -0
  25. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/docs/extension-specifications.md +0 -0
  26. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/docs/guide.md +0 -0
  27. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/docs/maintainer-architecture.md +0 -0
  28. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/mypy.ini +0 -0
  29. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/pyproject.toml +0 -0
  30. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/README.md +0 -0
  31. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/publish_github_release.sh +0 -0
  32. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/smoke_test_built_cli.sh +0 -0
  33. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/sync_codex_docs.sh +0 -0
  34. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/validate_baseline.sh +0 -0
  35. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/scripts/validate_runtime_matrix.sh +0 -0
  36. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/setup.cfg +0 -0
  37. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/__init__.py +0 -0
  38. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/auth.py +0 -0
  39. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/cli.py +0 -0
  40. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/__init__.py +0 -0
  41. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/agent_card.py +0 -0
  42. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/auth.py +0 -0
  43. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/client.py +0 -0
  44. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/config.py +0 -0
  45. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/errors.py +0 -0
  46. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/manager.py +0 -0
  47. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/payload_text.py +0 -0
  48. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/request_context.py +0 -0
  49. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/client/types.py +0 -0
  50. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/config.py +0 -0
  51. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/contracts/__init__.py +0 -0
  52. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/contracts/extensions.py +0 -0
  53. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/contracts/runtime_output.py +0 -0
  54. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/__init__.py +0 -0
  55. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/cancellation.py +0 -0
  56. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/directory_policy.py +0 -0
  57. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/discovery_runtime.py +0 -0
  58. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/exec_runtime.py +0 -0
  59. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/executor.py +0 -0
  60. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/output_mapping.py +0 -0
  61. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/request_metadata.py +0 -0
  62. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/request_overrides.py +0 -0
  63. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/response_emitter.py +0 -0
  64. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/review_runtime.py +0 -0
  65. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/session_runtime.py +0 -0
  66. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/stream_chunks.py +0 -0
  67. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/stream_processor.py +0 -0
  68. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/stream_state.py +0 -0
  69. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/streaming.py +0 -0
  70. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/thread_lifecycle_runtime.py +0 -0
  71. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/execution/tool_call_payloads.py +0 -0
  72. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/input_mapping.py +0 -0
  73. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/__init__.py +0 -0
  74. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/application.py +0 -0
  75. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/control_params.py +0 -0
  76. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/discovery_control.py +0 -0
  77. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/discovery_params.py +0 -0
  78. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/discovery_payload_mapping.py +0 -0
  79. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/discovery_query.py +0 -0
  80. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/dispatch.py +0 -0
  81. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/errors.py +0 -0
  82. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/exec_control.py +0 -0
  83. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/hooks.py +0 -0
  84. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/interrupt_lifecycle.py +0 -0
  85. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/interrupt_params.py +0 -0
  86. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/interrupt_recovery.py +0 -0
  87. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/interrupt_recovery_params.py +0 -0
  88. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/interrupts.py +0 -0
  89. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/params.py +0 -0
  90. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/params_common.py +0 -0
  91. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/payload_mapping.py +0 -0
  92. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/query_params.py +0 -0
  93. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/review_control.py +0 -0
  94. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/review_control_params.py +0 -0
  95. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/thread_lifecycle_control.py +0 -0
  96. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/thread_lifecycle_params.py +0 -0
  97. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/turn_control.py +0 -0
  98. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/jsonrpc/turn_control_params.py +0 -0
  99. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/logging_context.py +0 -0
  100. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/media_modes.py +0 -0
  101. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/metrics.py +0 -0
  102. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/parts/text.py +0 -0
  103. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/profile/__init__.py +0 -0
  104. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/profile/runtime.py +0 -0
  105. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/py.typed +0 -0
  106. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/__init__.py +0 -0
  107. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/agent_card.py +0 -0
  108. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/application.py +0 -0
  109. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/call_context.py +0 -0
  110. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/database.py +0 -0
  111. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/http_middlewares.py +0 -0
  112. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/migrations.py +0 -0
  113. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/openapi.py +0 -0
  114. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/output_negotiation.py +0 -0
  115. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/request_handler.py +0 -0
  116. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/runtime_state.py +0 -0
  117. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/server/task_store.py +0 -0
  118. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/__init__.py +0 -0
  119. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/client.py +0 -0
  120. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/exec_facade.py +0 -0
  121. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/interrupt_bridge.py +0 -0
  122. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/notification_mapping.py +0 -0
  123. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/request_mapping.py +0 -0
  124. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/startup.py +0 -0
  125. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/stream_bridge.py +0 -0
  126. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a/upstream/transport.py +0 -0
  127. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a.egg-info/SOURCES.txt +0 -0
  128. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a.egg-info/dependency_links.txt +0 -0
  129. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a.egg-info/entry_points.txt +0 -0
  130. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a.egg-info/requires.txt +0 -0
  131. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/src/codex_a2a.egg-info/top_level.txt +0 -0
  132. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/__init__.py +0 -0
  133. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/__init__.py +0 -0
  134. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_agent_card.py +0 -0
  135. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_cli.py +0 -0
  136. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_client_flow.py +0 -0
  137. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_client_manager.py +0 -0
  138. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_errors.py +0 -0
  139. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_lifecycle.py +0 -0
  140. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_payload_text.py +0 -0
  141. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_request_context.py +0 -0
  142. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/client/test_types.py +0 -0
  143. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/config/__init__.py +0 -0
  144. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/config/test_settings.py +0 -0
  145. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/contracts/__init__.py +0 -0
  146. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/contracts/test_extension_contract_consistency.py +0 -0
  147. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/__init__.py +0 -0
  148. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_agent_errors.py +0 -0
  149. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_cancellation.py +0 -0
  150. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_codex_agent_session_binding.py +0 -0
  151. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_directory_validation.py +0 -0
  152. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_discovery_exec_runtime.py +0 -0
  153. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_metrics.py +0 -0
  154. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_review_runtime.py +0 -0
  155. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_session_ownership.py +0 -0
  156. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_session_persistence.py +0 -0
  157. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_session_runtime.py +0 -0
  158. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_stream_chunks.py +0 -0
  159. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_streaming_output_contract.py +0 -0
  160. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_thread_lifecycle_runtime.py +0 -0
  161. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/execution/test_tool_call_payloads.py +0 -0
  162. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/fixtures/codex_app_server/command_execution_output_delta.json +0 -0
  163. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/fixtures/codex_app_server/file_change_output_delta.json +0 -0
  164. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/__init__.py +0 -0
  165. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_codex_discovery_extension.py +0 -0
  166. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_codex_exec_extension.py +0 -0
  167. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_codex_thread_lifecycle_extension.py +0 -0
  168. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_codex_turn_review_extension.py +0 -0
  169. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_dispatch.py +0 -0
  170. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_guard_hooks.py +0 -0
  171. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/jsonrpc/test_jsonrpc_models.py +0 -0
  172. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/__init__.py +0 -0
  173. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/test_logging.py +0 -0
  174. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/test_publish_github_release_script.py +0 -0
  175. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/test_release_distribution_contract.py +0 -0
  176. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/test_typing_contract.py +0 -0
  177. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/package/test_version.py +0 -0
  178. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/parts/__init__.py +0 -0
  179. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/parts/test_text.py +0 -0
  180. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/profile/__init__.py +0 -0
  181. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/profile/test_profile.py +0 -0
  182. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/__init__.py +0 -0
  183. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_agent_card.py +0 -0
  184. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_auth.py +0 -0
  185. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_call_context_builder.py +0 -0
  186. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_cli.py +0 -0
  187. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_database.py +0 -0
  188. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_database_app_persistence.py +0 -0
  189. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_migrations.py +0 -0
  190. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_request_handler.py +0 -0
  191. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_runtime_state.py +0 -0
  192. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_task_store.py +0 -0
  193. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/server/test_transport_contract.py +0 -0
  194. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/support/__init__.py +0 -0
  195. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/support/context.py +0 -0
  196. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/support/dummy_clients.py +0 -0
  197. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/support/fixtures.py +0 -0
  198. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/support/settings.py +0 -0
  199. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/test_input_mapping.py +0 -0
  200. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/upstream/__init__.py +0 -0
  201. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/tests/upstream/test_interrupt_persistence.py +0 -0
  202. {codex_a2a-0.6.0 → codex_a2a-0.6.1}/uv.lock +0 -0
@@ -43,6 +43,8 @@ logs/
43
43
  run/
44
44
  .codex-a2a/
45
45
  *.db
46
+ *.db-shm
47
+ *.db-wal
46
48
 
47
49
  # Unit test / coverage reports
48
50
  htmlcov/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-a2a
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Codex A2A runtime adapter
5
5
  Author: liujuanjuan1984@Intelligent-Internet
6
6
  License-Expression: Apache-2.0
@@ -3,6 +3,8 @@ from __future__ import annotations
3
3
  from collections.abc import Mapping
4
4
  from typing import Any
5
5
 
6
+ from codex_a2a.upstream.interrupts import resolve_permission_interrupt_semantic
7
+
6
8
  _INTERRUPT_ASKED_EVENT_TYPES = {
7
9
  "permission.asked",
8
10
  "question.asked",
@@ -189,12 +191,18 @@ def extract_interrupt_asked_event(event: Mapping[str, Any]) -> dict[str, Any] |
189
191
  if not normalized_request_id:
190
192
  return None
191
193
  if event_type == "permission.asked":
192
- details: dict[str, Any] = {
193
- "permission": _first_nested_string(
194
+ permission = _first_nested_string(
195
+ props,
196
+ ("permission",),
197
+ ("metadata", "raw", "permission"),
198
+ ) or resolve_permission_interrupt_semantic(
199
+ _first_nested_string(
194
200
  props,
195
- ("permission",),
196
- ("metadata", "raw", "permission"),
197
- ),
201
+ ("metadata", "method"),
202
+ )
203
+ )
204
+ details: dict[str, Any] = {
205
+ "permission": permission,
198
206
  "patterns": extract_interrupt_patterns(props),
199
207
  "always": extract_string_list(_nested_value(props, "always"))
200
208
  or extract_string_list(_nested_value(props, "metadata", "raw", "always")),
@@ -35,6 +35,7 @@ from codex_a2a.jsonrpc.payload_mapping import (
35
35
  as_a2a_message,
36
36
  message_to_item,
37
37
  )
38
+ from codex_a2a.upstream.models import CodexRPCError, is_thread_not_found_error
38
39
 
39
40
  if TYPE_CHECKING:
40
41
  from codex_a2a.jsonrpc.application import CodexSessionQueryJSONRPCApplication
@@ -187,6 +188,21 @@ async def handle_session_control_request(
187
188
  data={"type": "UPSTREAM_UNREACHABLE", "session_id": session_id},
188
189
  ),
189
190
  )
191
+ except CodexRPCError as exc:
192
+ if is_thread_not_found_error(exc):
193
+ return app._generate_error_response(
194
+ base_request.id,
195
+ JSONRPCError(
196
+ code=ERR_SESSION_NOT_FOUND,
197
+ message="Session not found",
198
+ data={"type": "SESSION_NOT_FOUND", "session_id": session_id},
199
+ ),
200
+ )
201
+ logger.exception("Codex session control JSON-RPC method failed")
202
+ return app._generate_error_response(
203
+ base_request.id,
204
+ A2AError(root=InternalError(message=str(exc))),
205
+ )
190
206
  except Exception as exc:
191
207
  logger.exception("Codex session control JSON-RPC method failed")
192
208
  return app._generate_error_response(
@@ -24,6 +24,7 @@ from codex_a2a.jsonrpc.payload_mapping import (
24
24
  as_a2a_session_task,
25
25
  extract_raw_items,
26
26
  )
27
+ from codex_a2a.upstream.models import CodexRPCError, is_thread_not_found_error
27
28
 
28
29
  if TYPE_CHECKING:
29
30
  from codex_a2a.jsonrpc.application import CodexSessionQueryJSONRPCApplication
@@ -81,6 +82,21 @@ async def handle_session_query_request(
81
82
  data={"type": "UPSTREAM_UNREACHABLE"},
82
83
  ),
83
84
  )
85
+ except CodexRPCError as exc:
86
+ if is_thread_not_found_error(exc) and session_id is not None:
87
+ return app._generate_error_response(
88
+ base_request.id,
89
+ JSONRPCError(
90
+ code=ERR_SESSION_NOT_FOUND,
91
+ message="Session not found",
92
+ data={"type": "SESSION_NOT_FOUND", "session_id": session_id},
93
+ ),
94
+ )
95
+ logger.exception("Codex session query JSON-RPC method failed")
96
+ return app._generate_error_response(
97
+ base_request.id,
98
+ A2AError(root=InternalError(message=str(exc))),
99
+ )
84
100
  except Exception as exc:
85
101
  logger.exception("Codex session query JSON-RPC method failed")
86
102
  return app._generate_error_response(
@@ -7,7 +7,12 @@ from typing import Any
7
7
 
8
8
  from codex_a2a.execution.request_overrides import RequestExecutionOptions
9
9
  from codex_a2a.input_mapping import build_turn_input_from_normalized_items
10
- from codex_a2a.upstream.models import CodexMessage, _TurnTracker
10
+ from codex_a2a.upstream.models import (
11
+ CodexMessage,
12
+ CodexRPCError,
13
+ _TurnTracker,
14
+ is_thread_not_found_error,
15
+ )
11
16
  from codex_a2a.upstream.request_mapping import (
12
17
  apply_thread_start_execution_options,
13
18
  apply_turn_start_execution_options,
@@ -37,6 +42,8 @@ class CodexConversationFacade:
37
42
  self._rpc_request = rpc_request
38
43
  self._get_or_create_tracker = get_or_create_tracker
39
44
  self._turn_trackers = turn_trackers
45
+ self._loaded_thread_ids: set[str] = set()
46
+ self._thread_resume_locks: dict[str, asyncio.Lock] = {}
40
47
 
41
48
  async def create_session(
42
49
  self,
@@ -67,7 +74,9 @@ class CodexConversationFacade:
67
74
  session_id = thread.get("id")
68
75
  if not isinstance(session_id, str) or not session_id.strip():
69
76
  raise RuntimeError("codex thread/start response missing thread id")
70
- return session_id.strip()
77
+ session_id = session_id.strip()
78
+ self._loaded_thread_ids.add(session_id)
79
+ return session_id
71
80
 
72
81
  async def list_sessions(self, *, query: dict[str, Any]) -> list[dict[str, Any]]:
73
82
  rpc_params: dict[str, Any] = {}
@@ -98,6 +107,7 @@ class CodexConversationFacade:
98
107
  thread = normalize_thread_summary(result.get("thread"))
99
108
  if thread is None:
100
109
  raise RuntimeError("codex thread/fork response missing thread")
110
+ self._loaded_thread_ids.add(thread["id"])
101
111
  return thread
102
112
 
103
113
  async def thread_archive(self, thread_id: str) -> None:
@@ -113,6 +123,7 @@ class CodexConversationFacade:
113
123
  thread = normalize_thread_summary(result.get("thread"))
114
124
  if thread is None:
115
125
  raise RuntimeError("codex thread/unarchive response missing thread")
126
+ self._loaded_thread_ids.add(thread["id"])
116
127
  return thread
117
128
 
118
129
  async def thread_metadata_update(
@@ -216,7 +227,12 @@ class CodexConversationFacade:
216
227
  default_model_id=self._model_id,
217
228
  )
218
229
 
219
- result = await self._rpc_request("turn/start", params)
230
+ result = await self._start_turn(
231
+ session_id,
232
+ params,
233
+ directory=directory,
234
+ execution_options=execution_options,
235
+ )
220
236
  if not isinstance(result, dict):
221
237
  raise RuntimeError("codex turn/start response missing result object")
222
238
  turn = result.get("turn")
@@ -268,7 +284,12 @@ class CodexConversationFacade:
268
284
  execution_options=coerce_request_execution_options(execution_options),
269
285
  default_model_id=self._model_id,
270
286
  )
271
- result = await self._rpc_request("turn/start", params)
287
+ result = await self._start_turn(
288
+ session_id,
289
+ params,
290
+ directory=directory,
291
+ execution_options=execution_options,
292
+ )
272
293
  if not isinstance(result, dict):
273
294
  raise RuntimeError("codex turn/start response missing result object")
274
295
  turn = result.get("turn")
@@ -279,6 +300,96 @@ class CodexConversationFacade:
279
300
  raise RuntimeError("codex turn/start response missing turn id")
280
301
  return {"ok": True, "session_id": session_id, "turn_id": turn_id.strip()}
281
302
 
303
+ async def _start_turn(
304
+ self,
305
+ session_id: str,
306
+ params: dict[str, Any],
307
+ *,
308
+ directory: str | None,
309
+ execution_options: RequestExecutionOptions | None,
310
+ ) -> Any:
311
+ await self._ensure_thread_loaded(
312
+ session_id,
313
+ directory=directory,
314
+ execution_options=execution_options,
315
+ )
316
+ try:
317
+ result = await self._rpc_request("turn/start", params)
318
+ except CodexRPCError as exc:
319
+ if not is_thread_not_found_error(exc):
320
+ raise
321
+ self._loaded_thread_ids.discard(session_id)
322
+ await self._ensure_thread_loaded(
323
+ session_id,
324
+ directory=directory,
325
+ execution_options=execution_options,
326
+ force_resume=True,
327
+ )
328
+ result = await self._rpc_request("turn/start", params)
329
+ self._loaded_thread_ids.add(session_id)
330
+ return result
331
+
332
+ async def _ensure_thread_loaded(
333
+ self,
334
+ session_id: str,
335
+ *,
336
+ directory: str | None,
337
+ execution_options: RequestExecutionOptions | None,
338
+ force_resume: bool = False,
339
+ ) -> None:
340
+ if not force_resume and session_id in self._loaded_thread_ids:
341
+ return
342
+
343
+ lock = self._thread_resume_locks.setdefault(session_id, asyncio.Lock())
344
+ async with lock:
345
+ if not force_resume and session_id in self._loaded_thread_ids:
346
+ return
347
+
348
+ if not force_resume:
349
+ loaded_threads = await self._list_loaded_thread_ids()
350
+ self._loaded_thread_ids.update(loaded_threads)
351
+ if session_id in self._loaded_thread_ids:
352
+ return
353
+
354
+ await self._resume_thread(
355
+ session_id,
356
+ directory=directory,
357
+ execution_options=execution_options,
358
+ )
359
+ self._loaded_thread_ids.add(session_id)
360
+
361
+ async def _list_loaded_thread_ids(self) -> set[str]:
362
+ result = await self._rpc_request("thread/loaded/list", {})
363
+ if not isinstance(result, dict):
364
+ return set()
365
+ data = result.get("data")
366
+ if not isinstance(data, list):
367
+ return set()
368
+ loaded: set[str] = set()
369
+ for item in data:
370
+ if isinstance(item, str) and item.strip():
371
+ loaded.add(item.strip())
372
+ return loaded
373
+
374
+ async def _resume_thread(
375
+ self,
376
+ session_id: str,
377
+ *,
378
+ directory: str | None,
379
+ execution_options: RequestExecutionOptions | None,
380
+ ) -> Any:
381
+ params: dict[str, Any] = {"threadId": session_id}
382
+ if directory:
383
+ params["cwd"] = directory
384
+ elif self._workspace_root:
385
+ params["cwd"] = self._workspace_root
386
+ apply_thread_start_execution_options(
387
+ params,
388
+ execution_options=coerce_request_execution_options(execution_options),
389
+ default_model_id=self._model_id,
390
+ )
391
+ return await self._rpc_request("thread/resume", params)
392
+
282
393
  async def turn_steer(
283
394
  self,
284
395
  thread_id: str,
@@ -5,6 +5,13 @@ from collections.abc import Mapping
5
5
  from dataclasses import dataclass
6
6
  from typing import Any
7
7
 
8
+ _PERMISSION_INTERRUPT_METHOD_MAP = {
9
+ "item/commandExecution/requestApproval": "command_execution",
10
+ "execCommandApproval": "command_execution",
11
+ "item/fileChange/requestApproval": "file_change",
12
+ "applyPatchApproval": "apply_patch",
13
+ }
14
+
8
15
 
9
16
  def _normalized_string(value: Any) -> str | None:
10
17
  if not isinstance(value, str):
@@ -87,6 +94,13 @@ def _extract_mapping(params: dict[str, Any], key: str) -> dict[str, Any] | None:
87
94
  return dict(value)
88
95
 
89
96
 
97
+ def resolve_permission_interrupt_semantic(method: str | None) -> str | None:
98
+ normalized_method = _normalized_string(method)
99
+ if normalized_method is None:
100
+ return None
101
+ return _PERMISSION_INTERRUPT_METHOD_MAP.get(normalized_method)
102
+
103
+
90
104
  class InterruptRequestError(RuntimeError):
91
105
  def __init__(
92
106
  self,
@@ -149,6 +163,9 @@ def build_codex_permission_interrupt_properties(
149
163
  )
150
164
  if display_message is not None:
151
165
  properties["display_message"] = display_message
166
+ permission = resolve_permission_interrupt_semantic(method)
167
+ if permission is not None:
168
+ properties["permission"] = permission
152
169
  patterns = _extract_permission_patterns(params)
153
170
  if patterns:
154
171
  properties["patterns"] = patterns
@@ -24,6 +24,10 @@ class CodexRPCError(RuntimeError):
24
24
  self.data = data
25
25
 
26
26
 
27
+ def is_thread_not_found_error(exc: BaseException) -> bool:
28
+ return isinstance(exc, CodexRPCError) and "thread not found" in str(exc).strip().lower()
29
+
30
+
27
31
  @dataclass
28
32
  class _PendingRpcRequest:
29
33
  request_id: str
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-a2a
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Codex A2A runtime adapter
5
5
  Author: liujuanjuan1984@Intelligent-Internet
6
6
  License-Expression: Apache-2.0
@@ -48,10 +48,11 @@ def test_extract_permission_interrupt_promotes_reason_and_parsed_paths() -> None
48
48
  "properties": {
49
49
  "id": "perm-2",
50
50
  "metadata": {
51
+ "method": "execCommandApproval",
51
52
  "raw": {
52
53
  "reason": "The command needs confirmation.",
53
54
  "parsedCmd": [{"path": "/repo/.env"}],
54
- }
55
+ },
55
56
  },
56
57
  },
57
58
  }
@@ -60,7 +61,7 @@ def test_extract_permission_interrupt_promotes_reason_and_parsed_paths() -> None
60
61
  "request_id": "perm-2",
61
62
  "interrupt_type": "permission",
62
63
  "details": {
63
- "permission": None,
64
+ "permission": "command_execution",
64
65
  "patterns": ["/repo/.env"],
65
66
  "always": [],
66
67
  "display_message": "The command needs confirmation.",
@@ -68,6 +69,34 @@ def test_extract_permission_interrupt_promotes_reason_and_parsed_paths() -> None
68
69
  }
69
70
 
70
71
 
72
+ def test_extract_permission_interrupt_derives_semantic_from_method_when_needed() -> None:
73
+ event = {
74
+ "type": "permission.asked",
75
+ "properties": {
76
+ "id": "perm-3",
77
+ "metadata": {
78
+ "method": "item/fileChange/requestApproval",
79
+ "raw": {
80
+ "request": {
81
+ "description": "Agent wants to update the patch.",
82
+ }
83
+ },
84
+ },
85
+ },
86
+ }
87
+
88
+ assert extract_interrupt_asked_event(event) == {
89
+ "request_id": "perm-3",
90
+ "interrupt_type": "permission",
91
+ "details": {
92
+ "permission": "file_change",
93
+ "patterns": [],
94
+ "always": [],
95
+ "display_message": "Agent wants to update the patch.",
96
+ },
97
+ }
98
+
99
+
71
100
  def test_extract_question_interrupt_promotes_nested_questions() -> None:
72
101
  event = {
73
102
  "type": "question.asked",
@@ -25,6 +25,7 @@ from codex_a2a.jsonrpc.application import CodexSessionQueryJSONRPCApplication
25
25
  from codex_a2a.jsonrpc.hooks import SessionGuardHooks
26
26
  from codex_a2a.profile.runtime import build_runtime_profile
27
27
  from codex_a2a.server.agent_card import build_agent_card
28
+ from codex_a2a.upstream.models import CodexRPCError
28
29
  from tests.support.dummy_clients import DummySessionQueryCodexClient as DummyCodexClient
29
30
  from tests.support.settings import make_settings
30
31
 
@@ -565,6 +566,39 @@ async def test_session_query_extension_maps_404_to_session_not_found(monkeypatch
565
566
  assert payload["error"]["data"]["type"] == "SESSION_NOT_FOUND"
566
567
 
567
568
 
569
+ @pytest.mark.asyncio
570
+ async def test_session_query_extension_maps_thread_not_found_to_session_not_found(monkeypatch):
571
+ import codex_a2a.server.application as app_module
572
+
573
+ class NotFoundCodexClient(DummyCodexClient):
574
+ async def list_messages(self, session_id: str, *, params=None):
575
+ raise CodexRPCError(code=-32000, message=f"thread not found: {session_id}")
576
+
577
+ monkeypatch.setattr(app_module, "CodexClient", NotFoundCodexClient)
578
+ app = app_module.create_app(
579
+ make_settings(a2a_bearer_token="t-1", a2a_log_payloads=False, **_BASE_SETTINGS)
580
+ )
581
+
582
+ transport = httpx.ASGITransport(app=app)
583
+ async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
584
+ headers = {"Authorization": "Bearer t-1"}
585
+ resp = await client.post(
586
+ "/",
587
+ headers=headers,
588
+ json={
589
+ "jsonrpc": "2.0",
590
+ "id": 3,
591
+ "method": "codex.sessions.messages.list",
592
+ "params": {"session_id": "s-rpc-404"},
593
+ },
594
+ )
595
+ payload = resp.json()
596
+ assert payload["jsonrpc"] == "2.0"
597
+ assert payload["id"] == 3
598
+ assert payload["error"]["code"] == -32001
599
+ assert payload["error"]["data"]["type"] == "SESSION_NOT_FOUND"
600
+
601
+
568
602
  @pytest.mark.asyncio
569
603
  async def test_session_query_extension_does_not_log_response_bodies(monkeypatch, caplog):
570
604
  import codex_a2a.server.application as app_module
@@ -641,6 +675,48 @@ async def test_session_control_prompt_async_returns_turn_handle(monkeypatch):
641
675
  }
642
676
 
643
677
 
678
+ @pytest.mark.asyncio
679
+ async def test_session_control_prompt_async_maps_thread_not_found_to_session_not_found(monkeypatch):
680
+ import codex_a2a.server.application as app_module
681
+
682
+ class NotFoundCodexClient(DummyCodexClient):
683
+ async def session_prompt_async(
684
+ self,
685
+ session_id: str,
686
+ request: dict[str, Any],
687
+ *,
688
+ directory: str | None = None,
689
+ execution_options=None,
690
+ ):
691
+ del request, directory, execution_options
692
+ raise CodexRPCError(code=-32000, message=f"thread not found: {session_id}")
693
+
694
+ monkeypatch.setattr(app_module, "CodexClient", NotFoundCodexClient)
695
+ app = app_module.create_app(
696
+ make_settings(a2a_bearer_token="t-1", a2a_log_payloads=False, **_BASE_SETTINGS)
697
+ )
698
+
699
+ transport = httpx.ASGITransport(app=app)
700
+ async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
701
+ headers = {"Authorization": "Bearer t-1"}
702
+ resp = await client.post(
703
+ "/",
704
+ headers=headers,
705
+ json={
706
+ "jsonrpc": "2.0",
707
+ "id": 22,
708
+ "method": "codex.sessions.prompt_async",
709
+ "params": {
710
+ "session_id": "s-rpc-404",
711
+ "request": {"parts": [{"type": "text", "text": "resume"}]},
712
+ },
713
+ },
714
+ )
715
+ payload = resp.json()
716
+ assert payload["error"]["code"] == -32001
717
+ assert payload["error"]["data"]["type"] == "SESSION_NOT_FOUND"
718
+
719
+
644
720
  @pytest.mark.asyncio
645
721
  async def test_session_control_prompt_async_accepts_rich_input(monkeypatch):
646
722
  import codex_a2a.server.application as app_module
@@ -1163,6 +1239,7 @@ async def test_interrupt_recovery_extension_lists_identity_scoped_pending_reques
1163
1239
  "properties": {
1164
1240
  "id": "perm-1",
1165
1241
  "sessionID": "ses-1",
1242
+ "permission": "command_execution",
1166
1243
  "metadata": {
1167
1244
  "method": "item/commandExecution/requestApproval",
1168
1245
  "raw": {"patterns": ["/tmp/config.yml"]},
@@ -17,6 +17,7 @@ from codex_a2a.upstream.client import (
17
17
  _PendingInterruptRequest,
18
18
  _PendingRpcRequest,
19
19
  )
20
+ from codex_a2a.upstream.models import CodexRPCError
20
21
  from tests.support.fixtures import (
21
22
  replay_codex_jsonrpc_line_fixture,
22
23
  replay_codex_notification_fixture,
@@ -262,6 +263,7 @@ async def test_session_prompt_async_maps_rich_input_parts_to_turn_start() -> Non
262
263
  return {"turn": {"id": "turn-42"}}
263
264
 
264
265
  client._rpc_request = fake_rpc_request
266
+ client._conversation_facade._loaded_thread_ids.add("thr-1")
265
267
 
266
268
  result = await client.session_prompt_async(
267
269
  "thr-1",
@@ -304,6 +306,105 @@ async def test_session_prompt_async_maps_rich_input_parts_to_turn_start() -> Non
304
306
  ]
305
307
 
306
308
 
309
+ @pytest.mark.asyncio
310
+ async def test_session_prompt_async_resumes_unloaded_thread_before_turn_start() -> None:
311
+ client = CodexClient(
312
+ make_settings(
313
+ a2a_bearer_token="t-1",
314
+ codex_workspace_root="/safe",
315
+ codex_timeout=1.0,
316
+ codex_model_id="gpt-5.2-codex",
317
+ )
318
+ )
319
+
320
+ seen: list[tuple[str, dict | None]] = []
321
+
322
+ async def fake_rpc_request(method: str, params: dict | None = None):
323
+ seen.append((method, params))
324
+ if method == "thread/loaded/list":
325
+ return {"data": [], "nextCursor": None}
326
+ if method == "thread/resume":
327
+ return {"thread": {"id": "thr-1"}}
328
+ return {"turn": {"id": "turn-42"}}
329
+
330
+ client._rpc_request = fake_rpc_request
331
+
332
+ result = await client.session_prompt_async(
333
+ "thr-1",
334
+ {"parts": [{"type": "text", "text": "Use the app."}]},
335
+ )
336
+
337
+ assert result == {"ok": True, "session_id": "thr-1", "turn_id": "turn-42"}
338
+ assert seen == [
339
+ ("thread/loaded/list", {}),
340
+ ("thread/resume", {"threadId": "thr-1", "cwd": "/safe", "model": "gpt-5.2-codex"}),
341
+ (
342
+ "turn/start",
343
+ {
344
+ "threadId": "thr-1",
345
+ "input": [{"type": "text", "text": "Use the app.", "text_elements": []}],
346
+ "cwd": "/safe",
347
+ "model": "gpt-5.2-codex",
348
+ },
349
+ ),
350
+ ]
351
+
352
+
353
+ @pytest.mark.asyncio
354
+ async def test_session_prompt_async_retries_turn_start_after_thread_not_found() -> None:
355
+ client = CodexClient(
356
+ make_settings(
357
+ a2a_bearer_token="t-1",
358
+ codex_workspace_root="/safe",
359
+ codex_timeout=1.0,
360
+ )
361
+ )
362
+
363
+ seen: list[tuple[str, dict | None]] = []
364
+ turn_attempts = 0
365
+
366
+ async def fake_rpc_request(method: str, params: dict | None = None):
367
+ nonlocal turn_attempts
368
+ seen.append((method, params))
369
+ if method == "turn/start":
370
+ turn_attempts += 1
371
+ if turn_attempts == 1:
372
+ raise CodexRPCError(code=-32000, message="thread not found: thr-1")
373
+ return {"turn": {"id": "turn-43"}}
374
+ if method == "thread/resume":
375
+ return {"thread": {"id": "thr-1"}}
376
+ return {}
377
+
378
+ client._rpc_request = fake_rpc_request
379
+ client._conversation_facade._loaded_thread_ids.add("thr-1")
380
+
381
+ result = await client.session_prompt_async(
382
+ "thr-1",
383
+ {"parts": [{"type": "text", "text": "Retry after resume."}]},
384
+ )
385
+
386
+ assert result == {"ok": True, "session_id": "thr-1", "turn_id": "turn-43"}
387
+ assert seen == [
388
+ (
389
+ "turn/start",
390
+ {
391
+ "threadId": "thr-1",
392
+ "input": [{"type": "text", "text": "Retry after resume.", "text_elements": []}],
393
+ "cwd": "/safe",
394
+ },
395
+ ),
396
+ ("thread/resume", {"threadId": "thr-1", "cwd": "/safe"}),
397
+ (
398
+ "turn/start",
399
+ {
400
+ "threadId": "thr-1",
401
+ "input": [{"type": "text", "text": "Retry after resume.", "text_elements": []}],
402
+ "cwd": "/safe",
403
+ },
404
+ ),
405
+ ]
406
+
407
+
307
408
  @pytest.mark.asyncio
308
409
  async def test_exec_start_uses_interactive_command_exec_params() -> None:
309
410
  client = CodexClient(
@@ -1375,6 +1476,7 @@ async def test_send_message_uses_structured_input_items_when_provided() -> None:
1375
1476
  return {"turn": {"id": "turn-1"}}
1376
1477
 
1377
1478
  client._rpc_request = fake_rpc_request
1479
+ client._conversation_facade._loaded_thread_ids.add("thr-1")
1378
1480
  tracker = client._get_or_create_tracker("thr-1", "turn-1")
1379
1481
  tracker.text_chunks.append("done")
1380
1482
  tracker.completed.set()
@@ -1426,6 +1528,7 @@ async def test_send_message_request_execution_options_are_forwarded_to_turn_star
1426
1528
  return {"turn": {"id": "turn-1"}}
1427
1529
 
1428
1530
  client._rpc_request = fake_rpc_request
1531
+ client._conversation_facade._loaded_thread_ids.add("thr-1")
1429
1532
  tracker = client._get_or_create_tracker("thr-1", "turn-1")
1430
1533
  tracker.text_chunks.append("done")
1431
1534
  tracker.completed.set()
@@ -1513,8 +1616,8 @@ async def test_permission_request_emits_shared_message_and_patterns() -> None:
1513
1616
  assert props["id"] == "301"
1514
1617
  assert props["sessionID"] == "thr-1"
1515
1618
  assert props["display_message"] == "The command needs confirmation before continuing."
1619
+ assert props["permission"] == "command_execution"
1516
1620
  assert props["patterns"] == ["/repo/.env"]
1517
- assert "permission" not in props
1518
1621
  assert "always" not in props
1519
1622
  assert "reason" not in props
1520
1623
  assert props["metadata"]["raw"]["parsedCmd"] == [
@@ -1676,6 +1779,7 @@ async def test_permission_request_promotes_nested_request_message() -> None:
1676
1779
  assert len(events) == 1
1677
1780
  props = events[0]["properties"]
1678
1781
  assert props["display_message"] == "Agent wants to read the environment file."
1782
+ assert props["permission"] == "command_execution"
1679
1783
  assert props["patterns"] == ["/repo/.env"]
1680
1784
  assert "request" not in props
1681
1785
  assert props["metadata"]["raw"]["request"] == {