Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@

### News

- **2026-07-31**: v0.0.6 released — side-effect-gated recovery (checkpointed halts with structured `InteractionRequest`), uncertain-effect reporting for failed outbound calls, centralized logging with an independent daemon log level, session-bound LeapBoard analysis, platform-neutral gateway validators, and end-to-end architecture contract tests with the CI gate restored.
- **2026-08-06**: v0.0.8 released — Cross-platform Windows support (DaemonTransport protocol with TCP loopback IPC), real journey test layer with cassette-backed CI (6 e2e journeys, cost-bounded), community Windows fixes (@fanqiNO1). 1,540 tests.
- **2026-08-06**: v0.0.7 released — 1M-class context windows end-to-end, self-calibrating token estimator, internal-defect failure category, concurrent-TUI session identity isolation. 1,442 tests.

<details>
<summary>Previous releases</summary>

- **2026-07-31**: v0.0.6 released — side-effect-gated recovery (checkpointed halts with structured `InteractionRequest`), uncertain-effect reporting for failed outbound calls, centralized logging with an independent daemon log level, session-bound LeapBoard analysis, platform-neutral gateway validators, and end-to-end architecture contract tests with the CI gate restored.
- **2026-07-28**: v0.0.5 released — adaptive-depth execution for long-horizon tasks, built-in coding tools, per-session daemon concurrency, improved TUI stability, and hardened leapd recovery/status diagnostics.
- **2026-07-16**: **LeapBoard** — general-purpose monitoring web dashboard (Watch→Finding + Server-Driven UI); `/board` entry, live session analysis, and finance/sentiment/research templates.
- **2026-07-16**: v0.0.4 released — protocol-driven recovery coordinator with budget-constrained strategies, unified error classification, checkpoint-based cross-turn resumption, structured audit trail, and tool execution idempotency ledger.
Expand Down
6 changes: 3 additions & 3 deletions src/leapflow/cli/commands/host.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def _read_pid_file() -> Optional[int]:
if not pid_file.exists():
return None
try:
pid = int(pid_file.read_text().strip())
pid = int(pid_file.read_text(encoding="utf-8").strip())
# Check if process is alive
os.kill(pid, 0)
return pid
Expand All @@ -111,7 +111,7 @@ def _write_pid_file(pid: int) -> None:
"""Write PID to daemon pid file."""
pid_file = _daemon_pid_file()
pid_file.parent.mkdir(parents=True, exist_ok=True)
pid_file.write_text(str(pid))
pid_file.write_text(str(pid), encoding="utf-8")


def _remove_pid_file() -> None:
Expand Down Expand Up @@ -279,7 +279,7 @@ async def _cmd_start() -> int:
creationflags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
start_new_session = False

with open(log_file, "a") as lf:
with open(log_file, "a", encoding="utf-8") as lf:
proc = subprocess.Popen(
[sys.executable, "-c", daemon_script],
stdout=lf,
Expand Down
4 changes: 2 additions & 2 deletions src/leapflow/daemon/_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def _port_path(self, runtime_dir: Path) -> Path:

def _read_port(self, runtime_dir: Path) -> int:
port_path = self._port_path(runtime_dir)
return int(port_path.read_text().strip())
return int(port_path.read_text(encoding="utf-8").strip())

async def start_server(
self,
Expand All @@ -98,7 +98,7 @@ async def start_server(
addr = server.sockets[0].getsockname()
port = addr[1]
port_path = self._port_path(runtime_dir)
port_path.write_text(str(port))
port_path.write_text(str(port), encoding="utf-8")
return server

async def connect(self, runtime_dir: Path) -> Tuple[asyncio.StreamReader, asyncio.StreamWriter]:
Expand Down
34 changes: 30 additions & 4 deletions src/leapflow/daemon/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,14 +159,14 @@ def write_pid_file(runtime_dir: Path, pid: Optional[int] = None) -> None:
"""Write the daemon PID file and metadata."""
runtime_dir.mkdir(parents=True, exist_ok=True)
actual_pid = pid or os.getpid()
(runtime_dir / "leapd.pid").write_text(str(actual_pid))
(runtime_dir / "leapd.pid").write_text(str(actual_pid), encoding="utf-8")

meta = {
"pid": actual_pid,
"start_time": time.time(),
"version": "1",
}
(runtime_dir / "leapd.json").write_text(json.dumps(meta))
(runtime_dir / "leapd.json").write_text(json.dumps(meta), encoding="utf-8")
logger.info("daemon: wrote pid=%d to %s", actual_pid, runtime_dir / "leapd.pid")


Expand Down Expand Up @@ -202,6 +202,8 @@ def send_signal(runtime_dir: Path, sig: DaemonSignal = DaemonSignal.SIGTERM) ->
return False
try:
os.kill(pid, sig.value)
if sys.platform == "win32":
_wait_process_exit(pid, timeout_ms=5000)
return True
except OSError:
return False
Expand Down Expand Up @@ -291,6 +293,8 @@ def spawn_daemon(settings: object, *, mock_host: bool = False) -> subprocess.Pop
if mock_host:
command.append("--mock-host")
command.extend(["daemon", "serve", "--internal"])
env = os.environ.copy()
env.setdefault("PYTHONIOENCODING", "utf-8")
log_file = open(log_path, "ab")
try:
if sys.platform == "win32": # pragma: no cover - platform specific
Expand All @@ -304,6 +308,7 @@ def spawn_daemon(settings: object, *, mock_host: bool = False) -> subprocess.Pop
stderr=subprocess.STDOUT,
start_new_session=False,
creationflags=creationflags,
env=env,
)
else:
proc = subprocess.Popen(
Expand All @@ -313,6 +318,7 @@ def spawn_daemon(settings: object, *, mock_host: bool = False) -> subprocess.Pop
stderr=subprocess.STDOUT,
start_new_session=True,
close_fds=True,
env=env,
)
finally:
log_file.close()
Expand Down Expand Up @@ -345,19 +351,39 @@ def _wait_until_stopped(runtime_dir: Path, *, deadline: float, interval_s: float
def _read_pid(path: Path) -> Optional[int]:
"""Read PID from file, returning None if missing/invalid."""
try:
return int(path.read_text().strip())
return int(path.read_text(encoding="utf-8").strip())
except (OSError, ValueError):
return None


def _read_meta(path: Path) -> Optional[dict]:
"""Read daemon metadata JSON."""
try:
return json.loads(path.read_text())
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError, ValueError):
return None


def _wait_process_exit(pid: int, timeout_ms: int = 5000) -> None:
"""Block until the process fully exits (Windows only). Best-effort."""
if sys.platform != "win32":
return
try:
import ctypes

kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
SYNCHRONIZE = 0x00100000
handle = kernel32.OpenProcess(SYNCHRONIZE, False, pid)
if not handle:
return
try:
kernel32.WaitForSingleObject(handle, timeout_ms)
finally:
kernel32.CloseHandle(handle)
except (OSError, AttributeError):
pass


# Windows kernel32 constants for the exit-code liveness probe below.
_PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
_STILL_ACTIVE = 259
Expand Down
4 changes: 2 additions & 2 deletions src/leapflow/skills/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ def _load_from_snapshot(self) -> Optional[List[SkillEntry]]:
if not self._snapshot_path.exists():
return None
try:
data = json.loads(self._snapshot_path.read_text())
data = json.loads(self._snapshot_path.read_text(encoding="utf-8"))
# Convert list[str] back to tuple for frozen dataclass
entries: List[SkillEntry] = []
for raw in data:
Expand All @@ -225,7 +225,7 @@ def _save_snapshot(self, entries: List[SkillEntry]) -> None:
try:
self._skills_dir.mkdir(parents=True, exist_ok=True)
data = [dataclasses.asdict(e) for e in entries]
self._snapshot_path.write_text(json.dumps(data, ensure_ascii=False))
self._snapshot_path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
except Exception:
pass # Non-critical — next scan will rebuild

Expand Down
5 changes: 3 additions & 2 deletions src/leapflow/storage/duckdb_connect.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import logging
import os
import sys
import time
from pathlib import Path
from typing import Optional
Expand All @@ -25,8 +26,8 @@

LOCK_KEYWORDS = frozenset({"lock", "locked", "exclusive", "cannot set lock"})

_CONNECT_RETRIES = int(os.getenv("LEAPFLOW_DB_CONNECT_RETRIES", "3"))
_CONNECT_BACKOFF_S = float(os.getenv("LEAPFLOW_DB_CONNECT_BACKOFF_S", "0.5"))
_CONNECT_RETRIES = int(os.getenv("LEAPFLOW_DB_CONNECT_RETRIES", "5" if sys.platform == "win32" else "3"))
_CONNECT_BACKOFF_S = float(os.getenv("LEAPFLOW_DB_CONNECT_BACKOFF_S", "1.0" if sys.platform == "win32" else "0.5"))


class DatabaseLockedError(RuntimeError):
Expand Down
2 changes: 1 addition & 1 deletion src/leapflow/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Version information for leapflow."""

__version__ = "0.0.7+main"
__version__ = "0.0.8+main"
Loading