hte-cli 0.2.31__tar.gz → 0.2.32__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.
- {hte_cli-0.2.31 → hte_cli-0.2.32}/PKG-INFO +1 -1
- {hte_cli-0.2.31 → hte_cli-0.2.32}/pyproject.toml +1 -1
- {hte_cli-0.2.31 → hte_cli-0.2.32}/src/hte_cli/cli.py +112 -5
- {hte_cli-0.2.31 → hte_cli-0.2.32}/.gitignore +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/README.md +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/src/hte_cli/__init__.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/src/hte_cli/__main__.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/src/hte_cli/api_client.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/src/hte_cli/config.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/src/hte_cli/errors.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/src/hte_cli/events.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/src/hte_cli/image_utils.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/src/hte_cli/runner.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/src/hte_cli/scorers.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/src/hte_cli/version_check.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/__init__.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/e2e/__init__.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/e2e/automated_runner.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/e2e/conftest.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/e2e/e2e_test.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/e2e/test_benchmark_flows.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/e2e/test_eval_logs.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/e2e/test_infrastructure.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/e2e/test_runtime_imports.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/e2e/test_session_lifecycle.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/e2e/verify_docker_deps.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/unit/__init__.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/unit/conftest.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/unit/test_image_utils.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/unit/test_runner.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/tests/unit/test_scorers.py +0 -0
- {hte_cli-0.2.31 → hte_cli-0.2.32}/uv.lock +0 -0
|
@@ -4,7 +4,9 @@ Uses Click for command parsing and Rich for pretty output.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
+
import signal
|
|
7
8
|
import sys
|
|
9
|
+
import threading
|
|
8
10
|
import webbrowser
|
|
9
11
|
|
|
10
12
|
import click
|
|
@@ -22,6 +24,83 @@ console = Console()
|
|
|
22
24
|
# Support email per spec
|
|
23
25
|
SUPPORT_EMAIL = "jacktpayne51@gmail.com"
|
|
24
26
|
|
|
27
|
+
# Warning before cap (15 minutes)
|
|
28
|
+
CAP_WARNING_SECONDS = 15 * 60
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CapEnforcer:
|
|
32
|
+
"""Background timer that enforces time cap on capped_completion tasks.
|
|
33
|
+
|
|
34
|
+
Shows warning 15 minutes before cap and terminates the task when cap is reached.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
time_cap_seconds: int,
|
|
40
|
+
start_time: float,
|
|
41
|
+
console: Console,
|
|
42
|
+
main_thread_id: int,
|
|
43
|
+
):
|
|
44
|
+
self.time_cap_seconds = time_cap_seconds
|
|
45
|
+
self.start_time = start_time
|
|
46
|
+
self.console = console
|
|
47
|
+
self.main_thread_id = main_thread_id
|
|
48
|
+
self._stop_event = threading.Event()
|
|
49
|
+
self._warning_shown = False
|
|
50
|
+
self._thread: threading.Thread | None = None
|
|
51
|
+
self.cap_reached = False
|
|
52
|
+
|
|
53
|
+
def start(self):
|
|
54
|
+
"""Start the background timer thread."""
|
|
55
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
|
56
|
+
self._thread.start()
|
|
57
|
+
|
|
58
|
+
def stop(self):
|
|
59
|
+
"""Stop the background timer."""
|
|
60
|
+
self._stop_event.set()
|
|
61
|
+
if self._thread:
|
|
62
|
+
self._thread.join(timeout=1.0)
|
|
63
|
+
|
|
64
|
+
def _run(self):
|
|
65
|
+
"""Timer loop that checks elapsed time."""
|
|
66
|
+
import time
|
|
67
|
+
|
|
68
|
+
warning_threshold = self.time_cap_seconds - CAP_WARNING_SECONDS
|
|
69
|
+
|
|
70
|
+
while not self._stop_event.is_set():
|
|
71
|
+
elapsed = time.monotonic() - self.start_time
|
|
72
|
+
|
|
73
|
+
# Show warning at 15 minutes before cap
|
|
74
|
+
if elapsed >= warning_threshold and not self._warning_shown:
|
|
75
|
+
self._warning_shown = True
|
|
76
|
+
remaining = self.time_cap_seconds - elapsed
|
|
77
|
+
minutes = int(remaining // 60)
|
|
78
|
+
self.console.print()
|
|
79
|
+
self.console.print(
|
|
80
|
+
f"[yellow bold]Warning: Time cap approaching - {minutes} minutes remaining[/yellow bold]"
|
|
81
|
+
)
|
|
82
|
+
self.console.print(
|
|
83
|
+
"[yellow]When cap is reached, session will end and you'll need to record progress in the web UI.[/yellow]"
|
|
84
|
+
)
|
|
85
|
+
self.console.print()
|
|
86
|
+
|
|
87
|
+
# Cap reached - terminate the main thread
|
|
88
|
+
if elapsed >= self.time_cap_seconds:
|
|
89
|
+
self.cap_reached = True
|
|
90
|
+
self.console.print()
|
|
91
|
+
self.console.print(
|
|
92
|
+
f"[red bold]Time cap reached ({self.time_cap_seconds // 60} minutes). Session ending.[/red bold]"
|
|
93
|
+
)
|
|
94
|
+
self.console.print(
|
|
95
|
+
"[yellow]Return to the web UI to record your progress and estimate completion time.[/yellow]"
|
|
96
|
+
)
|
|
97
|
+
# Send SIGINT to main thread to trigger KeyboardInterrupt
|
|
98
|
+
os.kill(os.getpid(), signal.SIGINT)
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
# Check every second
|
|
102
|
+
self._stop_event.wait(1.0)
|
|
103
|
+
|
|
25
104
|
|
|
26
105
|
def _find_eval_log_bytes(runner) -> bytes | None:
|
|
27
106
|
"""Find and read eval log bytes from runner's work directory.
|
|
@@ -45,7 +124,9 @@ def _find_eval_log_bytes(runner) -> bytes | None:
|
|
|
45
124
|
return None
|
|
46
125
|
|
|
47
126
|
|
|
48
|
-
def _upload_partial_log(
|
|
127
|
+
def _upload_partial_log(
|
|
128
|
+
api: APIClient, session_id: str, eval_log_bytes: bytes, console: Console
|
|
129
|
+
) -> None:
|
|
49
130
|
"""Upload partial eval log for interrupted session.
|
|
50
131
|
|
|
51
132
|
Best-effort: silently handles failures to not block exit.
|
|
@@ -470,7 +551,7 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
470
551
|
parts = line.split(": ", 1)
|
|
471
552
|
if len(parts) == 2:
|
|
472
553
|
layer_id = parts[0][-8:]
|
|
473
|
-
layer_status = parts[1][:
|
|
554
|
+
layer_status = parts[1][:85] # Include full progress bar + size
|
|
474
555
|
display = f"{layer_id}: {layer_status}"
|
|
475
556
|
if display != last_status[0]:
|
|
476
557
|
last_status[0] = display
|
|
@@ -606,6 +687,23 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
606
687
|
|
|
607
688
|
events.docker_started()
|
|
608
689
|
|
|
690
|
+
# Start cap enforcer if this is a capped_completion task
|
|
691
|
+
time_cap_seconds = assignment.get("time_cap_seconds")
|
|
692
|
+
cap_enforcer: CapEnforcer | None = None
|
|
693
|
+
if time_cap_seconds and session_info.get("mode") == "capped_completion":
|
|
694
|
+
cap_enforcer = CapEnforcer(
|
|
695
|
+
time_cap_seconds=time_cap_seconds,
|
|
696
|
+
start_time=time.monotonic(),
|
|
697
|
+
console=console,
|
|
698
|
+
main_thread_id=threading.get_ident(),
|
|
699
|
+
)
|
|
700
|
+
cap_enforcer.start()
|
|
701
|
+
console.print(
|
|
702
|
+
f"[dim]Time cap: {time_cap_seconds // 60} minutes "
|
|
703
|
+
f"(warning at {(time_cap_seconds - CAP_WARNING_SECONDS) // 60} min)[/dim]"
|
|
704
|
+
)
|
|
705
|
+
console.print()
|
|
706
|
+
|
|
609
707
|
runner = TaskRunner()
|
|
610
708
|
eval_log_bytes = None
|
|
611
709
|
try:
|
|
@@ -624,9 +722,15 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
624
722
|
if eval_log_bytes:
|
|
625
723
|
_upload_partial_log(api, session_id, eval_log_bytes, console)
|
|
626
724
|
console.print()
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
725
|
+
# Different message if cap was reached vs user interrupt
|
|
726
|
+
if cap_enforcer and cap_enforcer.cap_reached:
|
|
727
|
+
console.print(
|
|
728
|
+
"[yellow]Time cap reached. Return to web UI to record progress and estimate completion time.[/yellow]"
|
|
729
|
+
)
|
|
730
|
+
else:
|
|
731
|
+
console.print(
|
|
732
|
+
"[yellow]Interrupted. Session remains active - you can reconnect later.[/yellow]"
|
|
733
|
+
)
|
|
630
734
|
sys.exit(0)
|
|
631
735
|
except Exception as e:
|
|
632
736
|
events.docker_stopped(exit_code=1)
|
|
@@ -637,6 +741,9 @@ def session_join(ctx, session_id: str, force_setup: bool):
|
|
|
637
741
|
console.print(f"[red]Task execution failed: {e}[/red]")
|
|
638
742
|
sys.exit(1)
|
|
639
743
|
finally:
|
|
744
|
+
# Stop cap enforcer
|
|
745
|
+
if cap_enforcer:
|
|
746
|
+
cap_enforcer.stop()
|
|
640
747
|
runner.cleanup()
|
|
641
748
|
|
|
642
749
|
events.docker_stopped(exit_code=0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|