claude-code-tools 1.16.0__tar.gz → 1.16.2__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 (114) hide show
  1. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/PKG-INFO +12 -9
  2. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/README.md +11 -8
  3. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/__init__.py +1 -1
  4. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/codex_server.py +96 -92
  5. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/codex_server_cli.py +23 -23
  6. claude_code_tools-1.16.2/claude_code_tools/codex_server_generation.py +52 -0
  7. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/codex_server_models.py +223 -3
  8. claude_code_tools-1.16.2/claude_code_tools/codex_server_reuse.py +90 -0
  9. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/codex_server_worker.py +3 -2
  10. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_cli.py +5 -5
  11. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_cli_contract.py +2 -0
  12. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_cli_rendering.py +31 -6
  13. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_cli_snapshots.py +21 -0
  14. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_validation.py +1 -0
  15. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/pyproject.toml +2 -2
  16. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/.gitignore +0 -0
  17. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/LICENSE +0 -0
  18. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/action_rpc.py +0 -0
  19. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/__init__.py +0 -0
  20. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/backends.py +0 -0
  21. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/cli.py +0 -0
  22. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/config.py +0 -0
  23. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/convert.py +0 -0
  24. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
  25. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/locking.py +0 -0
  26. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/paths.py +0 -0
  27. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/registry.py +0 -0
  28. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/session.py +0 -0
  29. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/store.py +0 -0
  30. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/tmux.py +0 -0
  31. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/agent_tunnel/trust.py +0 -0
  32. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/aichat.py +0 -0
  33. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/claude_continue.py +0 -0
  34. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/codex_continue.py +0 -0
  35. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/codex_server_fingerprint.py +0 -0
  36. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/codex_server_process.py +0 -0
  37. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/codex_server_supervisor.py +0 -0
  38. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/config.py +0 -0
  39. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/csv2gsheet.py +0 -0
  40. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/delete_session.py +0 -0
  41. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/dotenv_vault.py +0 -0
  42. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/env_safe.py +0 -0
  43. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/export_all.py +0 -0
  44. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/export_claude_session.py +0 -0
  45. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/export_codex_session.py +0 -0
  46. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/export_session.py +0 -0
  47. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/find_claude_session.py +0 -0
  48. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/find_codex_session.py +0 -0
  49. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/find_original_session.py +0 -0
  50. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/find_session.py +0 -0
  51. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/find_trimmed_sessions.py +0 -0
  52. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/fix_session.py +0 -0
  53. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/gdoc2docx.py +0 -0
  54. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/gdoc2md.py +0 -0
  55. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/gsheet2csv.py +0 -0
  56. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/md2gdoc.py +0 -0
  57. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/msg/__init__.py +0 -0
  58. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/msg/cli.py +0 -0
  59. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/msg/hooks.py +0 -0
  60. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/msg/models.py +0 -0
  61. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/msg/prompt_detect.py +0 -0
  62. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/msg/store.py +0 -0
  63. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/msg/watcher.py +0 -0
  64. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/node_menu_ui.py +0 -0
  65. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/search_index.py +0 -0
  66. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/session_lineage.py +0 -0
  67. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/session_menu.py +0 -0
  68. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/session_menu_cli.py +0 -0
  69. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/session_utils.py +0 -0
  70. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/smart_trim.py +0 -0
  71. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/smart_trim_core.py +0 -0
  72. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/tmux_cli_controller.py +0 -0
  73. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/tmux_execution_helpers.py +0 -0
  74. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/tmux_remote_controller.py +0 -0
  75. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/trim_in_place.py +0 -0
  76. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/trim_session.py +0 -0
  77. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/trim_session_claude.py +0 -0
  78. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/trim_session_codex.py +0 -0
  79. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_cli_formatting.py +0 -0
  80. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
  81. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_cli_manifest.py +0 -0
  82. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_cli_projection.py +0 -0
  83. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_cli_store_backends.py +0 -0
  84. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_processes.py +0 -0
  85. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_runs.py +0 -0
  86. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/claude_code_tools/workflow_store_io.py +0 -0
  87. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/agent-tunnel-spec.md +0 -0
  88. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/ai-agent-writing-skills.md +0 -0
  89. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/cc-codex-instructions.md +0 -0
  90. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/claude-code-chutes.md +0 -0
  91. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/claude-code-tmux-tutorials.md +0 -0
  92. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/dot-zshrc.md +0 -0
  93. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/ensemble-ideas.md +0 -0
  94. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/example-with-images.md +0 -0
  95. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/find-claude-session.md +0 -0
  96. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/intercom-spec.md +0 -0
  97. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/linked-in-20260102.md +0 -0
  98. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/lmsh.md +0 -0
  99. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/local-llm-setup.md +0 -0
  100. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/msg-phase-a-plan.md +0 -0
  101. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/msg-plan-v2.md +0 -0
  102. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/paper-writing-style-transfer-tools.md +0 -0
  103. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/reddit-aichat-resume-v2.md +0 -0
  104. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/reddit-aichat-resume.md +0 -0
  105. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/reddit-aichat.md +0 -0
  106. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/reddit-post.md +0 -0
  107. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/rollover-details.md +0 -0
  108. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/tmux-cli-instructions.md +0 -0
  109. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/docs/vault-documentation.md +0 -0
  110. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/hatch_build.py +0 -0
  111. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/node_ui/action_config.js +0 -0
  112. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/node_ui/menu.js +0 -0
  113. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/node_ui/package-lock.json +0 -0
  114. {claude_code_tools-1.16.0 → claude_code_tools-1.16.2}/node_ui/package.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-tools
3
- Version: 1.16.0
3
+ Version: 1.16.2
4
4
  Summary: Collection of tools for working with Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -98,10 +98,12 @@ codex-dynamic resume --last
98
98
  ```
99
99
 
100
100
  `codex-dynamic` starts or reuses the local app server and then hands the
101
- terminal directly to Codex. Use `codex-server status` or `logs` for
102
- nondisruptive inspection. A forced stop or restart disconnects every attached
103
- TUI, so exit those sessions before running `codex-server stop --force` or
104
- `codex-server restart --force` from an ordinary terminal.
101
+ terminal directly to Codex. After a plugin or Codex upgrade, the next launch
102
+ automatically starts a fresh server generation; existing sessions and workflow
103
+ callbacks stay connected to their old generation. No manual restart is needed.
104
+ Use `codex-server status` or `logs` for nondisruptive inspection. A forced stop
105
+ or restart cleans up every retained generation and disconnects its attached
106
+ TUIs, so use those commands only after exiting the sessions normally.
105
107
 
106
108
  The Python package also installs `codex-workflows`, an observational dashboard
107
109
  for durable workflow runs:
@@ -113,10 +115,11 @@ codex-workflows show RUN_ID
113
115
  codex-workflows --all
114
116
  ```
115
117
 
116
- The default list contains only active workflows. Use `--all` to include
117
- completed and diagnostic history. The command reads durable workflow state and
118
- process metadata. It never changes a run, sends a signal, repairs state, or
119
- launches an agent. See the
118
+ The default list contains only active workflows and identifies the project
119
+ folder from which each workflow was launched. Use `show RUN_ID` for its full
120
+ working directory, or `--all` to include completed and diagnostic history. The
121
+ command reads durable workflow state and process metadata. It never changes a
122
+ run, sends a signal, repairs state, or launches an agent. See the
120
123
  [codex-workflows reference][codex-workflows-reference] for filters, JSON output,
121
124
  and the complete versioned schema.
122
125
 
@@ -72,10 +72,12 @@ codex-dynamic resume --last
72
72
  ```
73
73
 
74
74
  `codex-dynamic` starts or reuses the local app server and then hands the
75
- terminal directly to Codex. Use `codex-server status` or `logs` for
76
- nondisruptive inspection. A forced stop or restart disconnects every attached
77
- TUI, so exit those sessions before running `codex-server stop --force` or
78
- `codex-server restart --force` from an ordinary terminal.
75
+ terminal directly to Codex. After a plugin or Codex upgrade, the next launch
76
+ automatically starts a fresh server generation; existing sessions and workflow
77
+ callbacks stay connected to their old generation. No manual restart is needed.
78
+ Use `codex-server status` or `logs` for nondisruptive inspection. A forced stop
79
+ or restart cleans up every retained generation and disconnects its attached
80
+ TUIs, so use those commands only after exiting the sessions normally.
79
81
 
80
82
  The Python package also installs `codex-workflows`, an observational dashboard
81
83
  for durable workflow runs:
@@ -87,10 +89,11 @@ codex-workflows show RUN_ID
87
89
  codex-workflows --all
88
90
  ```
89
91
 
90
- The default list contains only active workflows. Use `--all` to include
91
- completed and diagnostic history. The command reads durable workflow state and
92
- process metadata. It never changes a run, sends a signal, repairs state, or
93
- launches an agent. See the
92
+ The default list contains only active workflows and identifies the project
93
+ folder from which each workflow was launched. Use `show RUN_ID` for its full
94
+ working directory, or `--all` to include completed and diagnostic history. The
95
+ command reads durable workflow state and process metadata. It never changes a
96
+ run, sends a signal, repairs state, or launches an agent. See the
94
97
  [codex-workflows reference][codex-workflows-reference] for filters, JSON output,
95
98
  and the complete versioned schema.
96
99
 
@@ -1,3 +1,3 @@
1
1
  """Claude Code Tools - Collection of utilities for Claude Code."""
2
2
 
3
- __version__ = "1.16.0"
3
+ __version__ = "1.16.2"
@@ -26,10 +26,11 @@ from claude_code_tools.codex_server_fingerprint import (
26
26
  plugin_configuration_snapshot as _plugin_configuration_snapshot,
27
27
  read_plugin_configuration as _read_plugin_configuration, # noqa: F401
28
28
  )
29
+ from claude_code_tools.codex_server_generation import server_generation
29
30
 
30
31
  from claude_code_tools.codex_server_models import (
32
+ CODEX_SERVER_GENERATION_ENV,
31
33
  CODEX_SERVER_OPTIONS_ENV,
32
- ENDPOINT,
33
34
  FORCED_STOP_SECONDS,
34
35
  GRACEFUL_STOP_SECONDS,
35
36
  LOG_TRUNCATION_MARKER_MAX_BYTES,
@@ -43,14 +44,20 @@ from claude_code_tools.codex_server_models import (
43
44
  ServerProbe,
44
45
  ServerStatus,
45
46
  StateFileError,
47
+ all_server_paths,
48
+ base_paths_from_env,
49
+ clear_current_generation,
46
50
  log_tail,
47
51
  log_tail_stream,
48
52
  open_log_append,
49
53
  open_log_reader,
50
54
  paths_from_env,
55
+ paths_for_generation,
51
56
  prepare_runtime,
57
+ publish_current_generation,
52
58
  quarantine_invalid_state,
53
59
  read_state,
60
+ require_generation_capacity,
54
61
  remove_state,
55
62
  write_state,
56
63
  )
@@ -67,6 +74,11 @@ from claude_code_tools.codex_server_process import (
67
74
  wait_for_process_group_exit,
68
75
  wait_for_process_identity,
69
76
  )
77
+ from claude_code_tools.codex_server_reuse import (
78
+ disconnect_refusal as _disconnect_refusal,
79
+ helper_restart_reason as _reuse_restart_reason,
80
+ require_external_compatible as _external_compatible,
81
+ )
70
82
 
71
83
 
72
84
  UNKNOWN_SUBCOMMAND_MARKERS = (
@@ -166,6 +178,10 @@ def _command_env(env: Mapping[str, str], paths: ServerPaths) -> dict[str, str]:
166
178
  """Return a child environment pinned to the resolved Codex home."""
167
179
  child_env = dict(env)
168
180
  child_env["CODEX_HOME"] = str(paths.codex_home)
181
+ if paths.generation is None:
182
+ child_env.pop(CODEX_SERVER_GENERATION_ENV, None)
183
+ else:
184
+ child_env[CODEX_SERVER_GENERATION_ENV] = paths.generation
169
185
  return child_env
170
186
 
171
187
 
@@ -345,6 +361,13 @@ def _probe_server(
345
361
  paths: ServerPaths,
346
362
  ) -> ServerProbe:
347
363
  """Probe the app-server protocol, with an older-CLI socket fallback."""
364
+ if paths.generation is not None:
365
+ accepting = _socket_accepts(paths.socket_path)
366
+ return ServerProbe(
367
+ running=accepting,
368
+ method="socket" if accepting else None,
369
+ accepting=accepting,
370
+ )
348
371
  if codex_path:
349
372
  result = _run_command(
350
373
  [codex_path, "app-server", "daemon", "version"],
@@ -508,66 +531,21 @@ def _helper_restart_reason(
508
531
  probe: ServerProbe,
509
532
  plugin_fingerprint: str,
510
533
  paths: ServerPaths,
534
+ codex_options: Sequence[str] = (),
511
535
  ) -> str | None:
512
536
  """Explain why a helper listener cannot be safely reused."""
513
- if not state_controller_matches(state):
514
- return "its durable supervisor exited"
515
- if state.codex_path != codex_path:
516
- return "the active Codex executable path changed"
517
- if state.codex_executable_identity is None:
518
- return "its Codex executable identity predates replacement detection"
519
- if state.codex_executable_identity != codex_executable_identity:
520
- return "the active Codex executable was replaced"
521
- if _version_key(state.codex_version) != _version_key(codex_version):
522
- return "the active Codex CLI version changed"
523
- if probe.server_version is not None and _version_key(
524
- probe.server_version
525
- ) != _version_key(codex_version):
526
- return "the running app-server version differs from the Codex CLI"
527
- if (probe.running or probe.accepting) and not _listener_matches_worker(
537
+ return _reuse_restart_reason(
528
538
  state,
539
+ codex_path,
540
+ codex_executable_identity,
541
+ codex_version,
542
+ probe,
543
+ plugin_fingerprint,
529
544
  paths,
530
- ):
531
- return "its socket listener is not owned by the supervised worker"
532
- if state.plugin_fingerprint is None:
533
- return "its plugin snapshot predates plugin-change detection"
534
- if state.plugin_fingerprint != plugin_fingerprint:
535
- return "the Codex plugin or marketplace configuration changed"
536
- return None
537
-
538
-
539
- def _disconnect_refusal(action: str) -> CodexServerError:
540
- """Explain why a shared-server lifecycle action needs acknowledgement."""
541
- return CodexServerError(
542
- f"refusing to {action} the shared app server without --force: this "
543
- "disconnects every codex-dynamic TUI, and Codex exits those sessions. "
544
- "Exit connected sessions first, then retry with --force; their "
545
- "transcripts remain resumable"
546
- )
547
-
548
-
549
- def _require_external_compatible(
550
- codex_version: str,
551
- probe: ServerProbe,
552
- ) -> None:
553
- """Reject external listeners, whose plugin snapshot is uncertifiable."""
554
- server_key = _version_key(probe.server_version)
555
- cli_key = _version_key(codex_version)
556
- if server_key is None:
557
- raise CodexServerError(
558
- "an external app server is running, but its version could not be "
559
- "verified; stop it before using codex-server"
560
- )
561
- if server_key != cli_key:
562
- raise CodexServerError(
563
- f"external app-server version {probe.server_version!r} does not "
564
- f"match the selected Codex CLI {codex_version!r}; stop or restart "
565
- "the external server"
566
- )
567
- raise CodexServerError(
568
- "an external app server is running, but codex-server cannot verify "
569
- "which plugin snapshot it loaded; stop it before using "
570
- "codex-dynamic workflow callbacks"
545
+ codex_options,
546
+ _listener_matches_worker,
547
+ state_controller_matches,
548
+ _version_key,
571
549
  )
572
550
 
573
551
 
@@ -646,7 +624,9 @@ def _certify_helper_boundary(
646
624
  probe = _probe_server(codex_path, child_env, paths)
647
625
  if not probe.running:
648
626
  raise CodexServerError(f"the app-server listener vanished during {operation}")
649
- if _version_key(probe.server_version) != _version_key(codex_version):
627
+ if paths.generation is None and _version_key(probe.server_version) != _version_key(
628
+ codex_version
629
+ ):
650
630
  raise CodexServerError(
651
631
  f"the app-server version changed or was uncertified during {operation}"
652
632
  )
@@ -671,6 +651,10 @@ def _certified_helper_status(
671
651
  probe: ServerProbe,
672
652
  ) -> ServerStatus:
673
653
  """Build a helper result only from final certified evidence."""
654
+ version = probe.server_version
655
+ if version is None:
656
+ key = _version_key(state.codex_version)
657
+ version = ".".join(map(str, key)) if key is not None else None
674
658
  return ServerStatus(
675
659
  status="running",
676
660
  ownership="helper",
@@ -678,7 +662,7 @@ def _certified_helper_status(
678
662
  pid=state.pid,
679
663
  codex_path=state.codex_path,
680
664
  codex_version=state.codex_version,
681
- server_version=probe.server_version,
665
+ server_version=version,
682
666
  probe_method=probe.method,
683
667
  )
684
668
 
@@ -747,17 +731,34 @@ def ensure_server(
747
731
  ) -> ServerStatus:
748
732
  """Reuse a compatible listener or start a supervised app server."""
749
733
  active_env = dict(os.environ if env is None else env)
750
- paths = _paths(active_env)
734
+ base_paths = base_paths_from_env(active_env)
751
735
  codex_path = _resolve_codex(active_env)
752
736
  codex_executable_identity = _codex_executable_identity(codex_path)
737
+ base_child_env = _command_env(active_env, base_paths)
738
+ codex_version = _require_compatible_codex(codex_path, base_child_env)
739
+ plugin_snapshot = _plugin_configuration_snapshot(base_paths, codex_options)
740
+ generation = server_generation(
741
+ codex_path,
742
+ codex_executable_identity,
743
+ codex_version,
744
+ plugin_snapshot.fingerprint,
745
+ codex_options,
746
+ )
747
+ paths = paths_for_generation(base_paths, generation)
748
+ require_generation_capacity(base_paths, generation)
753
749
  child_env = _command_env(active_env, paths)
754
750
  child_env[CODEX_SERVER_OPTIONS_ENV] = json.dumps(
755
751
  list(codex_options),
756
752
  separators=(",", ":"),
757
753
  )
758
- codex_version = _require_compatible_codex(codex_path, child_env)
759
754
  with _lifecycle_lock(paths):
760
- plugin_snapshot = _plugin_configuration_snapshot(paths, codex_options)
755
+ locked_snapshot = _plugin_configuration_snapshot(paths, codex_options)
756
+ if locked_snapshot.fingerprint != plugin_snapshot.fingerprint:
757
+ raise CodexServerError(
758
+ "the Codex plugin or marketplace snapshot changed during "
759
+ "app-server generation selection; retry after updates finish"
760
+ )
761
+ plugin_snapshot = locked_snapshot
761
762
  plugin_fingerprint = plugin_snapshot.fingerprint
762
763
  try:
763
764
  state = _read_state(paths)
@@ -774,7 +775,7 @@ def ensure_server(
774
775
  "shared app server cannot be reused because its socket "
775
776
  "listener is not owned by the supervised worker"
776
777
  )
777
- _require_external_compatible(codex_version, probe)
778
+ _external_compatible(codex_version, probe, _version_key)
778
779
  return status
779
780
  if state is not None and _state_is_owned(state):
780
781
  restart_reason = _helper_restart_reason(
@@ -785,6 +786,7 @@ def ensure_server(
785
786
  probe,
786
787
  plugin_fingerprint,
787
788
  paths,
789
+ codex_options,
788
790
  )
789
791
  if restart_reason is None and not probe.running:
790
792
  probe = _retry_helper_probe(
@@ -802,6 +804,7 @@ def ensure_server(
802
804
  probe,
803
805
  plugin_fingerprint,
804
806
  paths,
807
+ codex_options,
805
808
  )
806
809
  if restart_reason is None:
807
810
  certified_state, certified_probe = _certify_helper_boundary(
@@ -813,17 +816,16 @@ def ensure_server(
813
816
  codex_options,
814
817
  "app-server reuse checks",
815
818
  )
816
- return _certified_helper_status(
819
+ result = _certified_helper_status(
817
820
  paths,
818
821
  certified_state,
819
822
  certified_probe,
820
823
  )
824
+ publish_current_generation(base_paths, generation)
825
+ return result
821
826
  raise CodexServerError(
822
- "shared app server cannot be reused because "
823
- f"{restart_reason}. Refusing to restart it automatically "
824
- "because that would disconnect every codex-dynamic TUI. Exit "
825
- "connected sessions, run `codex-server restart --force`, then "
826
- "start or resume Codex"
827
+ "selected app-server generation cannot be reused because "
828
+ f"{restart_reason}; refusing to replace an uncertified listener"
827
829
  )
828
830
 
829
831
  if state_error:
@@ -888,11 +890,13 @@ def ensure_server(
888
890
  codex_options,
889
891
  "app-server startup checks",
890
892
  )
891
- return _certified_helper_status(
893
+ result = _certified_helper_status(
892
894
  paths,
893
895
  certified_state,
894
896
  certified_probe,
895
897
  )
898
+ publish_current_generation(base_paths, generation)
899
+ return result
896
900
  except BaseException as exc:
897
901
  if starting is not None:
898
902
  try:
@@ -910,14 +914,12 @@ def ensure_server(
910
914
  raise
911
915
 
912
916
 
913
- def stop_server(
914
- env: Mapping[str, str] | None = None,
915
- *,
916
- allow_disconnect: bool = False,
917
+ def _stop_server_at(
918
+ active_env: Mapping[str, str],
919
+ paths: ServerPaths,
920
+ allow_disconnect: bool,
917
921
  ) -> ServerStatus:
918
- """Stop a helper-owned server after explicit disconnect acknowledgement."""
919
- active_env = dict(os.environ if env is None else env)
920
- paths = _paths(active_env)
922
+ """Stop one selected helper generation."""
921
923
  try:
922
924
  codex_path = _resolve_codex(active_env)
923
925
  except CodexServerError:
@@ -954,6 +956,23 @@ def stop_server(
954
956
  return ServerStatus(status="stopped", ownership=None, paths=paths)
955
957
 
956
958
 
959
+ def stop_server(
960
+ env: Mapping[str, str] | None = None,
961
+ *,
962
+ allow_disconnect: bool = False,
963
+ ) -> ServerStatus:
964
+ """Stop helper-owned servers after explicit disconnect acknowledgement."""
965
+ active_env = dict(os.environ if env is None else env)
966
+ selected = _paths(active_env)
967
+ if not allow_disconnect:
968
+ return _stop_server_at(active_env, selected, False)
969
+ base = base_paths_from_env(active_env)
970
+ for paths in all_server_paths(base):
971
+ _stop_server_at(active_env, paths, True)
972
+ clear_current_generation(base)
973
+ return ServerStatus(status="stopped", ownership=None, paths=selected)
974
+
975
+
957
976
  def restart_server(
958
977
  env: Mapping[str, str] | None = None,
959
978
  *,
@@ -974,18 +993,3 @@ def restart_server(
974
993
  codex_options = state.codex_options
975
994
  stop_server(active_env, allow_disconnect=allow_disconnect)
976
995
  return ensure_server(active_env, codex_options=codex_options)
977
-
978
-
979
- __all__ = [
980
- "ENDPOINT",
981
- "CodexServerError",
982
- "OwnedServer",
983
- "ServerPaths",
984
- "ServerProbe",
985
- "ServerStatus",
986
- "StateFileError",
987
- "ensure_server",
988
- "get_status",
989
- "restart_server",
990
- "stop_server",
991
- ]
@@ -12,7 +12,6 @@ from typing import BinaryIO, NoReturn, Sequence
12
12
  import click
13
13
 
14
14
  from claude_code_tools.codex_server import (
15
- ENDPOINT,
16
15
  CodexServerError,
17
16
  ServerStatus,
18
17
  _command_env,
@@ -58,9 +57,6 @@ NON_TUI_COMMANDS = {
58
57
  }
59
58
 
60
59
  CALLBACK_ENDPOINT_ENV = "CCTOOLS_CODEX_CALLBACK_ENDPOINT"
61
- CALLBACK_SHELL_CONFIG = (
62
- f'shell_environment_policy.set.{CALLBACK_ENDPOINT_ENV}="{ENDPOINT}"'
63
- )
64
60
 
65
61
  GLOBAL_OPTIONS_WITH_VALUES = {
66
62
  "--add-dir",
@@ -107,20 +103,20 @@ def _echo_status(status: ServerStatus, json_output: bool) -> None:
107
103
  click.echo(f"{status.status} (managed by codex-server, pid {status.pid})")
108
104
  else:
109
105
  click.echo(status.status)
110
- click.echo(f"endpoint: {ENDPOINT}")
106
+ click.echo(f"endpoint: {status.paths.endpoint}")
111
107
  if status.detail:
112
108
  click.echo(f"detail: {status.detail}")
113
109
 
114
110
 
115
111
  @click.group(context_settings={"help_option_names": ["-h", "--help"]})
116
112
  def server_cli() -> None:
117
- """Manage the shared app server used for Codex workflow callbacks."""
113
+ """Manage generated app servers used for Codex workflow callbacks."""
118
114
 
119
115
 
120
116
  @server_cli.command("start")
121
117
  @click.option("--json", "json_output", is_flag=True, help="Emit JSON status.")
122
118
  def start_command(json_output: bool) -> None:
123
- """Start the server, or reuse the listener already running."""
119
+ """Start or reuse the server for the current Codex/plugin generation."""
124
120
  try:
125
121
  status = ensure_server()
126
122
  except CodexServerError as exc:
@@ -143,11 +139,11 @@ def status_command(json_output: bool) -> None:
143
139
  @click.option(
144
140
  "--force",
145
141
  is_flag=True,
146
- help="Acknowledge that every connected codex-dynamic TUI will exit.",
142
+ help="Stop all generations; every connected codex-dynamic TUI will exit.",
147
143
  )
148
144
  @click.option("--json", "json_output", is_flag=True, help="Emit JSON status.")
149
145
  def stop_command(force: bool, json_output: bool) -> None:
150
- """Stop a helper-owned server after acknowledging TUI disconnection."""
146
+ """Stop the current generation; use --force to stop all generations."""
151
147
  try:
152
148
  status = stop_server(allow_disconnect=force)
153
149
  except CodexServerError as exc:
@@ -159,11 +155,11 @@ def stop_command(force: bool, json_output: bool) -> None:
159
155
  @click.option(
160
156
  "--force",
161
157
  is_flag=True,
162
- help="Acknowledge that every connected codex-dynamic TUI will exit.",
158
+ help="Restart all generations; every connected codex-dynamic TUI will exit.",
163
159
  )
164
160
  @click.option("--json", "json_output", is_flag=True, help="Emit JSON status.")
165
161
  def restart_command(force: bool, json_output: bool) -> None:
166
- """Restart after acknowledging that connected TUIs will exit."""
162
+ """Restart the current generation; use --force to clean up all."""
167
163
  try:
168
164
  status = restart_server(allow_disconnect=force)
169
165
  except CodexServerError as exc:
@@ -331,34 +327,38 @@ def dynamic_main() -> NoReturn:
331
327
  raise SystemExit(2)
332
328
  active_env = dict(os.environ)
333
329
  use_remote = not (_is_information_only(arguments) or _is_non_tui_command(arguments))
330
+ endpoint: str | None = None
334
331
  try:
335
332
  codex_path = _resolve_codex(active_env)
336
333
  if use_remote:
337
- paths = _paths(active_env)
338
- child_env = _command_env(active_env, paths)
339
- ensure_server(
340
- child_env,
334
+ status = ensure_server(
335
+ active_env,
341
336
  codex_options=_server_configuration_options(arguments),
342
337
  )
343
- child_env[CALLBACK_ENDPOINT_ENV] = ENDPOINT
338
+ endpoint = status.paths.endpoint
339
+ child_env = _command_env(active_env, status.paths)
340
+ child_env[CALLBACK_ENDPOINT_ENV] = endpoint
344
341
  else:
345
342
  child_env = dict(active_env)
346
343
  child_env.pop(CALLBACK_ENDPOINT_ENV, None)
347
344
  except CodexServerError as exc:
348
345
  click.echo(f"Error: {exc}", err=True)
349
346
  raise SystemExit(1) from exc
350
- command = (
351
- [
347
+ if use_remote:
348
+ assert endpoint is not None
349
+ command = [
352
350
  codex_path,
353
351
  "--config",
354
- CALLBACK_SHELL_CONFIG,
352
+ (
353
+ f"shell_environment_policy.set.{CALLBACK_ENDPOINT_ENV}="
354
+ f"{json.dumps(endpoint)}"
355
+ ),
355
356
  "--remote",
356
- ENDPOINT,
357
+ endpoint,
357
358
  *arguments,
358
359
  ]
359
- if use_remote
360
- else [codex_path, *arguments]
361
- )
360
+ else:
361
+ command = [codex_path, *arguments]
362
362
  try:
363
363
  os.execvpe(codex_path, command, child_env)
364
364
  except OSError as exc:
@@ -0,0 +1,52 @@
1
+ """Deterministic identities for independently running app-server generations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import re
8
+ from typing import Sequence
9
+
10
+
11
+ GENERATION_PATTERN = re.compile(r"[0-9a-f]{24}")
12
+
13
+
14
+ def server_generation(
15
+ codex_path: str,
16
+ executable_identity: str,
17
+ codex_version: str,
18
+ plugin_fingerprint: str,
19
+ codex_options: Sequence[str],
20
+ ) -> str:
21
+ """Return the generation selected by every server-affecting input.
22
+
23
+ Args:
24
+ codex_path: Canonical path to the selected Codex executable.
25
+ executable_identity: Stable identity of the executable file.
26
+ codex_version: Version reported by that executable.
27
+ plugin_fingerprint: Fingerprint of the effective plugin configuration.
28
+ codex_options: Global Codex options forwarded to the app server.
29
+
30
+ Returns:
31
+ A short lowercase hexadecimal generation identifier.
32
+ """
33
+ payload = json.dumps(
34
+ {
35
+ "codexOptions": list(codex_options),
36
+ "codexPath": codex_path,
37
+ "codexVersion": codex_version,
38
+ "executableIdentity": executable_identity,
39
+ "pluginFingerprint": plugin_fingerprint,
40
+ },
41
+ ensure_ascii=True,
42
+ separators=(",", ":"),
43
+ sort_keys=True,
44
+ ).encode("utf-8")
45
+ return hashlib.sha256(payload).hexdigest()[:24]
46
+
47
+
48
+ def validate_generation(value: str) -> str:
49
+ """Validate an app-server generation received through the environment."""
50
+ if GENERATION_PATTERN.fullmatch(value) is None:
51
+ raise ValueError("app-server generation must be 24 lowercase hex characters")
52
+ return value