aline-ai 0.7.3__py3-none-any.whl → 0.7.4__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.
@@ -38,15 +38,6 @@ class ConfigPanel(Static):
38
38
  margin-bottom: 1;
39
39
  }
40
40
 
41
- ConfigPanel .button-row {
42
- height: 3;
43
- margin-top: 1;
44
- }
45
-
46
- ConfigPanel .button-row Button {
47
- margin-right: 1;
48
- }
49
-
50
41
  ConfigPanel .account-section {
51
42
  height: 3;
52
43
  align: left middle;
@@ -62,81 +53,36 @@ class ConfigPanel(Static):
62
53
  margin-right: 1;
63
54
  }
64
55
 
65
- ConfigPanel .tmux-settings {
56
+ ConfigPanel .settings-section {
66
57
  height: auto;
67
58
  margin-top: 2;
68
59
  }
69
60
 
70
- ConfigPanel .tmux-settings .setting-row {
61
+ ConfigPanel .settings-section .setting-row {
71
62
  height: auto;
72
63
  }
73
64
 
74
- ConfigPanel .tmux-settings .setting-label {
75
- width: auto;
65
+ ConfigPanel .settings-section .setting-label {
66
+ width: 16;
76
67
  }
77
68
 
78
- ConfigPanel .tmux-settings RadioSet {
69
+ ConfigPanel .settings-section RadioSet {
79
70
  width: auto;
80
71
  height: auto;
81
72
  layout: horizontal;
82
73
  }
83
74
 
84
- ConfigPanel .tmux-settings RadioButton {
75
+ ConfigPanel .settings-section RadioButton {
85
76
  width: auto;
86
77
  margin-right: 2;
87
78
  }
88
79
 
89
- ConfigPanel .tools-section {
90
- height: auto;
91
- margin-top: 2;
92
- }
93
-
94
- ConfigPanel .terminal-settings {
95
- height: auto;
96
- margin-top: 2;
97
- }
98
-
99
- ConfigPanel .terminal-settings .setting-row {
100
- height: auto;
101
- }
102
-
103
- ConfigPanel .terminal-settings .setting-label {
104
- width: auto;
105
- }
106
-
107
- ConfigPanel .terminal-settings RadioSet {
108
- width: auto;
109
- height: auto;
110
- layout: horizontal;
111
- }
112
-
113
- ConfigPanel .terminal-settings RadioButton {
114
- width: auto;
115
- margin-right: 2;
116
- }
117
-
118
- ConfigPanel .appearance-settings {
119
- height: auto;
120
- margin-top: 2;
121
- }
122
-
123
- ConfigPanel .appearance-settings .setting-row {
124
- height: auto;
125
- }
126
-
127
- ConfigPanel .appearance-settings .setting-label {
128
- width: auto;
129
- }
130
-
131
- ConfigPanel .appearance-settings RadioSet {
132
- width: auto;
133
- height: auto;
134
- layout: horizontal;
80
+ ConfigPanel .settings-section .button-row {
81
+ height: 3;
135
82
  }
136
83
 
137
- ConfigPanel .appearance-settings RadioButton {
138
- width: auto;
139
- margin-right: 2;
84
+ ConfigPanel .settings-section .button-row Button {
85
+ margin-right: 1;
140
86
  }
141
87
  """
142
88
 
@@ -146,7 +92,6 @@ class ConfigPanel(Static):
146
92
  self._syncing_radio: bool = False # Flag to prevent recursive radio updates
147
93
  self._login_in_progress: bool = False # Track login state
148
94
  self._refresh_timer = None # Timer for auto-refresh
149
- self._auto_close_stale_enabled: bool = False # Track auto-close setting
150
95
 
151
96
  def compose(self) -> ComposeResult:
152
97
  """Compose the config panel layout."""
@@ -157,7 +102,7 @@ class ConfigPanel(Static):
157
102
  yield Button("Login", id="auth-btn", variant="primary")
158
103
 
159
104
  # Appearance settings section
160
- with Static(classes="appearance-settings"):
105
+ with Static(classes="settings-section"):
161
106
  yield Static("[bold]Appearance[/bold]", classes="section-title")
162
107
  with Horizontal(classes="setting-row"):
163
108
  yield Static("Theme:", classes="setting-label")
@@ -166,7 +111,7 @@ class ConfigPanel(Static):
166
111
  yield RadioButton("Light", id="theme-light")
167
112
 
168
113
  # Tmux settings section
169
- with Static(classes="tmux-settings"):
114
+ with Static(classes="settings-section"):
170
115
  yield Static("[bold]Tmux Settings[/bold]", classes="section-title")
171
116
  with Horizontal(classes="setting-row"):
172
117
  yield Static("Border resize:", classes="setting-label")
@@ -174,17 +119,8 @@ class ConfigPanel(Static):
174
119
  yield RadioButton("Enabled", id="border-resize-enabled", value=True)
175
120
  yield RadioButton("Disabled", id="border-resize-disabled")
176
121
 
177
- # Terminal settings section
178
- with Static(classes="terminal-settings"):
179
- yield Static("[bold]Terminal Settings[/bold]", classes="section-title")
180
- with Horizontal(classes="setting-row"):
181
- yield Static("Auto-close stale terminals (24h):", classes="setting-label")
182
- with RadioSet(id="auto-close-stale-radio"):
183
- yield RadioButton("Enabled", id="auto-close-stale-enabled")
184
- yield RadioButton("Disabled", id="auto-close-stale-disabled", value=True)
185
-
186
122
  # Tools section
187
- with Static(classes="tools-section"):
123
+ with Static(classes="settings-section"):
188
124
  yield Static("[bold]Tools[/bold]", classes="section-title")
189
125
  with Horizontal(classes="button-row"):
190
126
  yield Button("Aline Doctor", id="doctor-btn", variant="default")
@@ -200,11 +136,25 @@ class ConfigPanel(Static):
200
136
  # Query and set the actual tmux border resize state
201
137
  self._sync_border_resize_radio()
202
138
 
203
- # Sync auto-close stale terminals setting from config
204
- self._sync_auto_close_stale_radio()
139
+ def on_show(self) -> None:
140
+ """Start periodic refresh when visible."""
141
+ if self._refresh_timer is None:
142
+ self._refresh_timer = self.set_interval(5.0, self._update_account_status)
143
+ else:
144
+ try:
145
+ self._refresh_timer.resume()
146
+ except Exception:
147
+ pass
148
+ self._update_account_status()
205
149
 
206
- # Start timer to periodically refresh account status (every 5 seconds)
207
- self._refresh_timer = self.set_interval(5.0, self._update_account_status)
150
+ def on_hide(self) -> None:
151
+ """Pause periodic refresh when hidden."""
152
+ if self._refresh_timer is None:
153
+ return
154
+ try:
155
+ self._refresh_timer.pause()
156
+ except Exception:
157
+ pass
208
158
 
209
159
  def on_button_pressed(self, event: Button.Pressed) -> None:
210
160
  """Handle button clicks."""
@@ -228,9 +178,6 @@ class ConfigPanel(Static):
228
178
  # Check which radio button is selected
229
179
  enabled = event.pressed.id == "border-resize-enabled"
230
180
  self._toggle_border_resize(enabled)
231
- elif event.radio_set.id == "auto-close-stale-radio":
232
- enabled = event.pressed.id == "auto-close-stale-enabled"
233
- self._toggle_auto_close_stale(enabled)
234
181
 
235
182
  def _update_account_status(self) -> None:
236
183
  """Update the account status display."""
@@ -379,7 +326,7 @@ class ConfigPanel(Static):
379
326
  """Query tmux state and sync the radio buttons to match."""
380
327
  try:
381
328
  # Check if MouseDrag1Border is bound by listing keys
382
- result = _run_outer_tmux(["list-keys", "-T", "root"], capture=True)
329
+ result = _run_outer_tmux(["list-keys", "-T", "root"], capture=True, timeout_s=0.5)
383
330
  output = result.stdout or ""
384
331
 
385
332
  # If MouseDrag1Border is in the output, resize is enabled
@@ -407,53 +354,19 @@ class ConfigPanel(Static):
407
354
  # Re-enable border resize by binding MouseDrag1Border to default resize behavior
408
355
  _run_outer_tmux([
409
356
  "bind", "-n", "MouseDrag1Border", "resize-pane", "-M"
410
- ])
357
+ ], timeout_s=0.5)
411
358
  self._border_resize_enabled = True
412
359
  self.app.notify("Border resize enabled", title="Tmux")
413
360
  else:
414
361
  # Disable border resize by unbinding MouseDrag1Border
415
362
  _run_outer_tmux([
416
363
  "unbind", "-n", "MouseDrag1Border"
417
- ])
364
+ ], timeout_s=0.5)
418
365
  self._border_resize_enabled = False
419
366
  self.app.notify("Border resize disabled", title="Tmux")
420
367
  except Exception as e:
421
368
  self.app.notify(f"Error toggling border resize: {e}", title="Tmux", severity="error")
422
369
 
423
- def _sync_auto_close_stale_radio(self) -> None:
424
- """Sync radio buttons with config file setting."""
425
- try:
426
- config = ReAlignConfig.load()
427
- is_enabled = config.auto_close_stale_terminals
428
- self._auto_close_stale_enabled = is_enabled
429
-
430
- # Update radio buttons without triggering the toggle action
431
- self._syncing_radio = True
432
- try:
433
- if is_enabled:
434
- radio = self.query_one("#auto-close-stale-enabled", RadioButton)
435
- else:
436
- radio = self.query_one("#auto-close-stale-disabled", RadioButton)
437
- radio.value = True
438
- finally:
439
- self._syncing_radio = False
440
- except Exception:
441
- pass
442
-
443
- def _toggle_auto_close_stale(self, enabled: bool) -> None:
444
- """Enable or disable auto-close stale terminals setting."""
445
- try:
446
- config = ReAlignConfig.load()
447
- config.auto_close_stale_terminals = enabled
448
- config.save()
449
- self._auto_close_stale_enabled = enabled
450
- if enabled:
451
- self.app.notify("Auto-close stale terminals enabled", title="Terminal")
452
- else:
453
- self.app.notify("Auto-close stale terminals disabled", title="Terminal")
454
- except Exception as e:
455
- self.app.notify(f"Error saving setting: {e}", title="Config", severity="error")
456
-
457
370
  def _handle_doctor(self) -> None:
458
371
  """Run aline doctor directly in background thread."""
459
372
  self.app.notify("Running Aline Doctor...", title="Doctor")
@@ -40,5 +40,5 @@ class AlineHeader(Static):
40
40
  line3 = " ███████║██║ ██║██╔██╗ ██║█████╗ "
41
41
  line4 = " ██╔══██║██║ ██║██║╚██╗██║██╔══╝ "
42
42
  line5 = " ██║ ██║███████╗██║██║ ╚████║███████╗"
43
- info = f"[dim]v{self._version} │ Shared Agent Context[/dim]"
43
+ info = f"[dim]v{self._version}[/dim]"
44
44
  return f"{line1}\n{line2}\n{line3}\n{line4}\n{line5} {info}"
realign/db/sqlite_db.py CHANGED
@@ -958,15 +958,23 @@ class SQLiteDatabase(DatabaseInterface):
958
958
  kind=excluded.kind,
959
959
  payload=excluded.payload,
960
960
  priority=MAX(COALESCE(jobs.priority, 0), COALESCE(excluded.priority, 0)),
961
+ attempts=CASE
962
+ WHEN jobs.status='retry' THEN 0
963
+ ELSE COALESCE(jobs.attempts, 0)
964
+ END,
961
965
  updated_at=datetime('now'),
962
966
  reschedule=CASE
963
967
  WHEN jobs.status='processing' THEN 1
964
968
  ELSE COALESCE(jobs.reschedule, 0)
965
969
  END,
970
+ last_error=CASE
971
+ WHEN jobs.status='retry' THEN NULL
972
+ ELSE jobs.last_error
973
+ END,
966
974
  status=CASE
967
975
  WHEN jobs.status='processing' THEN jobs.status
968
976
  WHEN jobs.status='queued' THEN jobs.status
969
- WHEN jobs.status='retry' THEN jobs.status
977
+ WHEN jobs.status='retry' THEN 'queued'
970
978
  WHEN jobs.status='done' AND ? = 0 THEN jobs.status
971
979
  ELSE 'queued'
972
980
  END,
@@ -1074,6 +1082,56 @@ class SQLiteDatabase(DatabaseInterface):
1074
1082
  requeue_done=True,
1075
1083
  )
1076
1084
 
1085
+ def enqueue_session_process_job(
1086
+ self,
1087
+ *,
1088
+ session_file_path: Path,
1089
+ session_id: str | None = None,
1090
+ workspace_path: str | Path | None = None,
1091
+ session_type: str | None = None,
1092
+ source_event: str | None = None,
1093
+ no_track: bool = False,
1094
+ agent_id: str | None = None,
1095
+ terminal_id: str | None = None,
1096
+ priority: int = 15,
1097
+ ) -> str:
1098
+ """Enqueue a per-session processing job.
1099
+
1100
+ This is the preferred way to react to Stop hooks / file changes:
1101
+ - Dedupe by session_id so repeated enqueues don't pile up.
1102
+ - Worker will process all missing turns up to the safe boundary.
1103
+ """
1104
+ sid = (session_id or session_file_path.stem or "").strip()
1105
+ if not sid:
1106
+ raise ValueError("session_id is required for session_process job")
1107
+ dedupe_key = f"session_process:{sid}"
1108
+ payload: Dict[str, Any] = {
1109
+ "session_id": sid,
1110
+ "session_file_path": str(session_file_path),
1111
+ }
1112
+ if workspace_path is not None:
1113
+ payload["workspace_path"] = str(workspace_path)
1114
+ if session_type:
1115
+ payload["session_type"] = str(session_type)
1116
+ if source_event:
1117
+ payload["source_event"] = str(source_event)
1118
+ if no_track:
1119
+ payload["no_track"] = True
1120
+ if agent_id:
1121
+ payload["agent_id"] = agent_id
1122
+ if terminal_id:
1123
+ payload["terminal_id"] = terminal_id
1124
+
1125
+ # Always requeue: session_process is an "edge triggered" event that may arrive
1126
+ # after new turns were appended. Even if the last job is done, re-run to catch up.
1127
+ return self.enqueue_job(
1128
+ kind="session_process",
1129
+ dedupe_key=dedupe_key,
1130
+ payload=payload,
1131
+ priority=int(priority),
1132
+ requeue_done=True,
1133
+ )
1134
+
1077
1135
  def enqueue_agent_description_job(self, *, agent_id: str) -> str:
1078
1136
  dedupe_key = f"agent_desc:{agent_id}"
1079
1137
  payload: Dict[str, Any] = {"agent_id": agent_id}
realign/logging_config.py CHANGED
@@ -7,11 +7,15 @@ Supports file rotation, environment variable configuration, and structured loggi
7
7
 
8
8
  import logging
9
9
  import os
10
+ import tempfile
10
11
  from pathlib import Path
11
12
  from logging.handlers import RotatingFileHandler
12
13
  from typing import Optional
13
14
 
14
15
 
16
+ _LOG_DIR_CACHE: tuple[str | None, Path] | None = None
17
+
18
+
15
19
  def get_log_level() -> int:
16
20
  """
17
21
  Get log level from environment variable or default to INFO.
@@ -46,17 +50,57 @@ def get_log_directory() -> Path:
46
50
  Returns:
47
51
  Path: Log directory path
48
52
  """
53
+ global _LOG_DIR_CACHE
54
+
49
55
  log_dir_str = os.getenv("REALIGN_LOG_DIR")
56
+ if _LOG_DIR_CACHE is not None and _LOG_DIR_CACHE[0] == log_dir_str:
57
+ return _LOG_DIR_CACHE[1]
50
58
 
59
+ candidates: list[Path] = []
51
60
  if log_dir_str:
52
- log_dir = Path(log_dir_str).expanduser()
61
+ candidates.append(Path(log_dir_str).expanduser())
53
62
  else:
54
- log_dir = Path.home() / ".aline" / ".logs"
63
+ candidates.append(Path.home() / ".aline" / ".logs")
55
64
 
56
- # Create directory if it doesn't exist
57
- log_dir.mkdir(parents=True, exist_ok=True)
65
+ # Fallback for restricted environments (e.g., sandboxed runners).
66
+ candidates.append(Path(tempfile.gettempdir()) / "aline-logs")
58
67
 
59
- return log_dir
68
+ def can_write_files(directory: Path) -> bool:
69
+ try:
70
+ directory.mkdir(parents=True, exist_ok=True)
71
+ probe = directory / f".write_probe_{os.getpid()}_{os.urandom(4).hex()}"
72
+ probe.write_text("ok", encoding="utf-8")
73
+ probe.unlink(missing_ok=True)
74
+ return True
75
+ except Exception:
76
+ return False
77
+
78
+ last_error: Exception | None = None
79
+ for candidate in candidates:
80
+ try:
81
+ if can_write_files(candidate):
82
+ _LOG_DIR_CACHE = (log_dir_str, candidate)
83
+ return candidate
84
+ except Exception as e:
85
+ last_error = e
86
+ continue
87
+
88
+ # Last resort: current working directory (best-effort, avoids crashing).
89
+ try:
90
+ cwd = Path.cwd() / ".aline-logs"
91
+ if can_write_files(cwd):
92
+ _LOG_DIR_CACHE = (log_dir_str, cwd)
93
+ return cwd
94
+ return cwd
95
+ except Exception:
96
+ # If even this fails, return the first candidate to keep callers deterministic.
97
+ # The logger will fall back to stderr-only.
98
+ if last_error:
99
+ chosen = candidates[0]
100
+ else:
101
+ chosen = Path.cwd()
102
+ _LOG_DIR_CACHE = (log_dir_str, chosen)
103
+ return chosen
60
104
 
61
105
 
62
106
  def setup_logger(
@@ -86,8 +130,9 @@ def setup_logger(
86
130
  """
87
131
  logger = logging.getLogger(name)
88
132
 
89
- # Only configure if not already configured
90
133
  if logger.handlers:
134
+ # Keep logger level in sync with env var, but avoid duplicating handlers.
135
+ logger.setLevel(get_log_level())
91
136
  return logger
92
137
 
93
138
  logger.setLevel(get_log_level())