overcode 0.1.2__py3-none-any.whl → 0.1.3__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 +147 -49
- overcode/config.py +66 -0
- overcode/daemon_claude_skill.md +36 -33
- overcode/history_reader.py +69 -8
- overcode/implementations.py +109 -84
- overcode/monitor_daemon.py +33 -38
- overcode/monitor_daemon_state.py +17 -15
- overcode/pid_utils.py +17 -3
- overcode/session_manager.py +53 -0
- overcode/settings.py +12 -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 +21 -5
- overcode/tmux_manager.py +101 -91
- overcode/tui.py +829 -133
- overcode/tui_helpers.py +4 -3
- {overcode-0.1.2.dist-info → overcode-0.1.3.dist-info}/METADATA +2 -1
- overcode-0.1.3.dist-info/RECORD +45 -0
- {overcode-0.1.2.dist-info → overcode-0.1.3.dist-info}/WHEEL +1 -1
- overcode-0.1.2.dist-info/RECORD +0 -45
- {overcode-0.1.2.dist-info → overcode-0.1.3.dist-info}/entry_points.txt +0 -0
- {overcode-0.1.2.dist-info → overcode-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {overcode-0.1.2.dist-info → overcode-0.1.3.dist-info}/top_level.txt +0 -0
overcode/status_detector.py
CHANGED
|
@@ -19,6 +19,7 @@ from .status_patterns import (
|
|
|
19
19
|
is_command_menu_line,
|
|
20
20
|
count_command_menu_lines,
|
|
21
21
|
clean_line,
|
|
22
|
+
strip_ansi,
|
|
22
23
|
StatusPatterns,
|
|
23
24
|
)
|
|
24
25
|
|
|
@@ -94,12 +95,17 @@ class StatusDetector:
|
|
|
94
95
|
if not content:
|
|
95
96
|
return self.STATUS_WAITING_USER, "Unable to read pane", ""
|
|
96
97
|
|
|
98
|
+
# Strip ANSI escape sequences for pattern matching
|
|
99
|
+
# The raw content (with colors) is returned for display, but pattern
|
|
100
|
+
# matching needs plain text since escape codes break string matching
|
|
101
|
+
clean_content = strip_ansi(content)
|
|
102
|
+
|
|
97
103
|
# Content change detection - if content is changing, Claude is actively working
|
|
98
104
|
# Key by session.id, not window index, to avoid stale hashes when windows are recycled
|
|
99
105
|
# IMPORTANT: Filter out status bar lines before hashing to avoid false positives
|
|
100
106
|
# from dynamic status bar elements (token counts, elapsed time) that update when idle
|
|
101
107
|
session_id = session.id
|
|
102
|
-
content_for_hash = self._filter_status_bar_for_hash(
|
|
108
|
+
content_for_hash = self._filter_status_bar_for_hash(clean_content)
|
|
103
109
|
content_hash = hash(content_for_hash)
|
|
104
110
|
content_changed = False
|
|
105
111
|
if session_id in self._previous_content:
|
|
@@ -107,7 +113,7 @@ class StatusDetector:
|
|
|
107
113
|
self._previous_content[session_id] = content_hash
|
|
108
114
|
self._content_changed[session_id] = content_changed
|
|
109
115
|
|
|
110
|
-
lines =
|
|
116
|
+
lines = clean_content.strip().split('\n')
|
|
111
117
|
# Get more lines for better context (menu prompts can be 5+ lines)
|
|
112
118
|
last_lines = [l.strip() for l in lines[-10:] if l.strip()]
|
|
113
119
|
|
overcode/status_patterns.py
CHANGED
|
@@ -10,9 +10,28 @@ Claude's current state. Centralizing these makes them:
|
|
|
10
10
|
Each pattern set includes documentation about when it's used and what it matches.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
|
+
import re
|
|
13
14
|
from dataclasses import dataclass, field
|
|
14
15
|
from typing import List
|
|
15
16
|
|
|
17
|
+
# Regex to match ANSI escape sequences (colors, cursor movement, etc.)
|
|
18
|
+
ANSI_ESCAPE_PATTERN = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]')
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def strip_ansi(text: str) -> str:
|
|
22
|
+
"""Remove ANSI escape sequences from text.
|
|
23
|
+
|
|
24
|
+
This is needed because tmux capture_pane with escape_sequences=True
|
|
25
|
+
preserves color codes, but pattern matching needs plain text.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
text: Text potentially containing ANSI escape sequences
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Text with all ANSI escape sequences removed
|
|
32
|
+
"""
|
|
33
|
+
return ANSI_ESCAPE_PATTERN.sub('', text)
|
|
34
|
+
|
|
16
35
|
|
|
17
36
|
@dataclass
|
|
18
37
|
class StatusPatterns:
|
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
|
@@ -678,11 +678,27 @@ class SupervisorDaemon:
|
|
|
678
678
|
self,
|
|
679
679
|
monitor_state: MonitorDaemonState
|
|
680
680
|
) -> List[SessionDaemonState]:
|
|
681
|
-
"""Get sessions that are not in running state from monitor daemon state.
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
681
|
+
"""Get sessions that are not in running state from monitor daemon state.
|
|
682
|
+
|
|
683
|
+
Filters out:
|
|
684
|
+
- Running (green) sessions
|
|
685
|
+
- The daemon_claude session itself
|
|
686
|
+
- Asleep sessions (#70)
|
|
687
|
+
- Sessions with DO_NOTHING standing orders (#70)
|
|
688
|
+
"""
|
|
689
|
+
result = []
|
|
690
|
+
for s in monitor_state.sessions:
|
|
691
|
+
# Skip green sessions and daemon_claude
|
|
692
|
+
if s.current_status == STATUS_RUNNING or s.name == 'daemon_claude':
|
|
693
|
+
continue
|
|
694
|
+
# Skip asleep sessions
|
|
695
|
+
if s.is_asleep:
|
|
696
|
+
continue
|
|
697
|
+
# Skip sessions with DO_NOTHING standing orders
|
|
698
|
+
if s.standing_instructions and 'DO_NOTHING' in s.standing_instructions.upper():
|
|
699
|
+
continue
|
|
700
|
+
result.append(s)
|
|
701
|
+
return result
|
|
686
702
|
|
|
687
703
|
def wait_for_monitor_daemon(self, timeout: int = 30, poll_interval: int = 2) -> bool:
|
|
688
704
|
"""Wait for monitor daemon to be running.
|