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/__init__.py
CHANGED
overcode/cli.py
CHANGED
|
@@ -39,6 +39,15 @@ supervisor_daemon_app = typer.Typer(
|
|
|
39
39
|
)
|
|
40
40
|
app.add_typer(supervisor_daemon_app, name="supervisor-daemon")
|
|
41
41
|
|
|
42
|
+
# Config subcommand group
|
|
43
|
+
config_app = typer.Typer(
|
|
44
|
+
name="config",
|
|
45
|
+
help="Manage configuration",
|
|
46
|
+
no_args_is_help=False,
|
|
47
|
+
invoke_without_command=True,
|
|
48
|
+
)
|
|
49
|
+
app.add_typer(config_app, name="config")
|
|
50
|
+
|
|
42
51
|
# Console for rich output
|
|
43
52
|
console = Console()
|
|
44
53
|
|
|
@@ -194,6 +203,33 @@ def cleanup(session: SessionOption = "agents"):
|
|
|
194
203
|
rprint("[dim]No terminated sessions to clean up[/dim]")
|
|
195
204
|
|
|
196
205
|
|
|
206
|
+
@app.command(name="set-value")
|
|
207
|
+
def set_value(
|
|
208
|
+
name: Annotated[str, typer.Argument(help="Name of agent")],
|
|
209
|
+
value: Annotated[int, typer.Argument(help="Priority value (default 1000, higher = more important)")],
|
|
210
|
+
session: SessionOption = "agents",
|
|
211
|
+
):
|
|
212
|
+
"""Set agent priority value for sorting (#61).
|
|
213
|
+
|
|
214
|
+
Higher values indicate higher priority. Default is 1000.
|
|
215
|
+
|
|
216
|
+
Examples:
|
|
217
|
+
overcode set-value my-agent 2000 # High priority
|
|
218
|
+
overcode set-value my-agent 500 # Low priority
|
|
219
|
+
overcode set-value my-agent 1000 # Reset to default
|
|
220
|
+
"""
|
|
221
|
+
from .session_manager import SessionManager
|
|
222
|
+
|
|
223
|
+
manager = SessionManager()
|
|
224
|
+
agent = manager.get_session_by_name(name)
|
|
225
|
+
if not agent:
|
|
226
|
+
rprint(f"[red]Error: Agent '{name}' not found[/red]")
|
|
227
|
+
raise typer.Exit(code=1)
|
|
228
|
+
|
|
229
|
+
manager.set_agent_value(agent.id, value)
|
|
230
|
+
rprint(f"[green]✓ Set {name} value to {value}[/green]")
|
|
231
|
+
|
|
232
|
+
|
|
197
233
|
@app.command()
|
|
198
234
|
def send(
|
|
199
235
|
name: Annotated[str, typer.Argument(help="Name of agent")],
|
|
@@ -771,70 +807,132 @@ def supervisor_daemon_watch(session: SessionOption = "agents"):
|
|
|
771
807
|
|
|
772
808
|
|
|
773
809
|
# =============================================================================
|
|
774
|
-
#
|
|
810
|
+
# Config Commands
|
|
775
811
|
# =============================================================================
|
|
776
812
|
|
|
813
|
+
CONFIG_TEMPLATE = """\
|
|
814
|
+
# Overcode configuration
|
|
815
|
+
# Location: ~/.overcode/config.yaml
|
|
816
|
+
|
|
817
|
+
# Default instructions sent to new agents
|
|
818
|
+
# default_standing_instructions: "Be concise. Ask before making large changes."
|
|
819
|
+
|
|
820
|
+
# AI summarizer settings (for corporate API gateways)
|
|
821
|
+
# summarizer:
|
|
822
|
+
# api_url: https://api.openai.com/v1/chat/completions
|
|
823
|
+
# model: gpt-4o-mini
|
|
824
|
+
# api_key_var: OPENAI_API_KEY # env var containing the API key
|
|
825
|
+
|
|
826
|
+
# Cloud relay for remote monitoring
|
|
827
|
+
# relay:
|
|
828
|
+
# enabled: false
|
|
829
|
+
# url: https://your-worker.workers.dev/update
|
|
830
|
+
# api_key: your-secret-key
|
|
831
|
+
# interval: 30 # seconds between pushes
|
|
832
|
+
|
|
833
|
+
# Web dashboard time presets
|
|
834
|
+
# web:
|
|
835
|
+
# time_presets:
|
|
836
|
+
# - name: "Morning"
|
|
837
|
+
# start: "09:00"
|
|
838
|
+
# end: "12:00"
|
|
839
|
+
# - name: "Full Day"
|
|
840
|
+
# start: "09:00"
|
|
841
|
+
# end: "17:00"
|
|
842
|
+
"""
|
|
777
843
|
|
|
778
|
-
@app.command()
|
|
779
|
-
def summarizer(
|
|
780
|
-
action: Annotated[
|
|
781
|
-
str, typer.Argument(help="Action: on, off, or status")
|
|
782
|
-
] = "status",
|
|
783
|
-
session: SessionOption = "agents",
|
|
784
|
-
):
|
|
785
|
-
"""Control the agent activity summarizer.
|
|
786
844
|
|
|
787
|
-
|
|
788
|
-
|
|
845
|
+
@config_app.callback(invoke_without_command=True)
|
|
846
|
+
def config_default(ctx: typer.Context):
|
|
847
|
+
"""Show current configuration (default when no subcommand given)."""
|
|
848
|
+
if ctx.invoked_subcommand is None:
|
|
849
|
+
_config_show()
|
|
789
850
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
851
|
+
|
|
852
|
+
@config_app.command("init")
|
|
853
|
+
def config_init(
|
|
854
|
+
force: Annotated[
|
|
855
|
+
bool, typer.Option("--force", "-f", help="Overwrite existing config file")
|
|
856
|
+
] = False,
|
|
857
|
+
):
|
|
858
|
+
"""Create a config file with documented defaults.
|
|
859
|
+
|
|
860
|
+
Creates ~/.overcode/config.yaml with all options commented out.
|
|
861
|
+
Use --force to overwrite an existing config file.
|
|
794
862
|
"""
|
|
795
|
-
from .
|
|
796
|
-
set_summarizer_enabled,
|
|
797
|
-
is_summarizer_enabled,
|
|
798
|
-
SummarizerClient,
|
|
799
|
-
)
|
|
800
|
-
from .monitor_daemon_state import get_monitor_daemon_state
|
|
863
|
+
from .config import CONFIG_PATH
|
|
801
864
|
|
|
802
|
-
|
|
865
|
+
# Ensure directory exists
|
|
866
|
+
CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
803
867
|
|
|
804
|
-
if
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
868
|
+
if CONFIG_PATH.exists() and not force:
|
|
869
|
+
rprint(f"[yellow]Config file already exists:[/yellow] {CONFIG_PATH}")
|
|
870
|
+
rprint("[dim]Use --force to overwrite[/dim]")
|
|
871
|
+
raise typer.Exit(1)
|
|
808
872
|
|
|
809
|
-
|
|
810
|
-
|
|
873
|
+
CONFIG_PATH.write_text(CONFIG_TEMPLATE)
|
|
874
|
+
rprint(f"[green]✓[/green] Created config file: [bold]{CONFIG_PATH}[/bold]")
|
|
875
|
+
rprint("[dim]Edit to customize your settings[/dim]")
|
|
811
876
|
|
|
812
|
-
rprint(f"[bold]Summarizer Status ({session}):[/bold]")
|
|
813
|
-
rprint(f" API key: {'[green]available[/green]' if api_available else '[red]not set[/red] (export OPENAI_API_KEY=...)'}")
|
|
814
|
-
rprint(f" Enabled: {'[green]yes[/green]' if enabled else '[dim]no[/dim]'}")
|
|
815
877
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
878
|
+
@config_app.command("show")
|
|
879
|
+
def config_show():
|
|
880
|
+
"""Show current configuration."""
|
|
881
|
+
_config_show()
|
|
819
882
|
|
|
820
|
-
elif action == "on":
|
|
821
|
-
if not SummarizerClient.is_available():
|
|
822
|
-
rprint("[red]Error:[/red] OPENAI_API_KEY environment variable not set")
|
|
823
|
-
rprint("[dim]Export your API key: export OPENAI_API_KEY='sk-...'[/dim]")
|
|
824
|
-
raise typer.Exit(1)
|
|
825
883
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
884
|
+
def _config_show():
|
|
885
|
+
"""Internal function to display current config."""
|
|
886
|
+
from .config import CONFIG_PATH, load_config
|
|
829
887
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
rprint(
|
|
888
|
+
if not CONFIG_PATH.exists():
|
|
889
|
+
rprint(f"[dim]No config file found at {CONFIG_PATH}[/dim]")
|
|
890
|
+
rprint("[dim]Run 'overcode config init' to create one[/dim]")
|
|
891
|
+
return
|
|
833
892
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
rprint("[dim]
|
|
837
|
-
|
|
893
|
+
config = load_config()
|
|
894
|
+
if not config:
|
|
895
|
+
rprint(f"[dim]Config file is empty: {CONFIG_PATH}[/dim]")
|
|
896
|
+
return
|
|
897
|
+
|
|
898
|
+
rprint(f"[bold]Configuration[/bold] ({CONFIG_PATH}):\n")
|
|
899
|
+
|
|
900
|
+
# Show each configured section
|
|
901
|
+
if "default_standing_instructions" in config:
|
|
902
|
+
instr = config["default_standing_instructions"]
|
|
903
|
+
display = instr[:60] + "..." if len(instr) > 60 else instr
|
|
904
|
+
rprint(f" default_standing_instructions: \"{display}\"")
|
|
905
|
+
|
|
906
|
+
if "summarizer" in config:
|
|
907
|
+
s = config["summarizer"]
|
|
908
|
+
rprint(" summarizer:")
|
|
909
|
+
if "api_url" in s:
|
|
910
|
+
rprint(f" api_url: {s['api_url']}")
|
|
911
|
+
if "model" in s:
|
|
912
|
+
rprint(f" model: {s['model']}")
|
|
913
|
+
if "api_key_var" in s:
|
|
914
|
+
rprint(f" api_key_var: {s['api_key_var']}")
|
|
915
|
+
|
|
916
|
+
if "relay" in config:
|
|
917
|
+
r = config["relay"]
|
|
918
|
+
rprint(" relay:")
|
|
919
|
+
rprint(f" enabled: {r.get('enabled', False)}")
|
|
920
|
+
if "url" in r:
|
|
921
|
+
rprint(f" url: {r['url']}")
|
|
922
|
+
if "interval" in r:
|
|
923
|
+
rprint(f" interval: {r['interval']}s")
|
|
924
|
+
|
|
925
|
+
if "web" in config:
|
|
926
|
+
w = config["web"]
|
|
927
|
+
if "time_presets" in w:
|
|
928
|
+
rprint(f" web.time_presets: {len(w['time_presets'])} presets")
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
@config_app.command("path")
|
|
932
|
+
def config_path():
|
|
933
|
+
"""Show the config file path."""
|
|
934
|
+
from .config import CONFIG_PATH
|
|
935
|
+
print(CONFIG_PATH)
|
|
838
936
|
|
|
839
937
|
|
|
840
938
|
# =============================================================================
|
overcode/config.py
CHANGED
|
@@ -72,6 +72,72 @@ def get_relay_config() -> Optional[dict]:
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
def get_summarizer_config() -> dict:
|
|
76
|
+
"""Get summarizer configuration for AI summaries.
|
|
77
|
+
|
|
78
|
+
Config file takes precedence, environment variables are fallback.
|
|
79
|
+
|
|
80
|
+
Config format in ~/.overcode/config.yaml:
|
|
81
|
+
summarizer:
|
|
82
|
+
api_url: https://api.openai.com/v1/chat/completions
|
|
83
|
+
model: gpt-4o-mini
|
|
84
|
+
api_key_var: OPENAI_API_KEY # env var name containing the key
|
|
85
|
+
|
|
86
|
+
Environment variable fallbacks:
|
|
87
|
+
OVERCODE_SUMMARIZER_API_URL
|
|
88
|
+
OVERCODE_SUMMARIZER_MODEL
|
|
89
|
+
OVERCODE_SUMMARIZER_API_KEY_VAR
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Dict with api_url, model, and api_key (resolved from env var)
|
|
93
|
+
"""
|
|
94
|
+
import os
|
|
95
|
+
|
|
96
|
+
# Defaults
|
|
97
|
+
default_api_url = "https://api.openai.com/v1/chat/completions"
|
|
98
|
+
default_model = "gpt-4o-mini"
|
|
99
|
+
default_api_key_var = "OPENAI_API_KEY"
|
|
100
|
+
|
|
101
|
+
config = load_config()
|
|
102
|
+
summarizer = config.get("summarizer", {})
|
|
103
|
+
|
|
104
|
+
# Config file takes precedence, env vars are fallback
|
|
105
|
+
api_url = summarizer.get("api_url") or os.environ.get("OVERCODE_SUMMARIZER_API_URL") or default_api_url
|
|
106
|
+
model = summarizer.get("model") or os.environ.get("OVERCODE_SUMMARIZER_MODEL") or default_model
|
|
107
|
+
api_key_var = summarizer.get("api_key_var") or os.environ.get("OVERCODE_SUMMARIZER_API_KEY_VAR") or default_api_key_var
|
|
108
|
+
|
|
109
|
+
# Resolve the actual API key from the configured env var
|
|
110
|
+
api_key = os.environ.get(api_key_var)
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
"api_url": api_url,
|
|
114
|
+
"model": model,
|
|
115
|
+
"api_key": api_key,
|
|
116
|
+
"api_key_var": api_key_var,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_timeline_config() -> dict:
|
|
121
|
+
"""Get timeline display configuration.
|
|
122
|
+
|
|
123
|
+
Config format in ~/.overcode/config.yaml:
|
|
124
|
+
timeline:
|
|
125
|
+
hours: 3.0 # How many hours of history to show
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dict with timeline settings (hours)
|
|
129
|
+
"""
|
|
130
|
+
default_hours = 3.0
|
|
131
|
+
|
|
132
|
+
config = load_config()
|
|
133
|
+
timeline = config.get("timeline", {})
|
|
134
|
+
hours = timeline.get("hours", default_hours)
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"hours": hours,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
75
141
|
def get_web_time_presets() -> list:
|
|
76
142
|
"""Get time presets for the web analytics dashboard.
|
|
77
143
|
|
overcode/daemon_claude_skill.md
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
# Overcode Supervisor Skill
|
|
2
2
|
|
|
3
|
-
You are the Overcode supervisor agent. Your mission: **
|
|
3
|
+
You are the Overcode supervisor agent. Your mission: **Attempt to unblock each RED session once, then exit**.
|
|
4
4
|
|
|
5
5
|
## Your Role
|
|
6
6
|
|
|
7
|
-
You
|
|
7
|
+
You unblock Claude agent sessions running in tmux. When sessions are stuck (RED status), you make ONE attempt to help each by:
|
|
8
8
|
- Reading their output to understand what they're stuck on
|
|
9
|
-
- Making decisions based on their
|
|
9
|
+
- Making decisions based on their standing instructions
|
|
10
10
|
- Approving safe permission requests
|
|
11
11
|
- Sending guidance or clarifying information
|
|
12
|
-
- Having multi-turn conversations with agents
|
|
13
12
|
|
|
14
|
-
**
|
|
13
|
+
**IMPORTANT: Make ONE attempt per RED session, then exit. Do not loop or wait to see if your action worked. The supervisor daemon will call you again later if sessions are still RED.**
|
|
15
14
|
|
|
16
15
|
## How to Control Sessions (Recommended)
|
|
17
16
|
|
|
@@ -115,35 +114,32 @@ You must follow these rules when deciding to approve operations:
|
|
|
115
114
|
# 1. Read current session states
|
|
116
115
|
cat ~/.overcode/sessions/sessions.json | jq '.[] | {name, tmux_window, standing_instructions, stats}'
|
|
117
116
|
|
|
118
|
-
# 2. Find RED sessions
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
# 3. For each RED session, read output
|
|
122
|
-
tmux capture-pane -t agents:1 -p -S -100
|
|
123
|
-
|
|
124
|
-
# 4. Make decision based on:
|
|
125
|
-
# - What they're stuck on
|
|
126
|
-
# - Their autopilot instruction
|
|
127
|
-
# - Approval rules
|
|
117
|
+
# 2. Find RED sessions (use overcode list)
|
|
118
|
+
overcode list
|
|
128
119
|
|
|
129
|
-
#
|
|
130
|
-
tmux send-keys -t agents:1 "" C-m
|
|
120
|
+
# 3. For EACH RED session, make ONE attempt:
|
|
131
121
|
|
|
132
|
-
#
|
|
133
|
-
|
|
122
|
+
# a. Read output to understand what they're stuck on
|
|
123
|
+
overcode show agent-name --lines 100
|
|
134
124
|
|
|
135
|
-
#
|
|
136
|
-
|
|
125
|
+
# b. Make decision based on:
|
|
126
|
+
# - What they're stuck on
|
|
127
|
+
# - Their standing instructions
|
|
128
|
+
# - Approval rules below
|
|
137
129
|
|
|
138
|
-
#
|
|
139
|
-
|
|
130
|
+
# c. Take action:
|
|
131
|
+
overcode send agent-name enter # Approve permission
|
|
132
|
+
overcode send agent-name escape # Reject permission
|
|
133
|
+
overcode send agent-name "guidance" # Send instructions
|
|
140
134
|
|
|
141
|
-
#
|
|
135
|
+
# d. Move to next RED session immediately (don't wait)
|
|
142
136
|
|
|
143
|
-
#
|
|
137
|
+
# 4. After attempting ALL RED sessions once, EXIT
|
|
144
138
|
exit 0
|
|
145
139
|
```
|
|
146
140
|
|
|
141
|
+
**Key point:** Do NOT loop back to check if sessions turned green. Make one attempt per session and exit. The supervisor daemon will invoke you again if needed.
|
|
142
|
+
|
|
147
143
|
## Real Example
|
|
148
144
|
|
|
149
145
|
**Session:** recipe-book
|
|
@@ -168,13 +164,20 @@ echo "$(date): recipe-book - Approved write to desserts.md (within working dir,
|
|
|
168
164
|
|
|
169
165
|
## Your Process
|
|
170
166
|
|
|
171
|
-
1. **Survey** -
|
|
172
|
-
2. **Identify** -
|
|
173
|
-
3. **
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
167
|
+
1. **Survey** - Run `overcode list` to see all sessions and their status
|
|
168
|
+
2. **Identify** - Note which sessions are RED (waiting for user)
|
|
169
|
+
3. **For each RED session:**
|
|
170
|
+
- **Investigate** - Run `overcode show <name>` to see what they're stuck on
|
|
171
|
+
- **Decide** - Apply approval rules and check their standing instructions
|
|
172
|
+
- **Act** - Send ONE command to unblock them
|
|
173
|
+
- **Move on** - Immediately proceed to next RED session
|
|
174
|
+
4. **Exit** - After attempting each RED session once, run `exit 0`
|
|
175
|
+
|
|
176
|
+
**Do NOT:**
|
|
177
|
+
- Loop back to check if sessions turned green
|
|
178
|
+
- Wait to see if your action worked
|
|
179
|
+
- Make multiple attempts on the same session
|
|
180
|
+
|
|
181
|
+
The supervisor daemon runs continuously and will invoke you again if sessions are still RED.
|
|
179
182
|
|
|
180
183
|
Remember: You're a decision-making agent that helps other agents make progress. Be helpful but safe. When in doubt, err on the side of caution.
|
overcode/history_reader.py
CHANGED
|
@@ -202,6 +202,44 @@ def get_session_ids_for_session(
|
|
|
202
202
|
return sorted(session_ids)
|
|
203
203
|
|
|
204
204
|
|
|
205
|
+
def get_current_session_id_for_directory(
|
|
206
|
+
directory: str,
|
|
207
|
+
since: datetime,
|
|
208
|
+
history_path: Path = CLAUDE_HISTORY_PATH
|
|
209
|
+
) -> Optional[str]:
|
|
210
|
+
"""Get the most recent Claude sessionId for a directory since a given time.
|
|
211
|
+
|
|
212
|
+
This is used to discover new Claude sessionIds that should be tracked
|
|
213
|
+
by an overcode agent running in that directory (#119).
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
directory: The project directory path
|
|
217
|
+
since: Only consider entries after this time
|
|
218
|
+
history_path: Path to history file
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
The most recent sessionId, or None if no matching entries
|
|
222
|
+
"""
|
|
223
|
+
entries = read_history(history_path)
|
|
224
|
+
session_dir = str(Path(directory).resolve())
|
|
225
|
+
since_ms = int(since.timestamp() * 1000)
|
|
226
|
+
|
|
227
|
+
latest_session_id = None
|
|
228
|
+
latest_timestamp = 0
|
|
229
|
+
|
|
230
|
+
for entry in entries:
|
|
231
|
+
if entry.timestamp_ms < since_ms:
|
|
232
|
+
continue
|
|
233
|
+
if entry.project:
|
|
234
|
+
entry_dir = str(Path(entry.project).resolve())
|
|
235
|
+
if entry_dir == session_dir and entry.session_id:
|
|
236
|
+
if entry.timestamp_ms > latest_timestamp:
|
|
237
|
+
latest_timestamp = entry.timestamp_ms
|
|
238
|
+
latest_session_id = entry.session_id
|
|
239
|
+
|
|
240
|
+
return latest_session_id
|
|
241
|
+
|
|
242
|
+
|
|
205
243
|
def encode_project_path(path: str) -> str:
|
|
206
244
|
"""Encode a project path to Claude Code's directory naming format.
|
|
207
245
|
|
|
@@ -397,6 +435,10 @@ def get_session_stats(
|
|
|
397
435
|
|
|
398
436
|
Combines interaction counting with token usage from session files.
|
|
399
437
|
|
|
438
|
+
For context window calculation, only owned sessionIds are used to avoid
|
|
439
|
+
cross-contamination when multiple agents run in the same directory (#119).
|
|
440
|
+
Total token counting still uses all matched sessionIds.
|
|
441
|
+
|
|
400
442
|
Args:
|
|
401
443
|
session: The overcode Session
|
|
402
444
|
history_path: Path to history.jsonl
|
|
@@ -418,21 +460,25 @@ def get_session_stats(
|
|
|
418
460
|
interactions = get_interactions_for_session(session, history_path)
|
|
419
461
|
interaction_count = len(interactions)
|
|
420
462
|
|
|
421
|
-
# Get unique session IDs
|
|
422
|
-
|
|
463
|
+
# Get unique session IDs from interactions (for total token counting)
|
|
464
|
+
all_session_ids = set()
|
|
423
465
|
for entry in interactions:
|
|
424
466
|
if entry.session_id:
|
|
425
|
-
|
|
467
|
+
all_session_ids.add(entry.session_id)
|
|
468
|
+
|
|
469
|
+
# Get owned sessionIds for context calculation (#119)
|
|
470
|
+
# Only use explicitly tracked sessionIds to avoid showing wrong agent's context
|
|
471
|
+
owned_session_ids = getattr(session, 'claude_session_ids', None) or []
|
|
426
472
|
|
|
427
473
|
# Sum token usage and work times across all session files
|
|
428
474
|
total_input = 0
|
|
429
475
|
total_output = 0
|
|
430
476
|
total_cache_creation = 0
|
|
431
477
|
total_cache_read = 0
|
|
432
|
-
current_context = 0 # Track most recent context size
|
|
478
|
+
current_context = 0 # Track most recent context size (only from owned sessions)
|
|
433
479
|
all_work_times: List[float] = []
|
|
434
480
|
|
|
435
|
-
for sid in
|
|
481
|
+
for sid in all_session_ids:
|
|
436
482
|
session_file = get_session_file_path(
|
|
437
483
|
session.start_directory, sid, projects_path
|
|
438
484
|
)
|
|
@@ -441,14 +487,29 @@ def get_session_stats(
|
|
|
441
487
|
total_output += usage["output_tokens"]
|
|
442
488
|
total_cache_creation += usage["cache_creation_tokens"]
|
|
443
489
|
total_cache_read += usage["cache_read_tokens"]
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
490
|
+
|
|
491
|
+
# Only track context from OWNED sessionIds to avoid cross-contamination (#119)
|
|
492
|
+
if sid in owned_session_ids:
|
|
493
|
+
if usage["current_context_tokens"] > current_context:
|
|
494
|
+
current_context = usage["current_context_tokens"]
|
|
447
495
|
|
|
448
496
|
# Collect work times from this session file
|
|
449
497
|
work_times = read_work_times_from_session_file(session_file, since=session_start)
|
|
450
498
|
all_work_times.extend(work_times)
|
|
451
499
|
|
|
500
|
+
# Check for subagent files in {sessionId}/subagents/
|
|
501
|
+
encoded = encode_project_path(session.start_directory)
|
|
502
|
+
subagents_dir = projects_path / encoded / sid / "subagents"
|
|
503
|
+
if subagents_dir.exists():
|
|
504
|
+
for subagent_file in subagents_dir.glob("agent-*.jsonl"):
|
|
505
|
+
sub_usage = read_token_usage_from_session_file(
|
|
506
|
+
subagent_file, since=session_start
|
|
507
|
+
)
|
|
508
|
+
total_input += sub_usage["input_tokens"]
|
|
509
|
+
total_output += sub_usage["output_tokens"]
|
|
510
|
+
total_cache_creation += sub_usage["cache_creation_tokens"]
|
|
511
|
+
total_cache_read += sub_usage["cache_read_tokens"]
|
|
512
|
+
|
|
452
513
|
return ClaudeSessionStats(
|
|
453
514
|
interaction_count=interaction_count,
|
|
454
515
|
input_tokens=total_input,
|