overcode 0.1.2__py3-none-any.whl → 0.1.4__py3-none-any.whl
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.
- overcode/__init__.py +1 -1
- overcode/cli.py +154 -51
- overcode/config.py +66 -0
- overcode/daemon_claude_skill.md +36 -33
- overcode/history_reader.py +69 -8
- overcode/implementations.py +178 -87
- overcode/monitor_daemon.py +87 -97
- overcode/monitor_daemon_core.py +261 -0
- overcode/monitor_daemon_state.py +24 -15
- overcode/pid_utils.py +17 -3
- overcode/session_manager.py +54 -0
- overcode/settings.py +34 -0
- overcode/status_constants.py +1 -1
- overcode/status_detector.py +8 -2
- overcode/status_patterns.py +19 -0
- overcode/summarizer_client.py +72 -27
- overcode/summarizer_component.py +87 -107
- overcode/supervisor_daemon.py +55 -38
- overcode/supervisor_daemon_core.py +210 -0
- overcode/testing/__init__.py +6 -0
- overcode/testing/renderer.py +268 -0
- overcode/testing/tmux_driver.py +223 -0
- overcode/testing/tui_eye.py +185 -0
- overcode/testing/tui_eye_skill.md +187 -0
- overcode/tmux_manager.py +117 -93
- overcode/tui.py +399 -1969
- overcode/tui_actions/__init__.py +20 -0
- overcode/tui_actions/daemon.py +201 -0
- overcode/tui_actions/input.py +128 -0
- overcode/tui_actions/navigation.py +117 -0
- overcode/tui_actions/session.py +428 -0
- overcode/tui_actions/view.py +357 -0
- overcode/tui_helpers.py +42 -9
- overcode/tui_logic.py +347 -0
- overcode/tui_render.py +414 -0
- overcode/tui_widgets/__init__.py +24 -0
- overcode/tui_widgets/command_bar.py +399 -0
- overcode/tui_widgets/daemon_panel.py +153 -0
- overcode/tui_widgets/daemon_status_bar.py +245 -0
- overcode/tui_widgets/help_overlay.py +71 -0
- overcode/tui_widgets/preview_pane.py +69 -0
- overcode/tui_widgets/session_summary.py +514 -0
- overcode/tui_widgets/status_timeline.py +253 -0
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/METADATA +4 -1
- overcode-0.1.4.dist-info/RECORD +68 -0
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/WHEEL +1 -1
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/entry_points.txt +1 -0
- overcode-0.1.2.dist-info/RECORD +0 -45
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {overcode-0.1.2.dist-info → overcode-0.1.4.dist-info}/top_level.txt +0 -0
overcode/summarizer_client.py
CHANGED
|
@@ -2,54 +2,93 @@
|
|
|
2
2
|
OpenAI API client for agent summarization.
|
|
3
3
|
|
|
4
4
|
Uses GPT-4o-mini for cost-effective, high-frequency summaries.
|
|
5
|
+
|
|
6
|
+
Configuration via ~/.overcode/config.yaml (preferred) or environment variables (fallback):
|
|
7
|
+
|
|
8
|
+
Config file format:
|
|
9
|
+
summarizer:
|
|
10
|
+
api_url: https://api.openai.com/v1/chat/completions
|
|
11
|
+
model: gpt-4o-mini
|
|
12
|
+
api_key_var: OPENAI_API_KEY
|
|
13
|
+
|
|
14
|
+
Environment variable fallbacks:
|
|
15
|
+
OVERCODE_SUMMARIZER_API_URL
|
|
16
|
+
OVERCODE_SUMMARIZER_MODEL
|
|
17
|
+
OVERCODE_SUMMARIZER_API_KEY_VAR
|
|
5
18
|
"""
|
|
6
19
|
|
|
7
20
|
import json
|
|
8
21
|
import logging
|
|
9
|
-
import os
|
|
10
22
|
import urllib.error
|
|
11
23
|
import urllib.request
|
|
12
24
|
from typing import Optional
|
|
13
25
|
|
|
14
|
-
|
|
26
|
+
from .config import get_summarizer_config
|
|
15
27
|
|
|
16
|
-
|
|
17
|
-
API_URL = "https://api.openai.com/v1/chat/completions"
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
18
29
|
|
|
19
|
-
#
|
|
20
|
-
|
|
30
|
+
# Short summary prompt - focuses on IMMEDIATE ACTION (verb-first, what's happening this second)
|
|
31
|
+
SUMMARIZE_PROMPT_SHORT = """What is the agent doing RIGHT NOW? Answer with the immediate action only.
|
|
21
32
|
|
|
22
|
-
##
|
|
33
|
+
## Terminal (last {lines} lines):
|
|
23
34
|
{pane_content}
|
|
24
35
|
|
|
25
|
-
##
|
|
36
|
+
## Previous:
|
|
37
|
+
{previous_summary}
|
|
38
|
+
|
|
39
|
+
FORMAT: Start with a verb. Examples:
|
|
40
|
+
- "reading src/auth.py"
|
|
41
|
+
- "running pytest -v"
|
|
42
|
+
- "waiting for approval"
|
|
43
|
+
- "writing migration file"
|
|
44
|
+
- "editing line 45"
|
|
26
45
|
|
|
27
|
-
|
|
46
|
+
RULES:
|
|
47
|
+
- Verb first, always (reading/writing/running/waiting/editing/fixing)
|
|
48
|
+
- Name the specific file or command if visible
|
|
49
|
+
- Max 40 chars
|
|
50
|
+
- If unchanged: UNCHANGED"""
|
|
51
|
+
|
|
52
|
+
# Context summary prompt - focuses on THE TASK (noun-first, the feature/bug/goal)
|
|
53
|
+
SUMMARIZE_PROMPT_CONTEXT = """What TASK or FEATURE is being worked on? Not the current action - the goal.
|
|
54
|
+
|
|
55
|
+
## Terminal (last {lines} lines):
|
|
56
|
+
{pane_content}
|
|
57
|
+
|
|
58
|
+
## Previous:
|
|
28
59
|
{previous_summary}
|
|
29
60
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
61
|
+
FORMAT: Describe the task/feature/bug. Examples:
|
|
62
|
+
- "JWT auth migration"
|
|
63
|
+
- "user search pagination"
|
|
64
|
+
- "fix: race condition in queue"
|
|
65
|
+
- "PR #42 review comments"
|
|
66
|
+
- "new settings dark mode"
|
|
36
67
|
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
68
|
+
RULES:
|
|
69
|
+
- Noun/task first (not a verb like "implementing")
|
|
70
|
+
- Include ticket/PR numbers if mentioned
|
|
71
|
+
- Focus on WHAT is being built/fixed, not HOW
|
|
72
|
+
- Max 60 chars
|
|
73
|
+
- If unchanged: UNCHANGED"""
|
|
41
74
|
|
|
42
75
|
|
|
43
76
|
class SummarizerClient:
|
|
44
|
-
"""Client for OpenAI API to generate agent summaries.
|
|
77
|
+
"""Client for OpenAI-compatible API to generate agent summaries.
|
|
78
|
+
|
|
79
|
+
Supports custom API endpoints for corporate gateways via config file or env vars.
|
|
80
|
+
"""
|
|
45
81
|
|
|
46
82
|
def __init__(self, api_key: Optional[str] = None):
|
|
47
83
|
"""Initialize the client.
|
|
48
84
|
|
|
49
85
|
Args:
|
|
50
|
-
api_key:
|
|
86
|
+
api_key: API key. If None, reads from config file or env var.
|
|
51
87
|
"""
|
|
52
|
-
|
|
88
|
+
config = get_summarizer_config()
|
|
89
|
+
self.api_url = config["api_url"]
|
|
90
|
+
self.model = config["model"]
|
|
91
|
+
self.api_key = api_key or config["api_key"]
|
|
53
92
|
self._available = bool(self.api_key)
|
|
54
93
|
|
|
55
94
|
@property
|
|
@@ -64,6 +103,7 @@ class SummarizerClient:
|
|
|
64
103
|
current_status: str,
|
|
65
104
|
lines: int = 200,
|
|
66
105
|
max_tokens: int = 150,
|
|
106
|
+
mode: str = "short",
|
|
67
107
|
) -> Optional[str]:
|
|
68
108
|
"""Get summary from GPT-4o-mini.
|
|
69
109
|
|
|
@@ -73,6 +113,7 @@ class SummarizerClient:
|
|
|
73
113
|
current_status: Current agent status (running, waiting_user, etc.)
|
|
74
114
|
lines: Number of lines being summarized (for prompt context)
|
|
75
115
|
max_tokens: Maximum tokens in response
|
|
116
|
+
mode: "short" for current activity, "context" for wider context
|
|
76
117
|
|
|
77
118
|
Returns:
|
|
78
119
|
New summary text, "UNCHANGED" if no update needed, or None on error
|
|
@@ -80,7 +121,10 @@ class SummarizerClient:
|
|
|
80
121
|
if not self.available:
|
|
81
122
|
return None
|
|
82
123
|
|
|
83
|
-
prompt
|
|
124
|
+
# Select prompt based on mode
|
|
125
|
+
prompt_template = SUMMARIZE_PROMPT_CONTEXT if mode == "context" else SUMMARIZE_PROMPT_SHORT
|
|
126
|
+
|
|
127
|
+
prompt = prompt_template.format(
|
|
84
128
|
lines=lines,
|
|
85
129
|
pane_content=pane_content,
|
|
86
130
|
status=current_status,
|
|
@@ -88,14 +132,14 @@ class SummarizerClient:
|
|
|
88
132
|
)
|
|
89
133
|
|
|
90
134
|
payload = json.dumps({
|
|
91
|
-
"model":
|
|
135
|
+
"model": self.model,
|
|
92
136
|
"max_tokens": max_tokens,
|
|
93
137
|
"temperature": 0.3, # Low temperature for consistent summaries
|
|
94
138
|
"messages": [{"role": "user", "content": prompt}],
|
|
95
139
|
}).encode("utf-8")
|
|
96
140
|
|
|
97
141
|
req = urllib.request.Request(
|
|
98
|
-
|
|
142
|
+
self.api_url,
|
|
99
143
|
data=payload,
|
|
100
144
|
headers={
|
|
101
145
|
"Authorization": f"Bearer {self.api_key}",
|
|
@@ -132,5 +176,6 @@ class SummarizerClient:
|
|
|
132
176
|
|
|
133
177
|
@staticmethod
|
|
134
178
|
def is_available() -> bool:
|
|
135
|
-
"""Check if
|
|
136
|
-
|
|
179
|
+
"""Check if API key is available (from config or environment)."""
|
|
180
|
+
config = get_summarizer_config()
|
|
181
|
+
return bool(config["api_key"])
|
overcode/summarizer_component.py
CHANGED
|
@@ -3,18 +3,17 @@ Summarizer component for generating agent activity summaries.
|
|
|
3
3
|
|
|
4
4
|
Uses GPT-4o-mini to summarize what each agent has been doing and
|
|
5
5
|
their current halt state if not running.
|
|
6
|
+
|
|
7
|
+
Note: The summarizer now lives in the TUI, not the daemon.
|
|
8
|
+
This ensures zero API costs when the TUI is closed (no one would see the summaries anyway).
|
|
6
9
|
"""
|
|
7
10
|
|
|
8
|
-
import json
|
|
9
11
|
import logging
|
|
10
|
-
import
|
|
11
|
-
from dataclasses import dataclass, field
|
|
12
|
+
from dataclasses import dataclass
|
|
12
13
|
from datetime import datetime
|
|
13
|
-
from pathlib import Path
|
|
14
14
|
from typing import Dict, Optional, TYPE_CHECKING
|
|
15
15
|
|
|
16
16
|
from .summarizer_client import SummarizerClient
|
|
17
|
-
from .settings import get_session_dir
|
|
18
17
|
|
|
19
18
|
if TYPE_CHECKING:
|
|
20
19
|
from .interfaces import TmuxInterface
|
|
@@ -26,17 +25,23 @@ logger = logging.getLogger(__name__)
|
|
|
26
25
|
class AgentSummary:
|
|
27
26
|
"""Summary for a single agent."""
|
|
28
27
|
|
|
28
|
+
# Short summary - current activity (~50 chars)
|
|
29
29
|
text: str = ""
|
|
30
30
|
updated_at: Optional[str] = None # ISO timestamp
|
|
31
31
|
tokens_used: int = 0
|
|
32
32
|
|
|
33
|
+
# Context summary - wider context (~80 chars)
|
|
34
|
+
context: str = ""
|
|
35
|
+
context_updated_at: Optional[str] = None # ISO timestamp
|
|
36
|
+
|
|
33
37
|
|
|
34
38
|
@dataclass
|
|
35
39
|
class SummarizerConfig:
|
|
36
40
|
"""Configuration for the summarizer."""
|
|
37
41
|
|
|
38
42
|
enabled: bool = False # Off by default
|
|
39
|
-
interval: float = 5.0 # Seconds between updates per agent
|
|
43
|
+
interval: float = 5.0 # Seconds between short summary updates per agent
|
|
44
|
+
context_interval: float = 15.0 # Seconds between context summary updates (less frequent)
|
|
40
45
|
lines: int = 200 # Pane lines to capture
|
|
41
46
|
max_tokens: int = 150 # Max response tokens
|
|
42
47
|
|
|
@@ -78,8 +83,9 @@ class SummarizerComponent:
|
|
|
78
83
|
# Per-agent summaries
|
|
79
84
|
self.summaries: Dict[str, AgentSummary] = {}
|
|
80
85
|
|
|
81
|
-
# Rate limiting per session
|
|
86
|
+
# Rate limiting per session (separate for short and context)
|
|
82
87
|
self._last_update: Dict[str, datetime] = {}
|
|
88
|
+
self._last_context_update: Dict[str, datetime] = {}
|
|
83
89
|
|
|
84
90
|
# Content hashes for change detection (avoid API calls when nothing changed)
|
|
85
91
|
self._last_content_hash: Dict[str, int] = {}
|
|
@@ -88,9 +94,6 @@ class SummarizerComponent:
|
|
|
88
94
|
self.total_calls = 0
|
|
89
95
|
self.total_tokens = 0
|
|
90
96
|
|
|
91
|
-
# Control file path
|
|
92
|
-
self._control_file = get_session_dir(tmux_session) / "summarizer_control.json"
|
|
93
|
-
|
|
94
97
|
@property
|
|
95
98
|
def available(self) -> bool:
|
|
96
99
|
"""Check if summarizer is available (API key present)."""
|
|
@@ -101,36 +104,6 @@ class SummarizerComponent:
|
|
|
101
104
|
"""Check if summarizer is currently enabled."""
|
|
102
105
|
return self.config.enabled and self._client is not None
|
|
103
106
|
|
|
104
|
-
def check_control_file(self) -> None:
|
|
105
|
-
"""Check control file for enable/disable commands."""
|
|
106
|
-
if not self._control_file.exists():
|
|
107
|
-
return
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
with open(self._control_file) as f:
|
|
111
|
-
data = json.load(f)
|
|
112
|
-
|
|
113
|
-
should_enable = data.get("enabled", False)
|
|
114
|
-
|
|
115
|
-
if should_enable and not self.config.enabled:
|
|
116
|
-
# Enable requested
|
|
117
|
-
if SummarizerClient.is_available():
|
|
118
|
-
self.config.enabled = True
|
|
119
|
-
self._client = SummarizerClient()
|
|
120
|
-
logger.info("Summarizer enabled via control file")
|
|
121
|
-
else:
|
|
122
|
-
logger.warning("Summarizer enable requested but OPENAI_API_KEY not set")
|
|
123
|
-
elif not should_enable and self.config.enabled:
|
|
124
|
-
# Disable requested
|
|
125
|
-
self.config.enabled = False
|
|
126
|
-
if self._client:
|
|
127
|
-
self._client.close()
|
|
128
|
-
self._client = None
|
|
129
|
-
logger.info("Summarizer disabled via control file")
|
|
130
|
-
|
|
131
|
-
except (json.JSONDecodeError, OSError) as e:
|
|
132
|
-
logger.warning(f"Error reading summarizer control file: {e}")
|
|
133
|
-
|
|
134
107
|
def update(self, sessions) -> Dict[str, AgentSummary]:
|
|
135
108
|
"""Update summaries for all sessions.
|
|
136
109
|
|
|
@@ -140,9 +113,6 @@ class SummarizerComponent:
|
|
|
140
113
|
Returns:
|
|
141
114
|
Dict mapping session_id to AgentSummary
|
|
142
115
|
"""
|
|
143
|
-
# Check control file for enable/disable
|
|
144
|
-
self.check_control_file()
|
|
145
|
-
|
|
146
116
|
if not self.enabled:
|
|
147
117
|
return self.summaries
|
|
148
118
|
|
|
@@ -152,7 +122,11 @@ class SummarizerComponent:
|
|
|
152
122
|
return self.summaries
|
|
153
123
|
|
|
154
124
|
def _update_session(self, session) -> None:
|
|
155
|
-
"""Update
|
|
125
|
+
"""Update summaries for a single session.
|
|
126
|
+
|
|
127
|
+
Generates two types of summaries:
|
|
128
|
+
- Short: current activity (updated frequently)
|
|
129
|
+
- Context: wider context (updated less frequently)
|
|
156
130
|
|
|
157
131
|
Args:
|
|
158
132
|
session: Session object with id, tmux_window, current_status
|
|
@@ -163,12 +137,18 @@ class SummarizerComponent:
|
|
|
163
137
|
session_id = session.id
|
|
164
138
|
now = datetime.now()
|
|
165
139
|
|
|
166
|
-
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
140
|
+
# Check rate limits for each summary type
|
|
141
|
+
last_short = self._last_update.get(session_id)
|
|
142
|
+
last_context = self._last_context_update.get(session_id)
|
|
143
|
+
|
|
144
|
+
short_elapsed = (now - last_short).total_seconds() if last_short else float('inf')
|
|
145
|
+
context_elapsed = (now - last_context).total_seconds() if last_context else float('inf')
|
|
146
|
+
|
|
147
|
+
need_short = short_elapsed >= self.config.interval
|
|
148
|
+
need_context = context_elapsed >= self.config.context_interval
|
|
149
|
+
|
|
150
|
+
if not need_short and not need_context:
|
|
151
|
+
return
|
|
172
152
|
|
|
173
153
|
# Skip terminated sessions
|
|
174
154
|
current_status = getattr(session, 'stats', None)
|
|
@@ -184,47 +164,88 @@ class SummarizerComponent:
|
|
|
184
164
|
|
|
185
165
|
# Check if content has actually changed (avoid unnecessary API calls)
|
|
186
166
|
content_hash = hash(content)
|
|
167
|
+
content_changed = True
|
|
187
168
|
if session_id in self._last_content_hash:
|
|
188
169
|
if self._last_content_hash[session_id] == content_hash:
|
|
189
|
-
|
|
190
|
-
|
|
170
|
+
content_changed = False
|
|
171
|
+
|
|
172
|
+
# If content hasn't changed, skip short summary but still allow context
|
|
173
|
+
# (context changes less often so we're more lenient)
|
|
174
|
+
if not content_changed and not need_context:
|
|
175
|
+
return
|
|
191
176
|
|
|
192
177
|
self._last_content_hash[session_id] = content_hash
|
|
193
178
|
|
|
194
|
-
# Get
|
|
179
|
+
# Get or create summary object
|
|
195
180
|
prev_summary = self.summaries.get(session_id)
|
|
196
|
-
|
|
181
|
+
if not prev_summary:
|
|
182
|
+
prev_summary = AgentSummary()
|
|
183
|
+
self.summaries[session_id] = prev_summary
|
|
197
184
|
|
|
198
185
|
# Get current detected status
|
|
199
186
|
status = "unknown"
|
|
200
187
|
if current_status:
|
|
201
188
|
status = getattr(current_status, 'current_state', 'unknown')
|
|
202
189
|
|
|
203
|
-
#
|
|
190
|
+
# Update short summary if needed and content changed
|
|
191
|
+
if need_short and content_changed:
|
|
192
|
+
self._update_short_summary(session, prev_summary, content, status, now)
|
|
193
|
+
|
|
194
|
+
# Update context summary if needed (less frequent, runs even if content same)
|
|
195
|
+
if need_context:
|
|
196
|
+
self._update_context_summary(session, prev_summary, content, status, now)
|
|
197
|
+
|
|
198
|
+
def _update_short_summary(
|
|
199
|
+
self, session, summary: AgentSummary, content: str, status: str, now: datetime
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Update the short (current activity) summary."""
|
|
204
202
|
try:
|
|
205
203
|
result = self._client.summarize(
|
|
206
204
|
pane_content=content,
|
|
207
|
-
previous_summary=
|
|
205
|
+
previous_summary=summary.text,
|
|
208
206
|
current_status=status,
|
|
209
207
|
lines=self.config.lines,
|
|
210
|
-
max_tokens=
|
|
208
|
+
max_tokens=50, # Aggressive limit for terse output
|
|
209
|
+
mode="short",
|
|
211
210
|
)
|
|
212
211
|
|
|
213
212
|
self.total_calls += 1
|
|
214
213
|
|
|
215
214
|
if result and result.strip().upper() != "UNCHANGED":
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
updated_at=now.isoformat(),
|
|
220
|
-
)
|
|
221
|
-
logger.debug(f"Updated summary for {session.name}: {result[:50]}...")
|
|
222
|
-
# If "UNCHANGED", keep the old summary
|
|
215
|
+
summary.text = result.strip()
|
|
216
|
+
summary.updated_at = now.isoformat()
|
|
217
|
+
logger.debug(f"Updated short summary for {session.name}: {result[:50]}...")
|
|
223
218
|
|
|
224
|
-
self._last_update[
|
|
219
|
+
self._last_update[session.id] = now
|
|
225
220
|
|
|
226
221
|
except Exception as e:
|
|
227
|
-
logger.warning(f"
|
|
222
|
+
logger.warning(f"Short summary error for {session.name}: {e}")
|
|
223
|
+
|
|
224
|
+
def _update_context_summary(
|
|
225
|
+
self, session, summary: AgentSummary, content: str, status: str, now: datetime
|
|
226
|
+
) -> None:
|
|
227
|
+
"""Update the context (wider context) summary."""
|
|
228
|
+
try:
|
|
229
|
+
result = self._client.summarize(
|
|
230
|
+
pane_content=content,
|
|
231
|
+
previous_summary=summary.context,
|
|
232
|
+
current_status=status,
|
|
233
|
+
lines=self.config.lines,
|
|
234
|
+
max_tokens=75, # Aggressive limit for terse output
|
|
235
|
+
mode="context",
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
self.total_calls += 1
|
|
239
|
+
|
|
240
|
+
if result and result.strip().upper() != "UNCHANGED":
|
|
241
|
+
summary.context = result.strip()
|
|
242
|
+
summary.context_updated_at = now.isoformat()
|
|
243
|
+
logger.debug(f"Updated context summary for {session.name}: {result[:50]}...")
|
|
244
|
+
|
|
245
|
+
self._last_context_update[session.id] = now
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.warning(f"Context summary error for {session.name}: {e}")
|
|
228
249
|
|
|
229
250
|
def _capture_pane(self, window: int) -> Optional[str]:
|
|
230
251
|
"""Capture pane content for summarization.
|
|
@@ -269,44 +290,3 @@ class SummarizerComponent:
|
|
|
269
290
|
if self._client:
|
|
270
291
|
self._client.close()
|
|
271
292
|
self._client = None
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
def get_summarizer_control_path(session: str) -> Path:
|
|
275
|
-
"""Get the summarizer control file path for a session."""
|
|
276
|
-
return get_session_dir(session) / "summarizer_control.json"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def set_summarizer_enabled(session: str, enabled: bool) -> None:
|
|
280
|
-
"""Set summarizer enabled state via control file.
|
|
281
|
-
|
|
282
|
-
The daemon reads this file each loop and adjusts accordingly.
|
|
283
|
-
|
|
284
|
-
Args:
|
|
285
|
-
session: tmux session name
|
|
286
|
-
enabled: Whether to enable or disable
|
|
287
|
-
"""
|
|
288
|
-
control_path = get_summarizer_control_path(session)
|
|
289
|
-
control_path.parent.mkdir(parents=True, exist_ok=True)
|
|
290
|
-
with open(control_path, 'w') as f:
|
|
291
|
-
json.dump({"enabled": enabled}, f)
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
def is_summarizer_enabled(session: str) -> bool:
|
|
295
|
-
"""Check if summarizer is enabled for a session.
|
|
296
|
-
|
|
297
|
-
Args:
|
|
298
|
-
session: tmux session name
|
|
299
|
-
|
|
300
|
-
Returns:
|
|
301
|
-
True if enabled, False otherwise
|
|
302
|
-
"""
|
|
303
|
-
control_path = get_summarizer_control_path(session)
|
|
304
|
-
if not control_path.exists():
|
|
305
|
-
return False
|
|
306
|
-
|
|
307
|
-
try:
|
|
308
|
-
with open(control_path) as f:
|
|
309
|
-
data = json.load(f)
|
|
310
|
-
return data.get("enabled", False)
|
|
311
|
-
except (json.JSONDecodeError, OSError):
|
|
312
|
-
return False
|
overcode/supervisor_daemon.py
CHANGED
|
@@ -16,7 +16,7 @@ Prerequisites:
|
|
|
16
16
|
Architecture:
|
|
17
17
|
Monitor Daemon (metrics) → monitor_daemon_state.json → Supervisor Daemon (claude)
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Pure business logic is extracted to supervisor_daemon_core.py for testability.
|
|
20
20
|
TODO: Extract _send_prompt_to_window to a shared tmux utilities module
|
|
21
21
|
(duplicated in launcher.py)
|
|
22
22
|
"""
|
|
@@ -60,6 +60,13 @@ from .status_constants import (
|
|
|
60
60
|
)
|
|
61
61
|
from .tmux_manager import TmuxManager
|
|
62
62
|
from .history_reader import encode_project_path, read_token_usage_from_session_file
|
|
63
|
+
from .supervisor_daemon_core import (
|
|
64
|
+
build_daemon_claude_context as _build_daemon_claude_context,
|
|
65
|
+
filter_non_green_sessions,
|
|
66
|
+
calculate_daemon_claude_run_seconds,
|
|
67
|
+
should_launch_daemon_claude,
|
|
68
|
+
parse_intervention_log_line,
|
|
69
|
+
)
|
|
63
70
|
|
|
64
71
|
|
|
65
72
|
@dataclass
|
|
@@ -473,14 +480,14 @@ class SupervisorDaemon:
|
|
|
473
480
|
def _mark_daemon_claude_stopped(self) -> None:
|
|
474
481
|
"""Mark daemon claude as stopped and accumulate run time."""
|
|
475
482
|
if self.supervisor_stats.supervisor_claude_running:
|
|
476
|
-
# Calculate run
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
self.supervisor_stats.supervisor_claude_total_run_seconds
|
|
482
|
-
|
|
483
|
-
|
|
483
|
+
# Calculate total run time using pure function
|
|
484
|
+
self.supervisor_stats.supervisor_claude_total_run_seconds = (
|
|
485
|
+
calculate_daemon_claude_run_seconds(
|
|
486
|
+
started_at_iso=self.supervisor_stats.supervisor_claude_started_at,
|
|
487
|
+
now_iso=datetime.now().isoformat(),
|
|
488
|
+
previous_total=self.supervisor_stats.supervisor_claude_total_run_seconds,
|
|
489
|
+
)
|
|
490
|
+
)
|
|
484
491
|
|
|
485
492
|
self.supervisor_stats.supervisor_claude_running = False
|
|
486
493
|
self.supervisor_stats.supervisor_claude_started_at = None
|
|
@@ -555,31 +562,18 @@ class SupervisorDaemon:
|
|
|
555
562
|
non_green_sessions: List[SessionDaemonState]
|
|
556
563
|
) -> str:
|
|
557
564
|
"""Build initial context for daemon claude."""
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
if session.standing_instructions:
|
|
571
|
-
context_parts.append(f" Autopilot: {session.standing_instructions}")
|
|
572
|
-
else:
|
|
573
|
-
context_parts.append(f" No autopilot instructions set")
|
|
574
|
-
if session.repo_name:
|
|
575
|
-
context_parts.append(f" Repo: {session.repo_name}")
|
|
576
|
-
context_parts.append("")
|
|
577
|
-
|
|
578
|
-
context_parts.append("Read the daemon claude skill for how to control sessions via tmux.")
|
|
579
|
-
context_parts.append("Start by reading ~/.overcode/sessions/sessions.json to see full state.")
|
|
580
|
-
context_parts.append("Then check each non-green session and help them make progress.")
|
|
581
|
-
|
|
582
|
-
return "\n".join(context_parts)
|
|
565
|
+
# Convert dataclass objects to dicts for pure function
|
|
566
|
+
session_dicts = [
|
|
567
|
+
{
|
|
568
|
+
"name": s.name,
|
|
569
|
+
"tmux_window": s.tmux_window,
|
|
570
|
+
"current_status": s.current_status,
|
|
571
|
+
"standing_instructions": s.standing_instructions,
|
|
572
|
+
"repo_name": s.repo_name,
|
|
573
|
+
}
|
|
574
|
+
for s in non_green_sessions
|
|
575
|
+
]
|
|
576
|
+
return _build_daemon_claude_context(self.tmux_session, session_dicts)
|
|
583
577
|
|
|
584
578
|
def _send_prompt_to_window(self, window_index: int, prompt: str) -> bool:
|
|
585
579
|
"""Send a large prompt to a tmux window via load-buffer/paste-buffer."""
|
|
@@ -678,12 +672,35 @@ class SupervisorDaemon:
|
|
|
678
672
|
self,
|
|
679
673
|
monitor_state: MonitorDaemonState
|
|
680
674
|
) -> List[SessionDaemonState]:
|
|
681
|
-
"""Get sessions that are not in running state from monitor daemon state.
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
675
|
+
"""Get sessions that are not in running state from monitor daemon state.
|
|
676
|
+
|
|
677
|
+
Filters out:
|
|
678
|
+
- Running (green) sessions
|
|
679
|
+
- The daemon_claude session itself
|
|
680
|
+
- Asleep sessions (#70)
|
|
681
|
+
- Sessions with DO_NOTHING standing orders (#70)
|
|
682
|
+
"""
|
|
683
|
+
# Convert to dicts for pure function
|
|
684
|
+
session_dicts = [
|
|
685
|
+
{
|
|
686
|
+
"name": s.name,
|
|
687
|
+
"current_status": s.current_status,
|
|
688
|
+
"is_asleep": s.is_asleep,
|
|
689
|
+
"standing_instructions": s.standing_instructions,
|
|
690
|
+
"_session": s, # Keep reference to original
|
|
691
|
+
}
|
|
692
|
+
for s in monitor_state.sessions
|
|
685
693
|
]
|
|
686
694
|
|
|
695
|
+
# Filter using pure function
|
|
696
|
+
filtered = filter_non_green_sessions(
|
|
697
|
+
session_dicts,
|
|
698
|
+
exclude_names=["daemon_claude"],
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
# Return original SessionDaemonState objects
|
|
702
|
+
return [d["_session"] for d in filtered]
|
|
703
|
+
|
|
687
704
|
def wait_for_monitor_daemon(self, timeout: int = 30, poll_interval: int = 2) -> bool:
|
|
688
705
|
"""Wait for monitor daemon to be running.
|
|
689
706
|
|