# session_logger.py """ Opens timestamped log/script files at startup and keeps them open for the lifetime of the process. The next run of the GUI creates new files; the previous run's files are simply closed when the process exits. File layout ----------- logs/ comms_.log - every comms entry (direction/kind/payload) as JSON-L toolcalls_.log - sequential record of every tool invocation clicalls_.log - sequential record of every CLI subprocess call scripts/generated/ _.ps1 - each PowerShell script the AI generated, in order Where = YYYYMMDD_HHMMSS of when this session was started. """ import atexit import datetime import json import threading from pathlib import Path _LOG_DIR = Path("./logs") _SCRIPTS_DIR = Path("./scripts/generated") _ts: str = "" # session timestamp string e.g. "20260301_142233" _session_id: str = "" # YYYYMMDD_HHMMSS[_Label] _session_dir: Path = None # Path to the sub-directory for this session _seq: int = 0 # monotonic counter for script files this session _seq_lock = threading.Lock() _comms_fh = None # file handle: logs//comms.log _tool_fh = None # file handle: logs//toolcalls.log _api_fh = None # file handle: logs//apihooks.log _cli_fh = None # file handle: logs//clicalls.log def _now_ts() -> str: return datetime.datetime.now().strftime("%Y%m%d_%H%M%S") def open_session(label: str | None = None): """ Called once at GUI startup. Creates the log directories if needed and opens the log files for this session within a sub-directory. """ global _ts, _session_id, _session_dir, _comms_fh, _tool_fh, _api_fh, _cli_fh, _seq if _comms_fh is not None: return # already open _ts = _now_ts() _session_id = _ts if label: # Sanitize label: remove non-alphanumeric chars safe_label = "".join(c if c.isalnum() or c in ("-", "_") else "_" for c in label) _session_id += f"_{safe_label}" _session_dir = _LOG_DIR / _session_id _session_dir.mkdir(parents=True, exist_ok=True) _SCRIPTS_DIR.mkdir(parents=True, exist_ok=True) _seq = 0 _comms_fh = open(_session_dir / "comms.log", "w", encoding="utf-8", buffering=1) _tool_fh = open(_session_dir / "toolcalls.log", "w", encoding="utf-8", buffering=1) _api_fh = open(_session_dir / "apihooks.log", "w", encoding="utf-8", buffering=1) _cli_fh = open(_session_dir / "clicalls.log", "w", encoding="utf-8", buffering=1) _tool_fh.write(f"# Tool-call log — session {_session_id}\n\n") _tool_fh.flush() _cli_fh.write(f"# CLI Subprocess Call Log — session {_session_id}\n\n") _cli_fh.flush() # Register this session in the log registry try: from log_registry import LogRegistry registry = LogRegistry(str(_LOG_DIR / "log_registry.toml")) registry.register_session(_session_id, str(_session_dir), datetime.datetime.now()) except Exception as e: print(f"Warning: Could not register session in LogRegistry: {e}") atexit.register(close_session) def close_session(): """Flush and close all log files. Called on clean exit (optional).""" global _comms_fh, _tool_fh, _api_fh, _cli_fh if _comms_fh: _comms_fh.close() _comms_fh = None if _tool_fh: _tool_fh.close() _tool_fh = None if _api_fh: _api_fh.close() _api_fh = None if _cli_fh: # Close the new log file handle _cli_fh.close() _cli_fh = None def log_api_hook(method: str, path: str, payload: str): """ Log an API hook invocation. """ if _api_fh is None: return ts_entry = datetime.datetime.now().strftime("%H:%M:%S") try: _api_fh.write(f"[{ts_entry}] {method} {path} - Payload: {payload}\n") _api_fh.flush() except Exception: pass def log_comms(entry: dict): """ Append one comms entry to the comms log file as a JSON-L line. Thread-safe (GIL + line-buffered file). """ if _comms_fh is None: return try: _comms_fh.write(json.dumps(entry, ensure_ascii=False, default=str) + "\n") except Exception: pass def log_tool_call(script: str, result: str, script_path: str | None): """ Append a tool-call record to the toolcalls log and write the PS1 script to scripts/generated/. Returns the path of the written script file. """ global _seq if _tool_fh is None: return script_path # logger not open yet with _seq_lock: _seq += 1 seq = _seq ts_entry = datetime.datetime.now().strftime("%H:%M:%S") # Write the .ps1 file ps1_name = f"{_ts}_{seq:04d}.ps1" ps1_path = _SCRIPTS_DIR / ps1_name try: ps1_path.write_text(script, encoding="utf-8") except Exception as exc: ps1_path = None ps1_name = f"(write error: {exc})" # Append to the tool-call sequence log (script body omitted - see .ps1 file) try: _tool_fh.write( f"## Call #{seq} [{ts_entry}]\n" f"Script file: {ps1_path}\n\n" f"### Result\n\n" f"```\n{result}\n```\n\n" f"---\n\n" ) _tool_fh.flush() except Exception: pass return str(ps1_path) if ps1_path else None def log_cli_call(command: str, stdin_content: str | None, stdout_content: str | None, stderr_content: str | None, latency: float): """ Log details of a CLI subprocess execution. """ if _cli_fh is None: return ts_entry = datetime.datetime.now().strftime("%H:%M:%S") try: log_data = { "timestamp": ts_entry, "command": command, "stdin": stdin_content, "stdout": stdout_content, "stderr": stderr_content, "latency_sec": latency } _cli_fh.write(json.dumps(log_data, ensure_ascii=False, default=str) + "\n") _cli_fh.flush() except Exception: pass