claude-team-mcp 0.9.0__tar.gz → 0.9.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.
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/.pebbles/events.jsonl +8 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/.pebbles/pebbles.db +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/CHANGELOG.md +13 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/PKG-INFO +1 -1
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/pyproject.toml +1 -1
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/registry.py +22 -14
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/session_state.py +36 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/adopt_worker.py +6 -2
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/close_workers.py +4 -1
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/discover_workers.py +5 -2
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/list_worktrees.py +4 -1
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/message_workers.py +6 -2
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/poll_worker_changes.py +4 -1
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/read_worker_logs.py +6 -2
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/spawn_workers.py +25 -3
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/wait_idle_workers.py +8 -3
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/worker_events.py +10 -4
- claude_team_mcp-0.9.2/tests/test_codex_jsonl_path.py +285 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/uv.lock +1 -1
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/.claude/settings.json +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/.claude/settings.local.json +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/.claude-plugin/marketplace.json +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/.claude-plugin/plugin.json +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/.gitattributes +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/.gitignore +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/.mcp.json +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/.pebbles/.gitignore +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/.pebbles/config.json +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/AGENTS.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/CLAUDE.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/HAPPY_INTEGRATION_RESEARCH.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/Makefile +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/NOTES.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/README.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/commands/check-workers.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/commands/cleanup-worktrees.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/commands/merge-worker.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/commands/pr-worker.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/commands/spawn-workers.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/commands/team-summary.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/config/mcporter.json +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/docs/ISSUE_TRACKER_ABSTRACTION.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/docs/design/unified-worker-state.md +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/scripts/install-commands.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/scripts/team-status.sh +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/settings.json +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team/__init__.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team/events.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team/idle_detection.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team/poller.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/__init__.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/__main__.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/cli_backends/__init__.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/cli_backends/base.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/cli_backends/claude.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/cli_backends/codex.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/colors.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/config.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/config_cli.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/formatting.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/idle_detection.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/issue_tracker/__init__.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/iterm_utils.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/names.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/profile.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/schemas/__init__.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/schemas/codex.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/server.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/subprocess_cache.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/terminal_backends/__init__.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/terminal_backends/base.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/terminal_backends/iterm.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/terminal_backends/tmux.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/__init__.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/annotate_worker.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/check_idle_workers.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/examine_worker.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/issue_tracker_help.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/list_workers.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/utils/__init__.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/utils/constants.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/utils/errors.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/utils/worktree_detection.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/worker_prompt.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/worktree.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/__init__.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/conftest.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_cli_backends.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_codex_schema.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_colors.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_config.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_config_cli.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_events.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_formatting.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_idle_detection.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_idle_detection_module.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_issue_tracker.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_issue_tracker_integration.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_iterm_utils.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_names.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_poll_worker_changes.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_poller.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_recover_from_events.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_recovered_session.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_registry.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_session_state.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_spawn_workers_defaults.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_startup_recovery.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_terminal_backends.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_tmux_backend.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_worker_events.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_worker_prompt.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_worktree.py +0 -0
- {claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/tests/test_worktree_detection.py +0 -0
|
@@ -228,3 +228,11 @@
|
|
|
228
228
|
{"type":"create","timestamp":"2026-02-02T23:29:52.330658Z","issue_id":"cic-ebd","payload":{"description":"tmux_session_name_for_project() hashes the full project_path. Worktree paths produce unique hashes, creating one tmux session per worker instead of one per project. Fix: resolve worktree paths to main repo before hashing.","priority":"1","title":"tmux: worktree paths create separate sessions instead of sharing per-project session","type":"bug"}}
|
|
229
229
|
{"type":"status_update","timestamp":"2026-02-02T23:30:32.877924Z","issue_id":"cic-ebd","payload":{"status":"in_progress"}}
|
|
230
230
|
{"type":"close","timestamp":"2026-02-02T23:33:33.785116Z","issue_id":"cic-ebd","payload":{}}
|
|
231
|
+
{"type":"create","timestamp":"2026-02-04T22:18:18.544978Z","issue_id":"cic-c29","payload":{"description":"","priority":"2","title":"message_workers: wait_mode validation rejects None from MCP clients","type":"task"}}
|
|
232
|
+
{"type":"comment","timestamp":"2026-02-04T22:18:27.550553Z","issue_id":"cic-c29","payload":{"body":"When message_workers is called via MCP clients (e.g. mcporter) that send explicit null for omitted optional params, pydantic rejects it:\n\nError: Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]\n\nThe type annotation is wait_mode: str = 'none' but needs to accept None.\n\nFix: Change to wait_mode: str | None = 'none' and add wait_mode = wait_mode or 'none' guard at the top of the function. Same for timeout: float = 600.0.\n\nFile: src/claude_team_mcp/tools/message_workers.py\nFunction: message_workers (line ~127)"}}
|
|
233
|
+
{"type":"update","timestamp":"2026-02-04T22:18:30.31878Z","issue_id":"cic-c29","payload":{"type":"bug"}}
|
|
234
|
+
{"type":"close","timestamp":"2026-02-05T00:07:32.417031Z","issue_id":"cic-c29","payload":{}}
|
|
235
|
+
{"type":"create","timestamp":"2026-02-05T23:19:38.480182Z","issue_id":"cic-e24","payload":{"description":"When read_worker_logs is called for Codex workers whose marker-based JSONL discovery fails, the fallback in get_jsonl_path() grabs the most recently modified Codex JSONL globally — often the COORDINATOR's own session. All workers return identical log content (the coordinator's conversation). Root cause: max_age_seconds=600 in find_codex_session_by_internal_id is too short, and the blind find_codex_session_file fallback returns wrong data. Fix: cache JSONL path on spawn, increase/remove max_age, remove blind fallback.","priority":"1","title":"read_worker_logs returns wrong session data for Codex workers","type":"bug"}}
|
|
236
|
+
{"type":"update","timestamp":"2026-02-05T23:23:47.02398Z","issue_id":"cic-e24","payload":{"description":"## Bug\n\nWhen read_worker_logs is called for Codex workers whose marker-based JSONL discovery fails, the fallback in get_jsonl_path() grabs the most recently modified Codex JSONL globally — often the COORDINATOR's own session. All workers return identical, wrong log content.\n\n## Observed Behavior\n\n- Three Codex workers (Rick, Morty, Pelé) on martian-todos project\n- All three show claude_session_id: null (marker discovery failed) \n- read_worker_logs returns identical content for all three — the coordinator's own conversation\n- examine_worker shows coordinator's last_user_prompt for all workers\n- Workers had actually completed their tasks (visible in tmux panes)\n\n## Root Cause\n\nIn registry.py ManagedSession.get_jsonl_path() for Codex:\n\n1. find_codex_session_by_internal_id uses max_age_seconds=600 (10 min) — too short for workers running longer\n2. Blind fallback find_codex_session_file grabs most recent Codex JSONL globally — returns wrong session\n3. Unlike Claude workers, Codex workers SKIP marker discovery at spawn time (spawn_workers.py line ~696 has an `if agent_type == claude` guard), so claude_session_id is never set\n\n## Fix Plan\n\n### 1. Remove blind fallback (critical, immediate)\nIn get_jsonl_path(), remove `find_codex_session_file()` fallback. Return None if marker-based discovery fails. Bad data is worse than no data.\n\n### 2. Cache JSONL path at spawn time (parity with Claude workers)\n- Add `codex_jsonl_path: Optional[Path] = None` field on ManagedSession\n- After sending marker to Codex workers in spawn_workers.py, poll for marker (adapt await_marker_in_jsonl or use find_codex_session_by_internal_id with generous timeout)\n- Store: `managed.codex_jsonl_path = match.jsonl_path`\n- In get_jsonl_path(), check self.codex_jsonl_path first; skip re-discovery if set\n\n### 3. Increase max_age_seconds for re-discovery\n- Change max_age_seconds from 600 to 86400 (24h) in find_codex_session_by_internal_id calls\n- Workers can run for hours; 10 min window is far too short\n\n### 4. Persist path in event log for recovery\n- Include codex_jsonl_path in worker_started events so RecoveredSession objects also have it\n- RecoveredSession.get_jsonl_path() should use persisted path\n\n### 5. Tests\n- Test that get_jsonl_path returns None (not wrong file) when discovery fails\n- Test that spawn caches the path for Codex workers\n- Test that recovered workers use persisted path"}}
|
|
237
|
+
{"type":"status_update","timestamp":"2026-02-05T23:24:08.656684Z","issue_id":"cic-e24","payload":{"status":"in_progress"}}
|
|
238
|
+
{"type":"close","timestamp":"2026-02-05T23:28:34.736194Z","issue_id":"cic-e24","payload":{}}
|
|
Binary file
|
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.9.2] - 2026-02-05
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Codex worker log discovery**: `read_worker_logs` no longer returns wrong session data (e.g. coordinator's own logs) for Codex workers. Removed blind `find_codex_session_file()` fallback that grabbed the most recent Codex JSONL globally regardless of session ownership.
|
|
14
|
+
- **Codex JSONL path caching**: Codex workers now cache their JSONL path at spawn time (parity with Claude workers), via new `await_codex_marker_in_jsonl()` polling.
|
|
15
|
+
- **Discovery window**: Increased `max_age_seconds` from 600 (10 min) to 86400 (24h) for Codex session file scanning — workers can run for hours.
|
|
16
|
+
- **RecoveredSession crashes**: Fixed `discover_workers`, `adopt_worker`, and `spawn_workers` crashing with `'RecoveredSession' object has no attribute 'terminal_session'` when recovered sessions exist in the registry.
|
|
17
|
+
|
|
18
|
+
## [0.9.1] - 2026-02-04
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **MCP tool optional param validation**: All 9 MCP tools now accept `null` for optional parameters with defaults. MCP clients (e.g. mcporter) that send explicit `null` for omitted params no longer trigger pydantic validation errors.
|
|
22
|
+
|
|
10
23
|
## [0.9.0] - 2026-02-03
|
|
11
24
|
|
|
12
25
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-team-mcp
|
|
3
|
-
Version: 0.9.
|
|
3
|
+
Version: 0.9.2
|
|
4
4
|
Summary: MCP server for managing multiple Claude Code sessions via iTerm2
|
|
5
5
|
Project-URL: Homepage, https://github.com/Martian-Engineering/claude-team
|
|
6
6
|
Project-URL: Repository, https://github.com/Martian-Engineering/claude-team
|
|
@@ -139,6 +139,7 @@ class RecoveredSession:
|
|
|
139
139
|
coordinator_annotation: Optional[str] = None
|
|
140
140
|
worktree_path: Optional[str] = None
|
|
141
141
|
main_repo_path: Optional[str] = None
|
|
142
|
+
codex_jsonl_path: Optional[str] = None
|
|
142
143
|
|
|
143
144
|
@staticmethod
|
|
144
145
|
def map_event_state_to_status(event_state: EventState) -> SessionStatus:
|
|
@@ -178,6 +179,7 @@ class RecoveredSession:
|
|
|
178
179
|
"worktree_path": self.worktree_path,
|
|
179
180
|
"main_repo_path": self.main_repo_path,
|
|
180
181
|
"agent_type": self.agent_type,
|
|
182
|
+
"codex_jsonl_path": self.codex_jsonl_path,
|
|
181
183
|
# Recovery-specific fields
|
|
182
184
|
"source": "event_log",
|
|
183
185
|
"event_state": self.event_state,
|
|
@@ -227,6 +229,9 @@ class ManagedSession:
|
|
|
227
229
|
# Agent type: "claude" (default) or "codex"
|
|
228
230
|
agent_type: AgentType = "claude"
|
|
229
231
|
|
|
232
|
+
# Cached Codex JSONL path (discovered at spawn time via marker polling)
|
|
233
|
+
codex_jsonl_path: Optional[Path] = None
|
|
234
|
+
|
|
230
235
|
def __post_init__(self):
|
|
231
236
|
"""Auto-populate terminal_id from terminal_session if not set."""
|
|
232
237
|
if self.terminal_id is None:
|
|
@@ -255,6 +260,7 @@ class ManagedSession:
|
|
|
255
260
|
"worktree_path": str(self.worktree_path) if self.worktree_path else None,
|
|
256
261
|
"main_repo_path": str(self.main_repo_path) if self.main_repo_path else None,
|
|
257
262
|
"agent_type": self.agent_type,
|
|
263
|
+
"codex_jsonl_path": str(self.codex_jsonl_path) if self.codex_jsonl_path else None,
|
|
258
264
|
# Source field for distinguishing live vs recovered sessions
|
|
259
265
|
"source": "registry",
|
|
260
266
|
}
|
|
@@ -292,22 +298,29 @@ class ManagedSession:
|
|
|
292
298
|
Get the path to this session's JSONL file.
|
|
293
299
|
|
|
294
300
|
For Claude workers: uses marker-based discovery in ~/.claude/projects/.
|
|
295
|
-
For Codex workers: uses marker-based discovery in
|
|
301
|
+
For Codex workers: uses cached path or marker-based discovery in
|
|
302
|
+
~/.codex/sessions/. Returns None (not a wrong file) when discovery fails.
|
|
296
303
|
|
|
297
304
|
Returns:
|
|
298
305
|
Path object, or None if session cannot be discovered
|
|
299
306
|
"""
|
|
300
307
|
if self.agent_type == "codex":
|
|
301
|
-
|
|
308
|
+
# Use cached path if available (set at spawn time)
|
|
309
|
+
if self.codex_jsonl_path and self.codex_jsonl_path.exists():
|
|
310
|
+
return self.codex_jsonl_path
|
|
302
311
|
|
|
303
|
-
#
|
|
312
|
+
# Try marker-based discovery with generous timeout (workers can run for hours)
|
|
304
313
|
match = find_codex_session_by_internal_id(
|
|
305
314
|
self.session_id,
|
|
306
|
-
max_age_seconds=
|
|
315
|
+
max_age_seconds=86400,
|
|
307
316
|
)
|
|
308
317
|
if match:
|
|
318
|
+
# Cache for future calls
|
|
319
|
+
self.codex_jsonl_path = match.jsonl_path
|
|
309
320
|
return match.jsonl_path
|
|
310
|
-
|
|
321
|
+
|
|
322
|
+
# No blind fallback - returning None is better than returning wrong data
|
|
323
|
+
return None
|
|
311
324
|
else:
|
|
312
325
|
# For Claude, use marker-based discovery
|
|
313
326
|
# Auto-discover if not already known
|
|
@@ -353,16 +366,10 @@ class ManagedSession:
|
|
|
353
366
|
True if idle, False if working or session file not available
|
|
354
367
|
"""
|
|
355
368
|
if self.agent_type == "codex":
|
|
356
|
-
from .idle_detection import
|
|
369
|
+
from .idle_detection import is_codex_idle
|
|
357
370
|
|
|
358
|
-
#
|
|
359
|
-
|
|
360
|
-
self.session_id,
|
|
361
|
-
max_age_seconds=600,
|
|
362
|
-
)
|
|
363
|
-
session_file = match.jsonl_path if match else None
|
|
364
|
-
if not session_file:
|
|
365
|
-
session_file = find_codex_session_file(max_age_seconds=600)
|
|
371
|
+
# Use the same path resolution as get_jsonl_path() (cached or marker-based)
|
|
372
|
+
session_file = self.get_jsonl_path()
|
|
366
373
|
if not session_file:
|
|
367
374
|
return False
|
|
368
375
|
return is_codex_idle(session_file)
|
|
@@ -795,6 +802,7 @@ class SessionRegistry:
|
|
|
795
802
|
coordinator_annotation=data.get("coordinator_annotation"),
|
|
796
803
|
worktree_path=data.get("worktree_path"),
|
|
797
804
|
main_repo_path=data.get("main_repo_path"),
|
|
805
|
+
codex_jsonl_path=data.get("codex_jsonl_path"),
|
|
798
806
|
)
|
|
799
807
|
|
|
800
808
|
def count(self) -> int:
|
|
@@ -838,6 +838,42 @@ async def await_marker_in_jsonl(
|
|
|
838
838
|
return None
|
|
839
839
|
|
|
840
840
|
|
|
841
|
+
async def await_codex_marker_in_jsonl(
|
|
842
|
+
session_id: str,
|
|
843
|
+
timeout: float = 30.0,
|
|
844
|
+
poll_interval: float = 0.5,
|
|
845
|
+
) -> Optional[CodexSessionMatch]:
|
|
846
|
+
"""
|
|
847
|
+
Poll for a Codex session marker to appear in the JSONL.
|
|
848
|
+
|
|
849
|
+
Codex workers write markers into ~/.codex/sessions/ files. This function
|
|
850
|
+
polls until the marker for the given session_id is found.
|
|
851
|
+
|
|
852
|
+
Args:
|
|
853
|
+
session_id: The internal session ID to search for in markers
|
|
854
|
+
timeout: Maximum seconds to wait (default 30)
|
|
855
|
+
poll_interval: Seconds between polls (default 0.5, slower than Claude
|
|
856
|
+
because Codex takes longer to start)
|
|
857
|
+
|
|
858
|
+
Returns:
|
|
859
|
+
CodexSessionMatch if found, None on timeout
|
|
860
|
+
"""
|
|
861
|
+
import asyncio
|
|
862
|
+
|
|
863
|
+
start = time.time()
|
|
864
|
+
|
|
865
|
+
while time.time() - start < timeout:
|
|
866
|
+
match = find_codex_session_by_internal_id(
|
|
867
|
+
session_id,
|
|
868
|
+
max_age_seconds=300,
|
|
869
|
+
)
|
|
870
|
+
if match:
|
|
871
|
+
return match
|
|
872
|
+
await asyncio.sleep(poll_interval)
|
|
873
|
+
|
|
874
|
+
return None
|
|
875
|
+
|
|
876
|
+
|
|
841
877
|
# =============================================================================
|
|
842
878
|
# Session Discovery
|
|
843
879
|
# =============================================================================
|
|
@@ -35,7 +35,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
35
35
|
iterm_session_id: str | None = None,
|
|
36
36
|
tmux_pane_id: str | None = None,
|
|
37
37
|
session_name: str | None = None,
|
|
38
|
-
max_age: int = 3600,
|
|
38
|
+
max_age: int | None = 3600,
|
|
39
39
|
) -> dict:
|
|
40
40
|
"""
|
|
41
41
|
Adopt an existing terminal Claude Code or Codex session into the MCP registry.
|
|
@@ -53,6 +53,9 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
53
53
|
Returns:
|
|
54
54
|
Dict with adopted worker info, or error if session not found
|
|
55
55
|
"""
|
|
56
|
+
# Handle None values from MCP clients that send explicit null for omitted params
|
|
57
|
+
max_age = max_age if max_age is not None else 3600
|
|
58
|
+
|
|
56
59
|
app_ctx = ctx.request_context.lifespan_context
|
|
57
60
|
registry = app_ctx.registry
|
|
58
61
|
|
|
@@ -75,7 +78,8 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
75
78
|
# Check if already managed
|
|
76
79
|
for managed in registry.list_all():
|
|
77
80
|
if (
|
|
78
|
-
managed
|
|
81
|
+
hasattr(managed, 'terminal_session')
|
|
82
|
+
and managed.terminal_session.backend_id == backend_id
|
|
79
83
|
and managed.terminal_session.native_id == target_id
|
|
80
84
|
):
|
|
81
85
|
return error_response(
|
|
@@ -133,7 +133,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
133
133
|
async def close_workers(
|
|
134
134
|
ctx: Context[ServerSession, "AppContext"],
|
|
135
135
|
session_ids: list[str],
|
|
136
|
-
force: bool = False,
|
|
136
|
+
force: bool | None = False,
|
|
137
137
|
) -> dict:
|
|
138
138
|
"""
|
|
139
139
|
Close one or more managed Claude Code sessions.
|
|
@@ -163,6 +163,9 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
163
163
|
- success_count: Number of sessions closed successfully
|
|
164
164
|
- failure_count: Number of sessions that failed to close
|
|
165
165
|
"""
|
|
166
|
+
# Handle None values from MCP clients that send explicit null for omitted params
|
|
167
|
+
force = force if force is not None else False
|
|
168
|
+
|
|
166
169
|
app_ctx = ctx.request_context.lifespan_context
|
|
167
170
|
registry = app_ctx.registry
|
|
168
171
|
backend = app_ctx.terminal_backend
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/discover_workers.py
RENAMED
|
@@ -33,7 +33,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
33
33
|
@mcp.tool()
|
|
34
34
|
async def discover_workers(
|
|
35
35
|
ctx: Context[ServerSession, "AppContext"],
|
|
36
|
-
max_age: int = 3600,
|
|
36
|
+
max_age: int | None = 3600,
|
|
37
37
|
) -> dict:
|
|
38
38
|
"""
|
|
39
39
|
Discover existing Claude Code and Codex sessions running in the active terminal backend.
|
|
@@ -69,6 +69,9 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
69
69
|
- count: Total number of sessions found
|
|
70
70
|
- unmanaged_count: Number not yet in registry (available to adopt)
|
|
71
71
|
"""
|
|
72
|
+
# Handle None values from MCP clients that send explicit null for omitted params
|
|
73
|
+
max_age = max_age if max_age is not None else 3600
|
|
74
|
+
|
|
72
75
|
app_ctx = ctx.request_context.lifespan_context
|
|
73
76
|
registry = app_ctx.registry
|
|
74
77
|
|
|
@@ -87,7 +90,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
87
90
|
managed_ids = {
|
|
88
91
|
s.terminal_session.native_id
|
|
89
92
|
for s in registry.list_all()
|
|
90
|
-
if s.terminal_session.backend_id == backend_id
|
|
93
|
+
if hasattr(s, 'terminal_session') and s.terminal_session.backend_id == backend_id
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
try:
|
|
@@ -31,7 +31,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
31
31
|
async def list_worktrees(
|
|
32
32
|
ctx: Context[ServerSession, "AppContext"],
|
|
33
33
|
repo_path: str,
|
|
34
|
-
remove_orphans: bool = False,
|
|
34
|
+
remove_orphans: bool | None = False,
|
|
35
35
|
) -> dict:
|
|
36
36
|
"""
|
|
37
37
|
List worktrees in a repository's .worktrees/ directory.
|
|
@@ -58,6 +58,9 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
58
58
|
- orphan_count: Number of orphaned worktrees
|
|
59
59
|
- removed_count: Number of orphans removed (when remove_orphans=True)
|
|
60
60
|
"""
|
|
61
|
+
# Handle None values from MCP clients that send explicit null for omitted params
|
|
62
|
+
remove_orphans = remove_orphans if remove_orphans is not None else False
|
|
63
|
+
|
|
61
64
|
resolved_path = Path(repo_path).resolve()
|
|
62
65
|
if not resolved_path.exists():
|
|
63
66
|
return error_response(
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/message_workers.py
RENAMED
|
@@ -131,8 +131,8 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
131
131
|
ctx: Context[ServerSession, "AppContext"],
|
|
132
132
|
session_ids: list[str],
|
|
133
133
|
message: str,
|
|
134
|
-
wait_mode: str = "none",
|
|
135
|
-
timeout: float = 600.0,
|
|
134
|
+
wait_mode: str | None = "none",
|
|
135
|
+
timeout: float | None = 600.0,
|
|
136
136
|
) -> dict:
|
|
137
137
|
"""
|
|
138
138
|
Send a message to one or more Claude Code worker sessions.
|
|
@@ -163,6 +163,10 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
163
163
|
- all_idle: Whether all sessions are idle (only if wait_mode != "none")
|
|
164
164
|
- timed_out: Whether the wait timed out (only if wait_mode != "none")
|
|
165
165
|
"""
|
|
166
|
+
# Handle None values from MCP clients that send explicit null for omitted params
|
|
167
|
+
wait_mode = wait_mode or "none"
|
|
168
|
+
timeout = timeout if timeout is not None else 600.0
|
|
169
|
+
|
|
166
170
|
app_ctx = ctx.request_context.lifespan_context
|
|
167
171
|
registry = app_ctx.registry
|
|
168
172
|
backend = app_ctx.terminal_backend
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/poll_worker_changes.py
RENAMED
|
@@ -122,7 +122,7 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
122
122
|
ctx: Context[ServerSession, "AppContext"],
|
|
123
123
|
since: str | None = None,
|
|
124
124
|
stale_threshold_minutes: int | None = None,
|
|
125
|
-
include_snapshots: bool = False,
|
|
125
|
+
include_snapshots: bool | None = False,
|
|
126
126
|
) -> dict:
|
|
127
127
|
"""
|
|
128
128
|
Poll worker event changes since a timestamp.
|
|
@@ -144,6 +144,9 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
144
144
|
- idle_count: Count of idle workers
|
|
145
145
|
- poll_ts: Timestamp when poll was generated
|
|
146
146
|
"""
|
|
147
|
+
# Handle None values from MCP clients that send explicit null for omitted params
|
|
148
|
+
include_snapshots = include_snapshots if include_snapshots is not None else False
|
|
149
|
+
|
|
147
150
|
app_ctx = ctx.request_context.lifespan_context
|
|
148
151
|
registry = app_ctx.registry
|
|
149
152
|
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/read_worker_logs.py
RENAMED
|
@@ -22,8 +22,8 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
22
22
|
async def read_worker_logs(
|
|
23
23
|
ctx: Context[ServerSession, "AppContext"],
|
|
24
24
|
session_id: str,
|
|
25
|
-
pages: int = 1,
|
|
26
|
-
offset: int = 0,
|
|
25
|
+
pages: int | None = 1,
|
|
26
|
+
offset: int | None = 0,
|
|
27
27
|
) -> dict:
|
|
28
28
|
"""
|
|
29
29
|
Get conversation history from a Claude Code session with reverse pagination.
|
|
@@ -51,6 +51,10 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
51
51
|
- page_info: Pagination metadata (total_messages, total_pages, etc.)
|
|
52
52
|
- session_id: The session ID
|
|
53
53
|
"""
|
|
54
|
+
# Handle None values from MCP clients that send explicit null for omitted params
|
|
55
|
+
pages = pages if pages is not None else 1
|
|
56
|
+
offset = offset if offset is not None else 0
|
|
57
|
+
|
|
54
58
|
app_ctx = ctx.request_context.lifespan_context
|
|
55
59
|
registry = app_ctx.registry
|
|
56
60
|
|
|
@@ -187,7 +187,11 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
187
187
|
# Then immediately:
|
|
188
188
|
message_workers(session_ids=["Groucho"], message="Your task is...")
|
|
189
189
|
"""
|
|
190
|
-
from ..session_state import
|
|
190
|
+
from ..session_state import (
|
|
191
|
+
await_codex_marker_in_jsonl,
|
|
192
|
+
await_marker_in_jsonl,
|
|
193
|
+
generate_marker_message,
|
|
194
|
+
)
|
|
191
195
|
|
|
192
196
|
app_ctx = ctx.request_context.lifespan_context
|
|
193
197
|
registry = app_ctx.registry
|
|
@@ -452,7 +456,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
452
456
|
managed_session_ids = {
|
|
453
457
|
s.terminal_session.native_id
|
|
454
458
|
for s in registry.list_all()
|
|
455
|
-
if s.terminal_session.backend_id == backend.backend_id
|
|
459
|
+
if hasattr(s, 'terminal_session') and s.terminal_session.backend_id == backend.backend_id
|
|
456
460
|
}
|
|
457
461
|
|
|
458
462
|
# Find a window with enough space for ALL workers
|
|
@@ -693,7 +697,7 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
693
697
|
submit=True,
|
|
694
698
|
)
|
|
695
699
|
|
|
696
|
-
# Wait for markers to appear in JSONL (Claude
|
|
700
|
+
# Wait for markers to appear in JSONL (Claude and Codex)
|
|
697
701
|
for i, managed in enumerate(managed_sessions):
|
|
698
702
|
if managed.agent_type == "claude":
|
|
699
703
|
claude_session_id = await await_marker_in_jsonl(
|
|
@@ -709,6 +713,24 @@ def register_tools(mcp: FastMCP, ensure_connection) -> None:
|
|
|
709
713
|
f"Marker polling timed out for {managed.session_id}, "
|
|
710
714
|
"JSONL correlation unavailable"
|
|
711
715
|
)
|
|
716
|
+
elif managed.agent_type == "codex":
|
|
717
|
+
# Poll for Codex marker and cache the JSONL path
|
|
718
|
+
codex_match = await await_codex_marker_in_jsonl(
|
|
719
|
+
managed.session_id,
|
|
720
|
+
timeout=30.0,
|
|
721
|
+
poll_interval=0.5,
|
|
722
|
+
)
|
|
723
|
+
if codex_match:
|
|
724
|
+
managed.codex_jsonl_path = codex_match.jsonl_path
|
|
725
|
+
logger.info(
|
|
726
|
+
f"Codex JSONL path cached for {managed.session_id}: "
|
|
727
|
+
f"{codex_match.jsonl_path}"
|
|
728
|
+
)
|
|
729
|
+
else:
|
|
730
|
+
logger.warning(
|
|
731
|
+
f"Codex marker polling timed out for {managed.session_id}, "
|
|
732
|
+
"JSONL correlation unavailable"
|
|
733
|
+
)
|
|
712
734
|
|
|
713
735
|
# Send worker prompts - always use generate_worker_prompt with bead/custom_prompt
|
|
714
736
|
workers_awaiting_task: list[str] = [] # Workers with no bead and no prompt
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/wait_idle_workers.py
RENAMED
|
@@ -28,9 +28,9 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
28
28
|
async def wait_idle_workers(
|
|
29
29
|
ctx: Context[ServerSession, "AppContext"],
|
|
30
30
|
session_ids: list[str],
|
|
31
|
-
mode: str = "all",
|
|
32
|
-
timeout: float = 600.0,
|
|
33
|
-
poll_interval: float = 2.0,
|
|
31
|
+
mode: str | None = "all",
|
|
32
|
+
timeout: float | None = 600.0,
|
|
33
|
+
poll_interval: float | None = 2.0,
|
|
34
34
|
) -> dict:
|
|
35
35
|
"""
|
|
36
36
|
Wait for worker sessions to become idle.
|
|
@@ -56,6 +56,11 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
56
56
|
- waited_seconds: How long we waited
|
|
57
57
|
- timed_out: Whether we hit the timeout
|
|
58
58
|
"""
|
|
59
|
+
# Handle None values from MCP clients that send explicit null for omitted params
|
|
60
|
+
mode = mode or "all"
|
|
61
|
+
timeout = timeout if timeout is not None else 600.0
|
|
62
|
+
poll_interval = poll_interval if poll_interval is not None else 2.0
|
|
63
|
+
|
|
59
64
|
app_ctx = ctx.request_context.lifespan_context
|
|
60
65
|
registry = app_ctx.registry
|
|
61
66
|
|
|
@@ -193,10 +193,10 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
193
193
|
async def worker_events(
|
|
194
194
|
ctx: Context[ServerSession, "AppContext"],
|
|
195
195
|
since: str | None = None,
|
|
196
|
-
limit: int = 1000,
|
|
197
|
-
include_snapshot: bool = False,
|
|
198
|
-
include_summary: bool = False,
|
|
199
|
-
stale_threshold_minutes: int = 10,
|
|
196
|
+
limit: int | None = 1000,
|
|
197
|
+
include_snapshot: bool | None = False,
|
|
198
|
+
include_summary: bool | None = False,
|
|
199
|
+
stale_threshold_minutes: int | None = 10,
|
|
200
200
|
project_filter: str | None = None,
|
|
201
201
|
) -> dict:
|
|
202
202
|
"""
|
|
@@ -230,6 +230,12 @@ def register_tools(mcp: FastMCP) -> None:
|
|
|
230
230
|
- last_event_ts: newest event timestamp
|
|
231
231
|
- snapshot: (if include_snapshot) Latest snapshot {ts, data}
|
|
232
232
|
"""
|
|
233
|
+
# Handle None values from MCP clients that send explicit null for omitted params
|
|
234
|
+
limit = limit if limit is not None else 1000
|
|
235
|
+
include_snapshot = include_snapshot if include_snapshot is not None else False
|
|
236
|
+
include_summary = include_summary if include_summary is not None else False
|
|
237
|
+
stale_threshold_minutes = stale_threshold_minutes if stale_threshold_minutes is not None else 10
|
|
238
|
+
|
|
233
239
|
# Parse the since timestamp if provided.
|
|
234
240
|
parsed_since = None
|
|
235
241
|
if since is not None and since.strip():
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""Tests for Codex worker JSONL path resolution (cic-e24).
|
|
2
|
+
|
|
3
|
+
Validates that:
|
|
4
|
+
1. get_jsonl_path() returns None (not wrong file) when Codex marker discovery fails
|
|
5
|
+
2. get_jsonl_path() uses cached codex_jsonl_path when available
|
|
6
|
+
3. is_idle() uses get_jsonl_path() without blind fallback
|
|
7
|
+
4. codex_jsonl_path is persisted in to_dict() for event log recovery
|
|
8
|
+
5. RecoveredSession includes codex_jsonl_path from event data
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from unittest.mock import MagicMock, patch
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from claude_team_mcp.registry import (
|
|
18
|
+
ManagedSession,
|
|
19
|
+
RecoveredSession,
|
|
20
|
+
SessionStatus,
|
|
21
|
+
TerminalId,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestCodexGetJsonlPathNoBlindFallback:
|
|
26
|
+
"""get_jsonl_path() must not return wrong session data for Codex workers."""
|
|
27
|
+
|
|
28
|
+
def _make_codex_session(self, session_id="test-abc1", codex_jsonl_path=None):
|
|
29
|
+
"""Create a ManagedSession configured as a Codex worker."""
|
|
30
|
+
mock_terminal = MagicMock()
|
|
31
|
+
session = ManagedSession(
|
|
32
|
+
session_id=session_id,
|
|
33
|
+
terminal_session=mock_terminal,
|
|
34
|
+
project_path="/test/project",
|
|
35
|
+
agent_type="codex",
|
|
36
|
+
)
|
|
37
|
+
if codex_jsonl_path:
|
|
38
|
+
session.codex_jsonl_path = codex_jsonl_path
|
|
39
|
+
return session
|
|
40
|
+
|
|
41
|
+
@patch("claude_team_mcp.registry.find_codex_session_by_internal_id")
|
|
42
|
+
def test_returns_none_when_discovery_fails(self, mock_find):
|
|
43
|
+
"""get_jsonl_path returns None when marker discovery fails, not a random file."""
|
|
44
|
+
mock_find.return_value = None
|
|
45
|
+
session = self._make_codex_session()
|
|
46
|
+
|
|
47
|
+
result = session.get_jsonl_path()
|
|
48
|
+
|
|
49
|
+
assert result is None
|
|
50
|
+
# Verify it used generous max_age
|
|
51
|
+
mock_find.assert_called_once_with("test-abc1", max_age_seconds=86400)
|
|
52
|
+
|
|
53
|
+
@patch("claude_team_mcp.registry.find_codex_session_by_internal_id")
|
|
54
|
+
def test_uses_cached_path_when_available(self, mock_find):
|
|
55
|
+
"""get_jsonl_path returns cached codex_jsonl_path without re-discovery."""
|
|
56
|
+
cached_path = Path("/tmp/test-codex-session.jsonl")
|
|
57
|
+
cached_path.touch()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
session = self._make_codex_session(codex_jsonl_path=cached_path)
|
|
61
|
+
|
|
62
|
+
result = session.get_jsonl_path()
|
|
63
|
+
|
|
64
|
+
assert result == cached_path
|
|
65
|
+
# Should NOT call find_codex_session_by_internal_id when cache is valid
|
|
66
|
+
mock_find.assert_not_called()
|
|
67
|
+
finally:
|
|
68
|
+
cached_path.unlink(missing_ok=True)
|
|
69
|
+
|
|
70
|
+
@patch("claude_team_mcp.registry.find_codex_session_by_internal_id")
|
|
71
|
+
def test_falls_through_when_cached_path_missing(self, mock_find):
|
|
72
|
+
"""get_jsonl_path tries re-discovery when cached file no longer exists."""
|
|
73
|
+
mock_find.return_value = None
|
|
74
|
+
nonexistent_path = Path("/tmp/nonexistent-codex-session.jsonl")
|
|
75
|
+
session = self._make_codex_session(codex_jsonl_path=nonexistent_path)
|
|
76
|
+
|
|
77
|
+
result = session.get_jsonl_path()
|
|
78
|
+
|
|
79
|
+
assert result is None
|
|
80
|
+
# Should try marker-based discovery when cached path doesn't exist
|
|
81
|
+
mock_find.assert_called_once()
|
|
82
|
+
|
|
83
|
+
@patch("claude_team_mcp.registry.find_codex_session_by_internal_id")
|
|
84
|
+
def test_caches_path_on_successful_discovery(self, mock_find):
|
|
85
|
+
"""get_jsonl_path caches the path when marker discovery succeeds."""
|
|
86
|
+
mock_match = MagicMock()
|
|
87
|
+
mock_match.jsonl_path = Path("/tmp/discovered-codex.jsonl")
|
|
88
|
+
mock_find.return_value = mock_match
|
|
89
|
+
|
|
90
|
+
session = self._make_codex_session()
|
|
91
|
+
|
|
92
|
+
result = session.get_jsonl_path()
|
|
93
|
+
|
|
94
|
+
assert result == Path("/tmp/discovered-codex.jsonl")
|
|
95
|
+
assert session.codex_jsonl_path == Path("/tmp/discovered-codex.jsonl")
|
|
96
|
+
|
|
97
|
+
@patch("claude_team_mcp.registry.find_codex_session_by_internal_id")
|
|
98
|
+
def test_uses_86400_max_age_for_rediscovery(self, mock_find):
|
|
99
|
+
"""get_jsonl_path uses 24h max_age (not 600s) for marker discovery."""
|
|
100
|
+
mock_find.return_value = None
|
|
101
|
+
session = self._make_codex_session()
|
|
102
|
+
|
|
103
|
+
session.get_jsonl_path()
|
|
104
|
+
|
|
105
|
+
mock_find.assert_called_once_with("test-abc1", max_age_seconds=86400)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TestCodexIsIdleNoBlindFallback:
|
|
109
|
+
"""is_idle() for Codex workers must not use blind fallback."""
|
|
110
|
+
|
|
111
|
+
def _make_codex_session(self, session_id="test-abc1"):
|
|
112
|
+
"""Create a ManagedSession configured as a Codex worker."""
|
|
113
|
+
mock_terminal = MagicMock()
|
|
114
|
+
return ManagedSession(
|
|
115
|
+
session_id=session_id,
|
|
116
|
+
terminal_session=mock_terminal,
|
|
117
|
+
project_path="/test/project",
|
|
118
|
+
agent_type="codex",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
@patch("claude_team_mcp.registry.find_codex_session_by_internal_id")
|
|
122
|
+
def test_returns_false_when_no_session_file(self, mock_find):
|
|
123
|
+
"""is_idle returns False (not True from wrong file) when discovery fails."""
|
|
124
|
+
mock_find.return_value = None
|
|
125
|
+
session = self._make_codex_session()
|
|
126
|
+
|
|
127
|
+
result = session.is_idle()
|
|
128
|
+
|
|
129
|
+
assert result is False
|
|
130
|
+
|
|
131
|
+
@patch("claude_team_mcp.idle_detection.is_codex_idle")
|
|
132
|
+
@patch("claude_team_mcp.registry.find_codex_session_by_internal_id")
|
|
133
|
+
def test_uses_correct_session_file(self, mock_find, mock_is_idle):
|
|
134
|
+
"""is_idle uses the cached/discovered path, not a random recent file."""
|
|
135
|
+
mock_find.return_value = None
|
|
136
|
+
session = self._make_codex_session()
|
|
137
|
+
|
|
138
|
+
# Create a temp file for the cached path
|
|
139
|
+
cached_path = Path("/tmp/test-correct-session.jsonl")
|
|
140
|
+
cached_path.touch()
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
session.codex_jsonl_path = cached_path
|
|
144
|
+
mock_is_idle.return_value = True
|
|
145
|
+
|
|
146
|
+
result = session.is_idle()
|
|
147
|
+
|
|
148
|
+
assert result is True
|
|
149
|
+
mock_is_idle.assert_called_once_with(cached_path)
|
|
150
|
+
# Should NOT have called find_codex_session_by_internal_id
|
|
151
|
+
mock_find.assert_not_called()
|
|
152
|
+
finally:
|
|
153
|
+
cached_path.unlink(missing_ok=True)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestCodexJsonlPathInToDict:
|
|
157
|
+
"""codex_jsonl_path must be serialized for event log persistence."""
|
|
158
|
+
|
|
159
|
+
def test_to_dict_includes_codex_jsonl_path_when_set(self):
|
|
160
|
+
"""ManagedSession.to_dict() includes codex_jsonl_path as string."""
|
|
161
|
+
mock_terminal = MagicMock()
|
|
162
|
+
session = ManagedSession(
|
|
163
|
+
session_id="test-123",
|
|
164
|
+
terminal_session=mock_terminal,
|
|
165
|
+
project_path="/test/path",
|
|
166
|
+
agent_type="codex",
|
|
167
|
+
)
|
|
168
|
+
session.codex_jsonl_path = Path("/tmp/codex-session.jsonl")
|
|
169
|
+
|
|
170
|
+
d = session.to_dict()
|
|
171
|
+
|
|
172
|
+
assert d["codex_jsonl_path"] == "/tmp/codex-session.jsonl"
|
|
173
|
+
|
|
174
|
+
def test_to_dict_includes_none_codex_jsonl_path(self):
|
|
175
|
+
"""ManagedSession.to_dict() includes None when codex_jsonl_path not set."""
|
|
176
|
+
mock_terminal = MagicMock()
|
|
177
|
+
session = ManagedSession(
|
|
178
|
+
session_id="test-123",
|
|
179
|
+
terminal_session=mock_terminal,
|
|
180
|
+
project_path="/test/path",
|
|
181
|
+
agent_type="claude",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
d = session.to_dict()
|
|
185
|
+
|
|
186
|
+
assert d["codex_jsonl_path"] is None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class TestRecoveredSessionCodexJsonlPath:
|
|
190
|
+
"""RecoveredSession must persist codex_jsonl_path from event data."""
|
|
191
|
+
|
|
192
|
+
def test_recovered_session_includes_codex_jsonl_path(self):
|
|
193
|
+
"""RecoveredSession stores codex_jsonl_path from snapshot data."""
|
|
194
|
+
now = datetime.now()
|
|
195
|
+
session = RecoveredSession(
|
|
196
|
+
session_id="abc12345",
|
|
197
|
+
name="Rick",
|
|
198
|
+
project_path="/test/project",
|
|
199
|
+
terminal_id=TerminalId("tmux", "%1"),
|
|
200
|
+
agent_type="codex",
|
|
201
|
+
status=SessionStatus.READY,
|
|
202
|
+
last_activity=now,
|
|
203
|
+
created_at=now,
|
|
204
|
+
event_state="idle",
|
|
205
|
+
recovered_at=now,
|
|
206
|
+
last_event_ts=now,
|
|
207
|
+
codex_jsonl_path="/codex/sessions/2026/02/05/rollout-abc.jsonl",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
assert session.codex_jsonl_path == "/codex/sessions/2026/02/05/rollout-abc.jsonl"
|
|
211
|
+
|
|
212
|
+
def test_recovered_session_to_dict_includes_codex_jsonl_path(self):
|
|
213
|
+
"""RecoveredSession.to_dict() includes codex_jsonl_path."""
|
|
214
|
+
now = datetime.now()
|
|
215
|
+
session = RecoveredSession(
|
|
216
|
+
session_id="abc12345",
|
|
217
|
+
name="Rick",
|
|
218
|
+
project_path="/test/project",
|
|
219
|
+
terminal_id=None,
|
|
220
|
+
agent_type="codex",
|
|
221
|
+
status=SessionStatus.READY,
|
|
222
|
+
last_activity=now,
|
|
223
|
+
created_at=now,
|
|
224
|
+
event_state="idle",
|
|
225
|
+
recovered_at=now,
|
|
226
|
+
last_event_ts=now,
|
|
227
|
+
codex_jsonl_path="/codex/sessions/rollout.jsonl",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
d = session.to_dict()
|
|
231
|
+
|
|
232
|
+
assert d["codex_jsonl_path"] == "/codex/sessions/rollout.jsonl"
|
|
233
|
+
|
|
234
|
+
def test_recovered_session_default_codex_jsonl_path_is_none(self):
|
|
235
|
+
"""RecoveredSession.codex_jsonl_path defaults to None."""
|
|
236
|
+
now = datetime.now()
|
|
237
|
+
session = RecoveredSession(
|
|
238
|
+
session_id="abc12345",
|
|
239
|
+
name="Morty",
|
|
240
|
+
project_path="/test/project",
|
|
241
|
+
terminal_id=None,
|
|
242
|
+
agent_type="claude",
|
|
243
|
+
status=SessionStatus.READY,
|
|
244
|
+
last_activity=now,
|
|
245
|
+
created_at=now,
|
|
246
|
+
event_state="idle",
|
|
247
|
+
recovered_at=now,
|
|
248
|
+
last_event_ts=now,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
assert session.codex_jsonl_path is None
|
|
252
|
+
assert session.to_dict()["codex_jsonl_path"] is None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class TestRecoverFromEventsCodexJsonlPath:
|
|
256
|
+
"""recover_from_events must propagate codex_jsonl_path to RecoveredSession."""
|
|
257
|
+
|
|
258
|
+
def test_codex_jsonl_path_recovered_from_snapshot(self):
|
|
259
|
+
"""codex_jsonl_path in worker snapshot data populates RecoveredSession."""
|
|
260
|
+
from claude_team_mcp.registry import SessionRegistry
|
|
261
|
+
|
|
262
|
+
registry = SessionRegistry()
|
|
263
|
+
|
|
264
|
+
snapshot = {
|
|
265
|
+
"ts": "2026-02-05T12:00:00Z",
|
|
266
|
+
"workers": [
|
|
267
|
+
{
|
|
268
|
+
"session_id": "codex-worker-1",
|
|
269
|
+
"name": "Rick",
|
|
270
|
+
"project_path": "/test/project",
|
|
271
|
+
"agent_type": "codex",
|
|
272
|
+
"state": "idle",
|
|
273
|
+
"codex_jsonl_path": "/codex/sessions/2026/02/05/rollout-xyz.jsonl",
|
|
274
|
+
"created_at": "2026-02-05T11:00:00Z",
|
|
275
|
+
"last_activity": "2026-02-05T11:30:00Z",
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
report = registry.recover_from_events(snapshot, [])
|
|
281
|
+
|
|
282
|
+
assert report.added == 1
|
|
283
|
+
recovered = registry._recovered_sessions["codex-worker-1"]
|
|
284
|
+
assert recovered.codex_jsonl_path == "/codex/sessions/2026/02/05/rollout-xyz.jsonl"
|
|
285
|
+
assert recovered.agent_type == "codex"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/cli_backends/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/issue_tracker/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/terminal_backends/__init__.py
RENAMED
|
File without changes
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/terminal_backends/base.py
RENAMED
|
File without changes
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/terminal_backends/iterm.py
RENAMED
|
File without changes
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/terminal_backends/tmux.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/annotate_worker.py
RENAMED
|
File without changes
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/check_idle_workers.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/tools/issue_tracker_help.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_team_mcp-0.9.0 → claude_team_mcp-0.9.2}/src/claude_team_mcp/utils/worktree_detection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|