overcode 0.5.4__tar.gz → 0.5.5__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 (168) hide show
  1. {overcode-0.5.4/src/overcode.egg-info → overcode-0.5.5}/PKG-INFO +2 -2
  2. {overcode-0.5.4 → overcode-0.5.5}/README.md +1 -1
  3. {overcode-0.5.4 → overcode-0.5.5}/pyproject.toml +1 -1
  4. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/opencode.py +4 -1
  5. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/_shared.py +18 -0
  6. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/agent.py +90 -10
  7. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/budget.py +6 -6
  8. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/focal.py +2 -2
  9. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/jobs.py +2 -2
  10. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/monitoring.py +6 -6
  11. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/split.py +63 -30
  12. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/tags.py +4 -4
  13. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/config.py +65 -0
  14. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/data_export.py +5 -0
  15. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/exceptions.py +9 -0
  16. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/follow_mode.py +63 -16
  17. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/history_reader.py +274 -112
  18. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/hook_handler.py +100 -10
  19. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/hook_status_detector.py +74 -7
  20. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/implementations.py +37 -1
  21. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/launcher.py +254 -27
  22. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/mocks.py +45 -2
  23. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/monitor_daemon.py +506 -107
  24. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/monitor_daemon_core.py +16 -0
  25. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/monitor_daemon_state.py +92 -15
  26. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/opencode_plugin/overcode-telemetry-v2/overcode-telemetry-core.mjs +62 -15
  27. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/opencode_plugin/overcode-telemetry.js +161 -21
  28. overcode-0.5.5/src/overcode/pane_capture_gate.py +140 -0
  29. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/presence_logger.py +85 -14
  30. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/protocols.py +26 -0
  31. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/session_manager.py +696 -136
  32. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/settings.py +58 -0
  33. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/sister_controller.py +0 -8
  34. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/standing_instructions.py +0 -21
  35. overcode-0.5.5/src/overcode/stat_gate.py +123 -0
  36. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/stats_reader.py +44 -14
  37. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/status_detector.py +14 -1
  38. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/status_detector_factory.py +14 -0
  39. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/status_history.py +316 -48
  40. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/summarizer_component.py +23 -4
  41. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/supervisor_daemon.py +4 -2
  42. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tmux_manager.py +19 -7
  43. overcode-0.5.5/src/overcode/tmux_utils.py +573 -0
  44. overcode-0.5.5/src/overcode/transcript_index.py +557 -0
  45. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui.py +681 -224
  46. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui.tcss +16 -0
  47. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_actions/session.py +32 -69
  48. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_logic.py +152 -28
  49. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/__init__.py +2 -0
  50. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/daemon_status_bar.py +23 -6
  51. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/help_overlay.py +4 -3
  52. overcode-0.5.5/src/overcode/tui_widgets/rename_agent_modal.py +188 -0
  53. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/status_timeline.py +7 -35
  54. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/web_api.py +19 -7
  55. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/web_control_api.py +3 -31
  56. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/web_server.py +8 -1
  57. overcode-0.5.5/src/overcode/worker_guard.py +163 -0
  58. {overcode-0.5.4 → overcode-0.5.5/src/overcode.egg-info}/PKG-INFO +2 -2
  59. {overcode-0.5.4 → overcode-0.5.5}/src/overcode.egg-info/SOURCES.txt +5 -0
  60. overcode-0.5.4/src/overcode/tmux_utils.py +0 -285
  61. {overcode-0.5.4 → overcode-0.5.5}/LICENSE +0 -0
  62. {overcode-0.5.4 → overcode-0.5.5}/MANIFEST.in +0 -0
  63. {overcode-0.5.4 → overcode-0.5.5}/setup.cfg +0 -0
  64. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/__init__.py +0 -0
  65. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/agent_scanner.py +0 -0
  66. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/__init__.py +0 -0
  67. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/base.py +0 -0
  68. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/claude_code.py +0 -0
  69. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/codex.py +0 -0
  70. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/codex_stats.py +0 -0
  71. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/grok.py +0 -0
  72. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/grok_stats.py +0 -0
  73. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/hermes.py +0 -0
  74. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/hermes_stats.py +0 -0
  75. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/opencode2.py +0 -0
  76. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/opencode2_patterns.py +0 -0
  77. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/opencode2_plugin_install.py +0 -0
  78. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/opencode2_stats.py +0 -0
  79. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/backends/opencode_stats.py +0 -0
  80. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/bundled_skills.py +0 -0
  81. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/claude_config.py +0 -0
  82. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/claude_pid.py +0 -0
  83. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/__init__.py +0 -0
  84. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/__main__.py +0 -0
  85. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/config.py +0 -0
  86. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/daemon.py +0 -0
  87. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/doctor.py +0 -0
  88. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/hooks.py +0 -0
  89. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/models.py +0 -0
  90. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/parallelism.py +0 -0
  91. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/perms.py +0 -0
  92. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/sister.py +0 -0
  93. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/skills.py +0 -0
  94. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/cli/wrappers.py +0 -0
  95. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/daemon_claude_skill.md +0 -0
  96. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/daemon_logging.py +0 -0
  97. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/daemon_utils.py +0 -0
  98. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/data/model_metadata.json +0 -0
  99. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/dependency_check.py +0 -0
  100. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/doctor.py +0 -0
  101. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/duration.py +0 -0
  102. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/entrypoint.py +0 -0
  103. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/hermes_plugin/__init__.py +0 -0
  104. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/hermes_plugin/plugin.yaml +0 -0
  105. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/interfaces.py +0 -0
  106. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/job_launcher.py +0 -0
  107. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/job_manager.py +0 -0
  108. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/logging_config.py +0 -0
  109. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/model_metadata.py +0 -0
  110. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/notifier.py +0 -0
  111. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/opencode_plugin/overcode-telemetry-v2/index.js +0 -0
  112. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/opencode_plugin/overcode-telemetry-v2/tui.js +0 -0
  113. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/pid_utils.py +0 -0
  114. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/pricing.py +0 -0
  115. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/process_resources.py +0 -0
  116. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/sandbox_detect.py +0 -0
  117. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/sister_poller.py +0 -0
  118. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/ssh_provisioner.py +0 -0
  119. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/status_constants.py +0 -0
  120. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/status_patterns.py +0 -0
  121. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/summarizer_client.py +0 -0
  122. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/summary_columns.py +0 -0
  123. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/summary_groups.py +0 -0
  124. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/supervisor_daemon_core.py +0 -0
  125. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/supervisor_layout.sh +0 -0
  126. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/testing/__init__.py +0 -0
  127. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/testing/renderer.py +0 -0
  128. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/testing/tmux_driver.py +0 -0
  129. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/testing/tui_eye.py +0 -0
  130. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/testing/tui_eye_skill.md +0 -0
  131. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/time_context.py +0 -0
  132. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_actions/__init__.py +0 -0
  133. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_actions/daemon.py +0 -0
  134. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_actions/input.py +0 -0
  135. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_actions/navigation.py +0 -0
  136. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_actions/view.py +0 -0
  137. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_helpers.py +0 -0
  138. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_render.py +0 -0
  139. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/agent_select_modal.py +0 -0
  140. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/command_bar.py +0 -0
  141. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/daemon_panel.py +0 -0
  142. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/fullscreen_preview.py +0 -0
  143. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/instruction_history_modal.py +0 -0
  144. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/job_summary.py +0 -0
  145. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/jump_modal.py +0 -0
  146. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/modal_base.py +0 -0
  147. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/new_agent_defaults_modal.py +0 -0
  148. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/new_agent_modal.py +0 -0
  149. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/passthru_config_modal.py +0 -0
  150. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/preview_pane.py +0 -0
  151. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/session_summary.py +0 -0
  152. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/sister_selection_modal.py +0 -0
  153. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/summary_config_modal.py +0 -0
  154. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/tmux_config_modal.py +0 -0
  155. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/tui_widgets/tui_log_panel.py +0 -0
  156. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/usage_monitor.py +0 -0
  157. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/web/__init__.py +0 -0
  158. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/web/templates/analytics.html +0 -0
  159. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/web/templates/dashboard.html +0 -0
  160. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/web_chartjs.py +0 -0
  161. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/web_server_runner.py +0 -0
  162. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/web_templates.py +0 -0
  163. {overcode-0.5.4 → overcode-0.5.5}/src/overcode/wrapper.py +0 -0
  164. {overcode-0.5.4 → overcode-0.5.5}/src/overcode.egg-info/dependency_links.txt +0 -0
  165. {overcode-0.5.4 → overcode-0.5.5}/src/overcode.egg-info/entry_points.txt +0 -0
  166. {overcode-0.5.4 → overcode-0.5.5}/src/overcode.egg-info/requires.txt +0 -0
  167. {overcode-0.5.4 → overcode-0.5.5}/src/overcode.egg-info/top_level.txt +0 -0
  168. {overcode-0.5.4 → overcode-0.5.5}/tests/test_e2e_multi_agent_jokes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: overcode
3
- Version: 0.5.4
3
+ Version: 0.5.5
4
4
  Summary: A supervisor for managing multiple Claude Code instances in tmux
5
5
  Author: Mike Bond
6
6
  Project-URL: Homepage, https://github.com/mkb23/overcode
@@ -176,7 +176,7 @@ See the [TUI Guide](docs/tui-guide.md) for all keyboard shortcuts.
176
176
  - [TUI Guide](docs/tui-guide.md) - Keyboard shortcuts and display modes
177
177
  - [Configuration](docs/configuration.md) - Config file and environment variables
178
178
  - [Wrappers](docs/wrappers.md) - Run agents in containers and custom environments
179
- - [Advanced Features](docs/advanced-features.md) - Sleep mode, handover, remote monitoring
179
+ - [Advanced Features](docs/advanced-features.md) - Sleep mode, remote monitoring
180
180
 
181
181
  ## Testing
182
182
 
@@ -134,7 +134,7 @@ See the [TUI Guide](docs/tui-guide.md) for all keyboard shortcuts.
134
134
  - [TUI Guide](docs/tui-guide.md) - Keyboard shortcuts and display modes
135
135
  - [Configuration](docs/configuration.md) - Config file and environment variables
136
136
  - [Wrappers](docs/wrappers.md) - Run agents in containers and custom environments
137
- - [Advanced Features](docs/advanced-features.md) - Sleep mode, handover, remote monitoring
137
+ - [Advanced Features](docs/advanced-features.md) - Sleep mode, remote monitoring
138
138
 
139
139
  ## Testing
140
140
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "overcode"
7
- version = "0.5.4"
7
+ version = "0.5.5"
8
8
  description = "A supervisor for managing multiple Claude Code instances in tmux"
9
9
  authors = [
10
10
  {name = "Mike Bond"}
@@ -14,7 +14,10 @@ remains the fallback for a launch where the plugin could not be installed.
14
14
  Everything below the flag table was captured from a real opencode
15
15
  v1.18.19 session; the pane corpus lives in
16
16
  ``tests/fixtures_opencode_panes/`` and is replayed by
17
- ``tests/unit/test_status_detector_opencode.py``.
17
+ ``tests/unit/test_status_detector_opencode.py``. Re-verified unchanged
18
+ against v1.18.29 (Sep 2026, #474): a second pane corpus under
19
+ ``tests/fixtures_opencode_panes/v1.18.29/`` and the plugin's event-stream
20
+ corpus in ``tests/fixtures_opencode_events/``.
18
21
  """
19
22
 
20
23
  import json
@@ -109,6 +109,24 @@ app.add_typer(models_app, name="models")
109
109
 
110
110
  # Console for rich output
111
111
  console = Console()
112
+ # Notices that must not mix into output a script may parse
113
+ err_console = Console(stderr=True)
114
+
115
+
116
+ def find_agent(manager, name: str):
117
+ """Look up the agent ``name`` addresses, following renames (#478).
118
+
119
+ ``manager`` is a SessionManager. When ``name`` is an agent's old name,
120
+ the agent is returned and a one-line notice goes to stderr, so a person
121
+ or script still using the old name learns the new one.
122
+ """
123
+ from ..session_manager import rename_notice
124
+
125
+ session = manager.resolve_session_name(name)
126
+ notice = rename_notice(name, session)
127
+ if notice:
128
+ err_console.print(f"[yellow]{notice}[/yellow]")
129
+ return session
112
130
 
113
131
  # Global session option (hidden advanced usage)
114
132
  SessionOption = Annotated[
@@ -9,7 +9,7 @@ import typer
9
9
  from rich import print as rprint
10
10
 
11
11
  from ..launcher import AgentLauncher
12
- from ._shared import app, SessionOption, _parse_duration
12
+ from ._shared import app, SessionOption, _parse_duration, find_agent
13
13
 
14
14
 
15
15
  def _parse_oversight_policy(on_stuck: Optional[str], oversight_timeout: Optional[str]) -> tuple[str, float]:
@@ -427,7 +427,7 @@ def fork(
427
427
  from ..backends import BackendCapability, get_backend, supports
428
428
 
429
429
  sm = SessionManager()
430
- source_session = sm.get_session_by_name(source)
430
+ source_session = find_agent(sm, source)
431
431
  if not source_session:
432
432
  rprint(f"[red]Error: Agent '{source}' not found[/red]")
433
433
  raise typer.Exit(code=1)
@@ -546,7 +546,7 @@ def list_agents(
546
546
 
547
547
  # Filter to specific agent + descendants if name given (#244)
548
548
  if name:
549
- root = launcher.sessions.get_session_by_name(name)
549
+ root = find_agent(launcher.sessions, name)
550
550
  if not root:
551
551
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
552
552
  raise typer.Exit(code=1)
@@ -842,7 +842,7 @@ def restart(
842
842
  from ..launcher import AgentLauncher
843
843
 
844
844
  launcher = AgentLauncher(session)
845
- sess = launcher.sessions.get_session_by_name(name)
845
+ sess = find_agent(launcher.sessions, name)
846
846
  if not sess:
847
847
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
848
848
  raise typer.Exit(code=1)
@@ -879,6 +879,83 @@ def follow(
879
879
  raise typer.Exit(code=exit_code)
880
880
 
881
881
 
882
+ @app.command()
883
+ def rename(
884
+ name: Annotated[str, typer.Argument(help="Current name of agent")],
885
+ new_name: Annotated[str, typer.Argument(help="New name for the agent")],
886
+ force: Annotated[
887
+ bool,
888
+ typer.Option(
889
+ "--force",
890
+ help="Rename even if the agent is mid-turn (its turn is cancelled)",
891
+ ),
892
+ ] = False,
893
+ session: SessionOption = "agents",
894
+ ):
895
+ """Rename an agent, keeping its conversation and history.
896
+
897
+ A live agent is briefly stopped and resumed under the new name, in the
898
+ same window with the same conversation; it is told its new name with
899
+ its next prompt. The old name keeps working as an alias — `send`,
900
+ `follow`, `kill` and the rest reach the agent and say it was renamed —
901
+ so a parent or script still using it is not broken.
902
+
903
+ An agent that is busy (working, or showing a permission dialog) is not
904
+ renamed, because stopping it would cancel its turn; wait until it is
905
+ idle, or pass --force. A terminated agent just has its record renamed.
906
+
907
+ Examples:
908
+ overcode rename bugfix-1 auth-refactor
909
+ """
910
+ from ..exceptions import AgentBusyError, InvalidSessionNameError
911
+ from ..launcher import AgentLauncher
912
+
913
+ launcher = AgentLauncher(session)
914
+ sess = find_agent(launcher.sessions, name)
915
+ if not sess:
916
+ rprint(f"[red]Error: Agent '{name}' not found[/red]")
917
+ raise typer.Exit(code=1)
918
+ name = sess.name
919
+
920
+ try:
921
+ renamed = launcher.rename(sess, new_name, force=force)
922
+ except InvalidSessionNameError:
923
+ rprint(
924
+ f"[red]Error: '{new_name}' is not a valid agent name "
925
+ f"(letters, digits, - and _ only, max 64 chars)[/red]"
926
+ )
927
+ raise typer.Exit(code=1)
928
+ except AgentBusyError as exc:
929
+ state = (
930
+ "could not be checked" if exc.status == "unknown" else f"is busy ({exc.status})"
931
+ )
932
+ rprint(
933
+ f"[yellow]Agent '{name}' {state}.[/yellow] Renaming restarts it, "
934
+ "which would cancel a turn in progress. Try again when it is idle, "
935
+ "or use --force."
936
+ )
937
+ raise typer.Exit(code=1)
938
+ except ValueError as exc:
939
+ rprint(f"[red]Error: {exc}[/red]")
940
+ raise typer.Exit(code=1)
941
+
942
+ if not renamed:
943
+ current = launcher.sessions.get_session(sess.id)
944
+ if current is not None and current.name == new_name:
945
+ rprint(
946
+ f"[yellow]Renamed '{name}' → '{new_name}', but the relaunch could "
947
+ f"not be sent.[/yellow] Run: overcode restart {new_name}"
948
+ )
949
+ else:
950
+ rprint(
951
+ f"[red]Error: tmux refused to rename the window; '{name}' was "
952
+ "restarted under its old name.[/red]"
953
+ )
954
+ raise typer.Exit(code=1)
955
+
956
+ rprint(f"[green]Renamed agent: {name} → {new_name} (conversation preserved)[/green]")
957
+
958
+
882
959
  @app.command()
883
960
  def report(
884
961
  status: Annotated[
@@ -930,7 +1007,7 @@ def report(
930
1007
  # Also update session fields for persistence
931
1008
  from ..session_manager import SessionManager
932
1009
  sm = SessionManager()
933
- session = sm.get_session_by_name(agent_name)
1010
+ session = find_agent(sm, agent_name)
934
1011
  if session:
935
1012
  sm.update_session(
936
1013
  session.id,
@@ -996,7 +1073,7 @@ def set_value(
996
1073
  from ..session_manager import SessionManager
997
1074
 
998
1075
  manager = SessionManager()
999
- agent = manager.get_session_by_name(name)
1076
+ agent = find_agent(manager, name)
1000
1077
  if not agent:
1001
1078
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
1002
1079
  raise typer.Exit(code=1)
@@ -1018,7 +1095,7 @@ def set_budget(
1018
1095
  from ..session_manager import SessionManager
1019
1096
 
1020
1097
  manager = SessionManager()
1021
- agent = manager.get_session_by_name(name)
1098
+ agent = find_agent(manager, name)
1022
1099
  if not agent:
1023
1100
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
1024
1101
  raise typer.Exit(code=1)
@@ -1055,7 +1132,7 @@ def annotate(
1055
1132
  from ..session_manager import SessionManager
1056
1133
 
1057
1134
  manager = SessionManager()
1058
- agent = manager.get_session_by_name(name)
1135
+ agent = find_agent(manager, name)
1059
1136
  if not agent:
1060
1137
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
1061
1138
  raise typer.Exit(code=1)
@@ -1099,7 +1176,9 @@ def send(
1099
1176
  sm = SessionManager()
1100
1177
 
1101
1178
  # Auto-wake sleeping agent (#168)
1102
- agent_session = sm.get_session_by_name(name)
1179
+ agent_session = find_agent(sm, name)
1180
+ if agent_session:
1181
+ name = agent_session.name # an old name resolved; say the current one
1103
1182
  if agent_session and agent_session.is_asleep:
1104
1183
  sm.update_session(agent_session.id, is_asleep=False)
1105
1184
  rprint(f"[dim]Woke agent '{name}' to send command[/dim]")
@@ -1149,10 +1228,11 @@ def show(
1149
1228
  launcher = AgentLauncher(session)
1150
1229
 
1151
1230
  # Get the Session object
1152
- sess = launcher.sessions.get_session_by_name(name)
1231
+ sess = find_agent(launcher.sessions, name)
1153
1232
  if sess is None:
1154
1233
  rprint(f"[red]✗[/red] Agent '[bold]{name}[/bold]' not found")
1155
1234
  raise typer.Exit(1)
1235
+ name = sess.name # an old name resolved; show under the current one
1156
1236
 
1157
1237
  # Read daemon state for status/activity (single source of truth)
1158
1238
  daemon_state = get_monitor_daemon_state(session)
@@ -7,7 +7,7 @@ from typing import Annotated, Optional
7
7
  import typer
8
8
  from rich import print as rprint
9
9
 
10
- from ._shared import budget_app, SessionOption
10
+ from ._shared import budget_app, SessionOption, find_agent
11
11
 
12
12
 
13
13
  @budget_app.command("set")
@@ -28,7 +28,7 @@ def budget_set(
28
28
  from ..session_manager import SessionManager
29
29
 
30
30
  manager = SessionManager()
31
- agent = manager.get_session_by_name(name)
31
+ agent = find_agent(manager, name)
32
32
  if not agent:
33
33
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
34
34
  raise typer.Exit(code=1)
@@ -73,12 +73,12 @@ def budget_transfer(
73
73
 
74
74
  manager = SessionManager()
75
75
 
76
- source_agent = manager.get_session_by_name(source)
76
+ source_agent = find_agent(manager, source)
77
77
  if not source_agent:
78
78
  rprint(f"[red]Error: Source agent '{source}' not found[/red]")
79
79
  raise typer.Exit(code=1)
80
80
 
81
- target_agent = manager.get_session_by_name(target)
81
+ target_agent = find_agent(manager, target)
82
82
  if not target_agent:
83
83
  rprint(f"[red]Error: Target agent '{target}' not found[/red]")
84
84
  raise typer.Exit(code=1)
@@ -118,7 +118,7 @@ def budget_reclaim(
118
118
  from ..session_manager import SessionManager
119
119
 
120
120
  manager = SessionManager()
121
- agent = manager.get_session_by_name(name)
121
+ agent = find_agent(manager, name)
122
122
  if not agent:
123
123
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
124
124
  raise typer.Exit(code=1)
@@ -162,7 +162,7 @@ def budget_show(
162
162
 
163
163
  if name:
164
164
  agents = []
165
- agent = manager.get_session_by_name(name)
165
+ agent = find_agent(manager, name)
166
166
  if not agent:
167
167
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
168
168
  raise typer.Exit(code=1)
@@ -7,7 +7,7 @@ from typing import Annotated, Optional
7
7
  import typer
8
8
  from rich import print as rprint
9
9
 
10
- from ._shared import app, SessionOption
10
+ from ._shared import app, SessionOption, find_agent
11
11
 
12
12
 
13
13
  @app.command("focal-repo")
@@ -40,7 +40,7 @@ def focal_repo(
40
40
  from ..session_manager import SessionManager
41
41
 
42
42
  manager = SessionManager()
43
- agent = manager.get_session_by_name(name)
43
+ agent = find_agent(manager, name)
44
44
  if not agent:
45
45
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
46
46
  raise typer.Exit(code=1)
@@ -7,7 +7,7 @@ from typing import Annotated, Optional
7
7
  import typer
8
8
  from rich import print as rprint
9
9
 
10
- from ._shared import app, jobs_app
10
+ from ._shared import app, jobs_app, find_agent
11
11
 
12
12
 
13
13
  @app.command()
@@ -32,7 +32,7 @@ def bash(
32
32
  agent = os.environ.get("OVERCODE_SESSION_NAME")
33
33
  if agent:
34
34
  sm = SessionManager()
35
- sess = sm.get_session_by_name(agent)
35
+ sess = find_agent(sm, agent)
36
36
  if sess:
37
37
  agent_session_id = sess.id
38
38
  agent_name = sess.name
@@ -9,7 +9,7 @@ from typing import Annotated, Optional, List
9
9
  import typer
10
10
  from rich import print as rprint
11
11
 
12
- from ._shared import app, SessionOption
12
+ from ._shared import app, SessionOption, find_agent
13
13
 
14
14
 
15
15
  @app.command("hook-handler", hidden=True)
@@ -67,7 +67,7 @@ def instruct(
67
67
  raise typer.Exit(1)
68
68
 
69
69
  sessions = SessionManager()
70
- sess = sessions.get_session_by_name(name)
70
+ sess = find_agent(sessions, name)
71
71
 
72
72
  if sess is None:
73
73
  rprint(f"[red]✗[/red] Agent '[bold]{name}[/bold]' not found")
@@ -150,7 +150,7 @@ def heartbeat(
150
150
  from ..tui_helpers import format_duration
151
151
 
152
152
  manager = SessionManager()
153
- agent = manager.get_session_by_name(name)
153
+ agent = find_agent(manager, name)
154
154
  if not agent:
155
155
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
156
156
  raise typer.Exit(code=1)
@@ -463,9 +463,9 @@ def history(
463
463
  sessions = SessionManager()
464
464
 
465
465
  if name:
466
- # Show specific archived session
467
- archived = sessions.list_archived_sessions()
468
- session = next((s for s in archived if s.name == name), None)
466
+ # Show specific archived session: stream the archive (one record
467
+ # at a time, file order) instead of materialising every entry.
468
+ session = next((s for s in sessions.iter_archived_sessions() if s.name == name), None)
469
469
  if not session:
470
470
  rprint(f"[red]✗[/red] No archived session named '[bold]{name}[/bold]'")
471
471
  raise typer.Exit(1)
@@ -72,9 +72,40 @@ def _acquire_setup_lock() -> bool:
72
72
 
73
73
 
74
74
  def _release_setup_lock() -> None:
75
- """Release the setup lock."""
75
+ """Release the setup lock, but only if this process still owns it.
76
+
77
+ Release is ownership-aware because it can legitimately run twice: once in
78
+ `_exec_tmux_attach` just before exec, and again from the outer `finally:`
79
+ in `_tmux_layout` if that exec raises. Between the two, another
80
+ `overcode tmux` may have acquired the lock; unlinking unconditionally
81
+ would delete *its* lock and let a third invocation run setup concurrently.
82
+ """
76
83
  lock_path = Path.home() / ".overcode" / "tmux-setup.lock"
77
- lock_path.unlink(missing_ok=True)
84
+ try:
85
+ holder = lock_path.read_text().strip()
86
+ except FileNotFoundError:
87
+ return # already released, or never acquired
88
+ if holder == str(os.getpid()):
89
+ lock_path.unlink(missing_ok=True)
90
+
91
+
92
+ def _exec_tmux_attach(oc_session: str) -> None:
93
+ """Release the setup lock, then replace this process with `tmux attach`.
94
+
95
+ `os.execvp` never returns, so any `finally:` that would normally release
96
+ the lock is skipped. Worse, the PID recorded in the lock file becomes the
97
+ long-lived tmux client, so the stale-lock check treats it as a genuine
98
+ holder for as long as the client stays attached and every later
99
+ `overcode tmux` is refused. Always go through this helper to exec.
100
+
101
+ If exec itself fails, the caller's `finally:` releases again; that is
102
+ safe because `_release_setup_lock` only removes a lock this PID owns.
103
+ """
104
+ from ..tmux_utils import _build_tmux_cmd
105
+
106
+ _release_setup_lock()
107
+ _cmd = [*_build_tmux_cmd(), "attach-session", "-t", oc_session]
108
+ os.execvp("tmux", _cmd)
78
109
 
79
110
 
80
111
  def _find_overcode_cmd() -> str:
@@ -785,22 +816,22 @@ def _tmux_layout_locked(session: str, ratio: int, rprint, *, restart: bool = Fal
785
816
  if existing:
786
817
  if _is_split_window_healthy(existing):
787
818
  if in_tmux:
788
- # Check if a real (non-nested) client is already on overcode.
789
- # If so, no switch needed — the user is already there or
790
- # another terminal has it open. Switching blindly can
791
- # accidentally move the bottom pane's nested client from
792
- # oc-view-agents to overcode, creating a recursive display
819
+ # Move the caller's own client to the split window — but
820
+ # never the bottom pane's nested client. Switching that one
821
+ # from oc-view-agents to overcode creates a recursive display
793
822
  # that collapses the window.
794
823
  #
795
824
  # Detect nested clients by comparing client TTYs against
796
825
  # pane TTYs in the split window (#387). A nested tmux
797
826
  # client's TTY matches one of the pane TTYs.
798
- pane_ttys = set(
799
- _tmux_output(
827
+ pane_ttys = {
828
+ tty.strip()
829
+ for tty in _tmux_output(
800
830
  "list-panes", "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}",
801
831
  "-F", "#{pane_tty}",
802
832
  ).splitlines()
803
- )
833
+ if tty.strip()
834
+ }
804
835
  oc_clients = _tmux_output(
805
836
  "list-clients", "-t", oc_session,
806
837
  "-F", "#{client_tty}",
@@ -810,29 +841,33 @@ def _tmux_layout_locked(session: str, ratio: int, rprint, *, restart: bool = Fal
810
841
  for tty in oc_clients.splitlines()
811
842
  if tty.strip()
812
843
  )
813
- if not has_real_client:
814
- # No real client on overcode yet — switch the caller's
815
- client_tty = _tmux_output(
816
- "display-message", "-p", "#{client_tty}",
817
- )
818
- if client_tty:
819
- _tmux("switch-client", "-c", client_tty,
820
- "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}")
821
- else:
822
- # Can't determine client — tell user how to get there
823
- rprint(f"[green]Split layout is running.[/green] Switch to it with:")
824
- rprint(f" tmux switch-client -t {oc_session}")
825
- return
844
+ client_tty = _tmux_output(
845
+ "display-message", "-p", "#{client_tty}",
846
+ ).strip()
847
+ if client_tty and client_tty not in pane_ttys:
848
+ # The caller may already be attached to the overcode
849
+ # session but sitting on another window (e.g. an agent
850
+ # window opened there), so always target the split
851
+ # window explicitly rather than just the session.
852
+ _tmux("switch-client", "-c", client_tty,
853
+ "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}")
854
+ elif client_tty:
855
+ # Caller is inside the nested bottom pane — it's already
856
+ # looking at the split; leave the client alone.
857
+ pass
858
+ elif not has_real_client:
859
+ # Can't determine client — tell user how to get there
860
+ rprint(f"[green]Split layout is running.[/green] Switch to it with:")
861
+ rprint(f" tmux switch-client -t {oc_session}")
862
+ return
826
863
  else:
827
- # Respawn before attach — execlp replaces this process
864
+ # Respawn before attach — exec replaces this process
828
865
  _tmux("respawn-pane", "-k",
829
866
  "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.{get_pane_base_index()}",
830
867
  monitor_cmd)
831
868
  rprint(f"[green]Attaching to existing {SPLIT_WINDOW_NAME} window (monitor restarted)...[/green]")
832
869
  time.sleep(0.2)
833
- from ..tmux_utils import _build_tmux_cmd
834
- _cmd = [*_build_tmux_cmd(), "attach-session", "-t", oc_session]
835
- os.execvp("tmux", _cmd)
870
+ _exec_tmux_attach(oc_session)
836
871
  # Always restart the monitor so code changes take effect
837
872
  _tmux("respawn-pane", "-k",
838
873
  "-t", f"{oc_session}:{SPLIT_WINDOW_NAME}.{get_pane_base_index()}",
@@ -1004,6 +1039,4 @@ def _tmux_layout_locked(session: str, ratio: int, rprint, *, restart: bool = Fal
1004
1039
  # Attach to the session (replaces this process)
1005
1040
  rprint(f"[green]Attaching to split layout...[/green]")
1006
1041
  time.sleep(0.2)
1007
- from ..tmux_utils import _build_tmux_cmd
1008
- _cmd = [*_build_tmux_cmd(), "attach-session", "-t", oc_session]
1009
- os.execvp("tmux", _cmd)
1042
+ _exec_tmux_attach(oc_session)
@@ -11,7 +11,7 @@ import typer
11
11
  from rich import print as rprint
12
12
  from rich.table import Table
13
13
 
14
- from ._shared import app, SessionOption
14
+ from ._shared import app, SessionOption, find_agent
15
15
 
16
16
 
17
17
  @app.command("tag")
@@ -32,7 +32,7 @@ def tag_add(
32
32
  from ..session_manager import SessionManager
33
33
 
34
34
  manager = SessionManager()
35
- agent = manager.get_session_by_name(name)
35
+ agent = find_agent(manager, name)
36
36
  if not agent:
37
37
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
38
38
  raise typer.Exit(code=1)
@@ -66,7 +66,7 @@ def tag_remove(
66
66
  from ..session_manager import SessionManager
67
67
 
68
68
  manager = SessionManager()
69
- agent = manager.get_session_by_name(name)
69
+ agent = find_agent(manager, name)
70
70
  if not agent:
71
71
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
72
72
  raise typer.Exit(code=1)
@@ -95,7 +95,7 @@ def tags_list(
95
95
 
96
96
  manager = SessionManager()
97
97
  if name:
98
- agent = manager.get_session_by_name(name)
98
+ agent = find_agent(manager, name)
99
99
  if not agent:
100
100
  rprint(f"[red]Error: Agent '{name}' not found[/red]")
101
101
  raise typer.Exit(code=1)
@@ -624,6 +624,71 @@ def get_model_metadata_config() -> dict:
624
624
  }
625
625
 
626
626
 
627
+ def get_session_archive_config() -> dict:
628
+ """How long terminated sessions stay in sessions.json before being archived.
629
+
630
+ sessions.json is the shared, whole-file-rewritten state every TUI and
631
+ daemon parses; the monitor daemon moves entries whose status has been
632
+ ``terminated`` for longer than the grace to the append-only
633
+ ``archive.jsonl`` (the same record ``overcode cleanup`` writes), so the
634
+ live file stays the size of the live fleet. The TUI's "show terminated"
635
+ ghosts keep showing a killed agent for the whole grace.
636
+
637
+ Config format in ~/.overcode/config.yaml:
638
+ session_archive:
639
+ terminated_grace_seconds: 3600 # -1 (or any negative) disables automatic archiving
640
+
641
+ Returns:
642
+ Dict with ``terminated_grace_seconds`` (float); a missing or invalid
643
+ entry falls back to ``settings.DAEMON.terminated_archive_grace_seconds``.
644
+ """
645
+ from .settings import DAEMON
646
+
647
+ cfg = _get_config_value("session_archive", {})
648
+ if not isinstance(cfg, dict):
649
+ cfg = {}
650
+ default = float(DAEMON.terminated_archive_grace_seconds)
651
+ try:
652
+ grace = float(cfg.get("terminated_grace_seconds", default))
653
+ except (TypeError, ValueError):
654
+ grace = default
655
+ return {"terminated_grace_seconds": grace}
656
+
657
+
658
+ def get_monitor_daemon_config() -> dict:
659
+ """The monitor daemon's loop interval while nobody is watching.
660
+
661
+ With no client attached to the agents tmux session, no fresh TUI
662
+ keypress heartbeat and no TUI touching its attended file, the daemon
663
+ stretches its loop from ``interval_fast`` (2 s) to this many seconds.
664
+ Status history is written on change, so the timeline it keeps for the
665
+ user's return has no holes at this resolution. Read once when the
666
+ daemon starts.
667
+
668
+ Config format in ~/.overcode/config.yaml:
669
+ monitor_daemon:
670
+ interval_unattended_seconds: 10 # >= interval_fast
671
+ # (the default is settings.DAEMON.interval_unattended)
672
+
673
+ Returns:
674
+ Dict with ``interval_unattended`` (int); a missing or invalid entry,
675
+ or one below ``interval_fast``, falls back to the default.
676
+ """
677
+ from .settings import DAEMON
678
+
679
+ cfg = _get_config_value("monitor_daemon", {})
680
+ if not isinstance(cfg, dict):
681
+ cfg = {}
682
+ default = int(DAEMON.interval_unattended)
683
+ try:
684
+ interval = int(cfg.get("interval_unattended_seconds", default))
685
+ except (TypeError, ValueError):
686
+ interval = default
687
+ if interval < DAEMON.interval_fast:
688
+ interval = default
689
+ return {"interval_unattended": interval}
690
+
691
+
627
692
  def get_sisters_config() -> List[dict]:
628
693
  """Get sister instance configuration for cross-machine monitoring.
629
694
 
@@ -199,6 +199,11 @@ def _get_sessions_schema():
199
199
 
200
200
 
201
201
  def _get_timeline_schema():
202
+ # One record per agent_status_history.csv row, as written: a row is
203
+ # written when the agent's (status, activity) changes and as a periodic
204
+ # keepalive (status_history.status_row_due), so each record is valid
205
+ # for its agent until that agent's next record — not a fixed-interval
206
+ # sample.
202
207
  import pyarrow as pa
203
208
  return pa.schema([
204
209
  ("timestamp", pa.string()),
@@ -119,6 +119,15 @@ class SessionAlreadyExistsError(SessionError):
119
119
  super().__init__(f"Session '{name}' already exists")
120
120
 
121
121
 
122
+ class AgentBusyError(SessionError):
123
+ """The agent is mid-turn, so an operation that restarts it was refused (#478)."""
124
+
125
+ def __init__(self, name: str, status: str):
126
+ self.name = name
127
+ self.status = status
128
+ super().__init__(f"Agent '{name}' is busy ({status})")
129
+
130
+
122
131
  class SessionLaunchError(SessionError):
123
132
  """Error launching an agent session."""
124
133