aline-ai 0.6.2__py3-none-any.whl → 0.6.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.
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/METADATA +1 -1
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/RECORD +38 -37
- realign/__init__.py +1 -1
- realign/adapters/__init__.py +0 -3
- realign/adapters/codex.py +14 -9
- realign/cli.py +42 -236
- realign/codex_detector.py +72 -32
- realign/codex_home.py +85 -0
- realign/codex_terminal_linker.py +172 -0
- realign/commands/__init__.py +2 -2
- realign/commands/add.py +89 -9
- realign/commands/doctor.py +495 -0
- realign/commands/export_shares.py +154 -226
- realign/commands/init.py +66 -4
- realign/commands/watcher.py +30 -80
- realign/config.py +9 -46
- realign/dashboard/app.py +7 -11
- realign/dashboard/screens/event_detail.py +0 -3
- realign/dashboard/screens/session_detail.py +0 -1
- realign/dashboard/tmux_manager.py +129 -4
- realign/dashboard/widgets/config_panel.py +175 -241
- realign/dashboard/widgets/events_table.py +71 -128
- realign/dashboard/widgets/sessions_table.py +77 -136
- realign/dashboard/widgets/terminal_panel.py +349 -27
- realign/dashboard/widgets/watcher_panel.py +0 -2
- realign/db/sqlite_db.py +77 -2
- realign/events/event_summarizer.py +76 -35
- realign/events/session_summarizer.py +73 -32
- realign/hooks.py +334 -647
- realign/llm_client.py +201 -520
- realign/triggers/__init__.py +0 -2
- realign/triggers/next_turn_trigger.py +4 -5
- realign/triggers/registry.py +1 -4
- realign/watcher_core.py +53 -35
- realign/adapters/antigravity.py +0 -159
- realign/triggers/antigravity_trigger.py +0 -140
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/top_level.txt +0 -0
|
@@ -15,7 +15,9 @@ import asyncio
|
|
|
15
15
|
import os
|
|
16
16
|
import re
|
|
17
17
|
import shlex
|
|
18
|
+
import time
|
|
18
19
|
import traceback
|
|
20
|
+
from datetime import datetime, timedelta, timezone
|
|
19
21
|
from pathlib import Path
|
|
20
22
|
from typing import Callable, Union
|
|
21
23
|
|
|
@@ -385,13 +387,175 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
385
387
|
# Native terminal: check provider
|
|
386
388
|
return w.provider == "claude"
|
|
387
389
|
|
|
388
|
-
# tmux: check window name and tags
|
|
389
390
|
window_name = (w.window_name or "").strip().lower()
|
|
391
|
+
if re.fullmatch(r"codex(?:-\d+)?", window_name or ""):
|
|
392
|
+
return False
|
|
393
|
+
|
|
394
|
+
# tmux: prefer explicit provider/session_type tags
|
|
395
|
+
if (w.provider or w.session_type):
|
|
396
|
+
return (w.provider == "claude") or (w.session_type == "claude")
|
|
397
|
+
|
|
398
|
+
# tmux: fallback to window name heuristic
|
|
390
399
|
if re.fullmatch(r"cc(?:-\d+)?", window_name or ""):
|
|
391
400
|
return True
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
@staticmethod
|
|
404
|
+
def _is_codex_window(w: WindowData) -> bool:
|
|
405
|
+
"""Check if a window is a Codex terminal."""
|
|
406
|
+
if isinstance(w, TerminalInfo):
|
|
407
|
+
return w.provider == "codex"
|
|
408
|
+
|
|
409
|
+
# tmux: prefer explicit provider/session_type tags
|
|
410
|
+
if (w.provider or w.session_type):
|
|
411
|
+
return (w.provider == "codex") or (w.session_type == "codex")
|
|
412
|
+
|
|
413
|
+
window_name = (w.window_name or "").strip().lower()
|
|
414
|
+
if re.fullmatch(r"codex(?:-\d+)?", window_name or ""):
|
|
415
|
+
return True
|
|
416
|
+
|
|
417
|
+
return False
|
|
418
|
+
|
|
419
|
+
@classmethod
|
|
420
|
+
def _supports_context(cls, w: WindowData) -> bool:
|
|
421
|
+
return cls._is_claude_window(w) or cls._is_codex_window(w)
|
|
422
|
+
|
|
423
|
+
@staticmethod
|
|
424
|
+
def _is_internal_tmux_window(w: tmux_manager.InnerWindow) -> bool:
|
|
425
|
+
"""Hide internal tmux windows (e.g., the reserved 'home' window)."""
|
|
426
|
+
if not w.no_track:
|
|
427
|
+
return False
|
|
428
|
+
if (w.window_name or "").strip().lower() != "home":
|
|
429
|
+
return False
|
|
430
|
+
return not any(
|
|
431
|
+
(
|
|
432
|
+
(w.terminal_id or "").strip(),
|
|
433
|
+
(w.provider or "").strip(),
|
|
434
|
+
(w.session_type or "").strip(),
|
|
435
|
+
(w.session_id or "").strip(),
|
|
436
|
+
(w.context_id or "").strip(),
|
|
437
|
+
(w.transcript_path or "").strip(),
|
|
438
|
+
(w.attention or "").strip(),
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def _maybe_link_codex_session_for_terminal(
|
|
443
|
+
self, *, terminal_id: str, created_at: float | None
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Best-effort: bind a Codex session file to a dashboard terminal (no watcher required)."""
|
|
446
|
+
terminal_id = (terminal_id or "").strip()
|
|
447
|
+
if not terminal_id:
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
now = time.time()
|
|
451
|
+
last = self._codex_link_last_attempt.get(terminal_id, 0.0)
|
|
452
|
+
if now - last < 2.0:
|
|
453
|
+
return
|
|
454
|
+
self._codex_link_last_attempt[terminal_id] = now
|
|
455
|
+
|
|
456
|
+
try:
|
|
457
|
+
from ...codex_terminal_linker import read_codex_session_meta
|
|
458
|
+
from ...db import get_database
|
|
459
|
+
from ...codex_home import codex_sessions_dir_for_terminal
|
|
460
|
+
except Exception:
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
try:
|
|
464
|
+
db = get_database(read_only=False)
|
|
465
|
+
agent = db.get_agent_by_id(terminal_id)
|
|
466
|
+
if not agent or agent.provider != "codex" or agent.status != "active":
|
|
467
|
+
return
|
|
468
|
+
if agent.session_id:
|
|
469
|
+
return
|
|
470
|
+
cwd = (agent.cwd or "").strip()
|
|
471
|
+
if not cwd:
|
|
472
|
+
return
|
|
473
|
+
except Exception:
|
|
474
|
+
return
|
|
475
|
+
|
|
476
|
+
candidates: list[Path] = []
|
|
477
|
+
sessions_root = codex_sessions_dir_for_terminal(terminal_id)
|
|
478
|
+
if sessions_root.exists():
|
|
479
|
+
# Deterministic: isolated per-terminal CODEX_HOME.
|
|
480
|
+
try:
|
|
481
|
+
candidates = list(sessions_root.rglob("rollout-*.jsonl"))
|
|
482
|
+
except Exception:
|
|
483
|
+
candidates = []
|
|
484
|
+
else:
|
|
485
|
+
# Fallback for legacy terminals not launched with isolated CODEX_HOME.
|
|
486
|
+
try:
|
|
487
|
+
from ...codex_detector import find_codex_sessions_for_project
|
|
392
488
|
|
|
393
|
-
|
|
394
|
-
|
|
489
|
+
candidates = find_codex_sessions_for_project(Path(cwd), days_back=3)
|
|
490
|
+
except Exception:
|
|
491
|
+
candidates = []
|
|
492
|
+
if not candidates:
|
|
493
|
+
return
|
|
494
|
+
|
|
495
|
+
created_dt: datetime | None = None
|
|
496
|
+
if created_at is not None:
|
|
497
|
+
try:
|
|
498
|
+
created_dt = datetime.fromtimestamp(float(created_at), tz=timezone.utc)
|
|
499
|
+
except Exception:
|
|
500
|
+
created_dt = None
|
|
501
|
+
|
|
502
|
+
best: Path | None = None
|
|
503
|
+
best_score: float | None = None
|
|
504
|
+
candidates.sort(key=lambda p: p.stat().st_mtime if p.exists() else 0, reverse=True)
|
|
505
|
+
for session_file in candidates[:200]:
|
|
506
|
+
meta = read_codex_session_meta(session_file)
|
|
507
|
+
if meta is None or (meta.cwd or "").strip() != cwd:
|
|
508
|
+
continue
|
|
509
|
+
|
|
510
|
+
started_dt: datetime | None = meta.started_at
|
|
511
|
+
if started_dt is None:
|
|
512
|
+
try:
|
|
513
|
+
started_dt = datetime.fromtimestamp(
|
|
514
|
+
session_file.stat().st_mtime, tz=timezone.utc
|
|
515
|
+
)
|
|
516
|
+
except Exception:
|
|
517
|
+
started_dt = None
|
|
518
|
+
if started_dt is None:
|
|
519
|
+
continue
|
|
520
|
+
|
|
521
|
+
if created_dt is not None:
|
|
522
|
+
delta = abs((started_dt - created_dt).total_seconds())
|
|
523
|
+
else:
|
|
524
|
+
try:
|
|
525
|
+
delta = abs(time.time() - session_file.stat().st_mtime)
|
|
526
|
+
except Exception:
|
|
527
|
+
continue
|
|
528
|
+
|
|
529
|
+
penalty = 0.0
|
|
530
|
+
origin = (meta.originator or "").lower()
|
|
531
|
+
if "vscode" in origin:
|
|
532
|
+
penalty += 3600.0
|
|
533
|
+
score = float(delta) + penalty
|
|
534
|
+
|
|
535
|
+
if best_score is None or score < best_score:
|
|
536
|
+
best_score = score
|
|
537
|
+
best = session_file
|
|
538
|
+
|
|
539
|
+
if not best:
|
|
540
|
+
return
|
|
541
|
+
|
|
542
|
+
# Avoid binding wildly unrelated sessions.
|
|
543
|
+
if best_score is not None and best_score > 6 * 60 * 60:
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
try:
|
|
547
|
+
db.update_agent(
|
|
548
|
+
terminal_id,
|
|
549
|
+
provider="codex",
|
|
550
|
+
session_type="codex",
|
|
551
|
+
session_id=best.stem,
|
|
552
|
+
transcript_path=str(best),
|
|
553
|
+
cwd=cwd,
|
|
554
|
+
project_dir=cwd,
|
|
555
|
+
source="dashboard:auto-link",
|
|
556
|
+
)
|
|
557
|
+
except Exception:
|
|
558
|
+
return
|
|
395
559
|
|
|
396
560
|
def __init__(self, use_native_terminal: bool | None = None) -> None:
|
|
397
561
|
"""Initialize the terminal panel.
|
|
@@ -420,6 +584,9 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
420
584
|
self._native_backend: TerminalBackend | None = None
|
|
421
585
|
self._native_backend_checked = False
|
|
422
586
|
|
|
587
|
+
# Best-effort Codex session binding without requiring the watcher process.
|
|
588
|
+
self._codex_link_last_attempt: dict[str, float] = {}
|
|
589
|
+
|
|
423
590
|
def compose(self) -> ComposeResult:
|
|
424
591
|
logger.debug("TerminalPanel.compose() started")
|
|
425
592
|
try:
|
|
@@ -501,11 +668,60 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
501
668
|
|
|
502
669
|
async def refresh_data(self) -> None:
|
|
503
670
|
async with self._refresh_lock:
|
|
671
|
+
# Check and close stale terminals if enabled
|
|
672
|
+
await self._close_stale_terminals_if_enabled()
|
|
673
|
+
|
|
504
674
|
if self._is_native_mode():
|
|
505
675
|
await self._refresh_native_data()
|
|
506
676
|
else:
|
|
507
677
|
await self._refresh_tmux_data()
|
|
508
678
|
|
|
679
|
+
async def _close_stale_terminals_if_enabled(self) -> None:
|
|
680
|
+
"""Close terminals that haven't been updated for the configured hours."""
|
|
681
|
+
try:
|
|
682
|
+
from ...config import ReAlignConfig
|
|
683
|
+
|
|
684
|
+
config = ReAlignConfig.load()
|
|
685
|
+
if not config.auto_close_stale_terminals:
|
|
686
|
+
return
|
|
687
|
+
|
|
688
|
+
stale_hours = config.stale_terminal_hours or 24
|
|
689
|
+
cutoff_time = datetime.now() - timedelta(hours=stale_hours)
|
|
690
|
+
|
|
691
|
+
# Get stale agents from database
|
|
692
|
+
from ...db import get_database
|
|
693
|
+
|
|
694
|
+
db = get_database(read_only=True)
|
|
695
|
+
all_agents = db.list_agents(status="active", limit=1000)
|
|
696
|
+
|
|
697
|
+
stale_agent_ids = set()
|
|
698
|
+
for agent in all_agents:
|
|
699
|
+
if agent.updated_at and agent.updated_at < cutoff_time:
|
|
700
|
+
stale_agent_ids.add(agent.id)
|
|
701
|
+
|
|
702
|
+
if not stale_agent_ids:
|
|
703
|
+
return
|
|
704
|
+
|
|
705
|
+
# Get current windows
|
|
706
|
+
if self._is_native_mode():
|
|
707
|
+
backend = await self._ensure_native_backend()
|
|
708
|
+
if not backend:
|
|
709
|
+
return
|
|
710
|
+
windows = await backend.list_tabs()
|
|
711
|
+
for w in windows:
|
|
712
|
+
if w.terminal_id in stale_agent_ids:
|
|
713
|
+
logger.info(f"Auto-closing stale terminal: {w.terminal_id}")
|
|
714
|
+
await backend.close_tab(w.session_id)
|
|
715
|
+
else:
|
|
716
|
+
windows = tmux_manager.list_inner_windows()
|
|
717
|
+
for w in windows:
|
|
718
|
+
if w.terminal_id in stale_agent_ids:
|
|
719
|
+
logger.info(f"Auto-closing stale terminal: {w.terminal_id}")
|
|
720
|
+
tmux_manager.kill_inner_window(w.window_id)
|
|
721
|
+
|
|
722
|
+
except Exception as e:
|
|
723
|
+
logger.debug(f"Error checking stale terminals: {e}")
|
|
724
|
+
|
|
509
725
|
async def _refresh_native_data(self) -> None:
|
|
510
726
|
"""Refresh data using native terminal backend."""
|
|
511
727
|
backend = await self._ensure_native_backend()
|
|
@@ -525,15 +741,26 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
525
741
|
logger.error(f"Failed to list native terminals: {e}")
|
|
526
742
|
return
|
|
527
743
|
|
|
744
|
+
# Best-effort: bind Codex session ids for native terminals (no watcher required).
|
|
745
|
+
try:
|
|
746
|
+
for w in windows:
|
|
747
|
+
if self._is_codex_window(w) and w.terminal_id:
|
|
748
|
+
self._maybe_link_codex_session_for_terminal(
|
|
749
|
+
terminal_id=w.terminal_id, created_at=w.created_at
|
|
750
|
+
)
|
|
751
|
+
except Exception:
|
|
752
|
+
pass
|
|
753
|
+
|
|
528
754
|
active_window_id = next(
|
|
529
755
|
(w.session_id for w in windows if w.active), None
|
|
530
756
|
)
|
|
531
757
|
if self._expanded_window_id and self._expanded_window_id != active_window_id:
|
|
532
758
|
self._expanded_window_id = None
|
|
533
759
|
|
|
534
|
-
#
|
|
760
|
+
# Titles (best-effort; native terminals only expose Claude session ids today)
|
|
535
761
|
claude_ids = [
|
|
536
|
-
w.claude_session_id
|
|
762
|
+
w.claude_session_id
|
|
763
|
+
for w in windows
|
|
537
764
|
if self._is_claude_window(w) and w.claude_session_id
|
|
538
765
|
]
|
|
539
766
|
titles = self._fetch_claude_session_titles(claude_ids)
|
|
@@ -542,7 +769,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
542
769
|
context_info_by_context_id: dict[str, tuple[list[str], int, int]] = {}
|
|
543
770
|
all_context_session_ids: set[str] = set()
|
|
544
771
|
for w in windows:
|
|
545
|
-
if not self.
|
|
772
|
+
if not self._supports_context(w) or not w.context_id:
|
|
546
773
|
continue
|
|
547
774
|
session_ids, session_count, event_count = self._get_loaded_context_info(
|
|
548
775
|
w.context_id
|
|
@@ -578,20 +805,29 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
578
805
|
windows = tmux_manager.list_inner_windows()
|
|
579
806
|
except Exception:
|
|
580
807
|
return
|
|
808
|
+
windows = [w for w in windows if not self._is_internal_tmux_window(w)]
|
|
809
|
+
|
|
810
|
+
# Best-effort: bind Codex session ids for tmux terminals (no watcher required).
|
|
811
|
+
try:
|
|
812
|
+
for w in windows:
|
|
813
|
+
if self._is_codex_window(w) and not w.session_id and w.terminal_id:
|
|
814
|
+
self._maybe_link_codex_session_for_terminal(
|
|
815
|
+
terminal_id=w.terminal_id, created_at=w.created_at
|
|
816
|
+
)
|
|
817
|
+
except Exception:
|
|
818
|
+
pass
|
|
581
819
|
|
|
582
820
|
active_window_id = next((w.window_id for w in windows if w.active), None)
|
|
583
821
|
if self._expanded_window_id and self._expanded_window_id != active_window_id:
|
|
584
822
|
self._expanded_window_id = None
|
|
585
823
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
]
|
|
589
|
-
titles = self._fetch_claude_session_titles(claude_ids)
|
|
824
|
+
session_ids = [w.session_id for w in windows if self._supports_context(w) and w.session_id]
|
|
825
|
+
titles = self._fetch_claude_session_titles(session_ids)
|
|
590
826
|
|
|
591
827
|
context_info_by_context_id: dict[str, tuple[list[str], int, int]] = {}
|
|
592
828
|
all_context_session_ids: set[str] = set()
|
|
593
829
|
for w in windows:
|
|
594
|
-
if not self.
|
|
830
|
+
if not self._supports_context(w) or not w.context_id:
|
|
595
831
|
continue
|
|
596
832
|
session_ids, session_count, event_count = self._get_loaded_context_info(
|
|
597
833
|
w.context_id
|
|
@@ -614,6 +850,10 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
614
850
|
return
|
|
615
851
|
|
|
616
852
|
def _fetch_claude_session_titles(self, session_ids: list[str]) -> dict[str, str]:
|
|
853
|
+
# Back-compat hook for tests and older call sites.
|
|
854
|
+
return self._fetch_session_titles(session_ids)
|
|
855
|
+
|
|
856
|
+
def _fetch_session_titles(self, session_ids: list[str]) -> dict[str, str]:
|
|
617
857
|
if not session_ids:
|
|
618
858
|
return {}
|
|
619
859
|
try:
|
|
@@ -701,7 +941,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
701
941
|
loaded_ids: list[str] = []
|
|
702
942
|
raw_sessions = 0
|
|
703
943
|
raw_events = 0
|
|
704
|
-
if self.
|
|
944
|
+
if self._supports_context(w) and w.context_id:
|
|
705
945
|
loaded_ids, raw_sessions, raw_events = context_info_by_context_id.get(
|
|
706
946
|
w.context_id, ([], 0, 0)
|
|
707
947
|
)
|
|
@@ -716,9 +956,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
716
956
|
)
|
|
717
957
|
)
|
|
718
958
|
|
|
719
|
-
can_toggle_ctx = bool(
|
|
720
|
-
self._is_claude_window(w) and w.context_id and (raw_sessions or raw_events)
|
|
721
|
-
)
|
|
959
|
+
can_toggle_ctx = bool(self._supports_context(w) and w.context_id and (raw_sessions or raw_events))
|
|
722
960
|
expanded = bool(w.active and w.session_id == self._expanded_window_id)
|
|
723
961
|
if w.active and can_toggle_ctx:
|
|
724
962
|
await row.mount(
|
|
@@ -741,7 +979,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
741
979
|
)
|
|
742
980
|
)
|
|
743
981
|
|
|
744
|
-
if w.active and self.
|
|
982
|
+
if w.active and self._supports_context(w) and w.context_id and expanded:
|
|
745
983
|
ctx = VerticalScroll(id=f"ctx-{safe}", classes="context-sessions")
|
|
746
984
|
await container.mount(ctx)
|
|
747
985
|
if loaded_ids:
|
|
@@ -790,7 +1028,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
790
1028
|
loaded_ids: list[str] = []
|
|
791
1029
|
raw_sessions = 0
|
|
792
1030
|
raw_events = 0
|
|
793
|
-
if self.
|
|
1031
|
+
if self._supports_context(w) and w.context_id:
|
|
794
1032
|
loaded_ids, raw_sessions, raw_events = context_info_by_context_id.get(
|
|
795
1033
|
w.context_id, ([], 0, 0)
|
|
796
1034
|
)
|
|
@@ -803,9 +1041,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
803
1041
|
classes=switch_classes,
|
|
804
1042
|
)
|
|
805
1043
|
)
|
|
806
|
-
can_toggle_ctx = bool(
|
|
807
|
-
self._is_claude_window(w) and w.context_id and (raw_sessions or raw_events)
|
|
808
|
-
)
|
|
1044
|
+
can_toggle_ctx = bool(self._supports_context(w) and w.context_id and (raw_sessions or raw_events))
|
|
809
1045
|
expanded = bool(w.active and w.window_id == self._expanded_window_id)
|
|
810
1046
|
if w.active and can_toggle_ctx:
|
|
811
1047
|
await row.mount(
|
|
@@ -827,7 +1063,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
827
1063
|
)
|
|
828
1064
|
)
|
|
829
1065
|
|
|
830
|
-
if w.active and self.
|
|
1066
|
+
if w.active and self._supports_context(w) and w.context_id and expanded:
|
|
831
1067
|
ctx = VerticalScroll(id=f"ctx-{safe}", classes="context-sessions")
|
|
832
1068
|
await container.mount(ctx)
|
|
833
1069
|
if loaded_ids:
|
|
@@ -868,9 +1104,26 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
868
1104
|
raw_events: int = 0,
|
|
869
1105
|
) -> str | Text:
|
|
870
1106
|
"""Generate label for native terminal window."""
|
|
871
|
-
if not self.
|
|
1107
|
+
if not self._supports_context(w):
|
|
872
1108
|
return Text(w.name, no_wrap=True, overflow="ellipsis")
|
|
873
1109
|
|
|
1110
|
+
if self._is_codex_window(w):
|
|
1111
|
+
details = Text(no_wrap=True, overflow="ellipsis")
|
|
1112
|
+
details.append("Codex")
|
|
1113
|
+
details.append("\n")
|
|
1114
|
+
detail_line = "[Codex]"
|
|
1115
|
+
if w.active:
|
|
1116
|
+
loaded_count = raw_sessions + raw_events
|
|
1117
|
+
detail_line = f"{detail_line} | loaded context: {loaded_count}"
|
|
1118
|
+
else:
|
|
1119
|
+
detail_line = (
|
|
1120
|
+
f"{detail_line} · {self._format_context_summary(raw_sessions, raw_events)}"
|
|
1121
|
+
)
|
|
1122
|
+
if w.metadata.get("no_track") == "1":
|
|
1123
|
+
detail_line = f"{detail_line} [NT]"
|
|
1124
|
+
details.append(detail_line, style="dim not bold")
|
|
1125
|
+
return details
|
|
1126
|
+
|
|
874
1127
|
title = titles.get(w.claude_session_id or "", "").strip() if w.claude_session_id else ""
|
|
875
1128
|
header = title or ("Claude" if w.claude_session_id else "New Claude")
|
|
876
1129
|
|
|
@@ -902,9 +1155,32 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
902
1155
|
raw_events: int = 0,
|
|
903
1156
|
) -> str | Text:
|
|
904
1157
|
"""Generate label for tmux window."""
|
|
905
|
-
if not self.
|
|
1158
|
+
if not self._supports_context(w):
|
|
906
1159
|
return Text(w.window_name, no_wrap=True, overflow="ellipsis")
|
|
907
1160
|
|
|
1161
|
+
if self._is_codex_window(w):
|
|
1162
|
+
title = titles.get(w.session_id or "", "").strip() if w.session_id else ""
|
|
1163
|
+
header = title or ("Codex" if w.session_id else "New Codex")
|
|
1164
|
+
|
|
1165
|
+
details = Text(no_wrap=True, overflow="ellipsis")
|
|
1166
|
+
details.append(header)
|
|
1167
|
+
details.append("\n")
|
|
1168
|
+
|
|
1169
|
+
detail_line = "[Codex]"
|
|
1170
|
+
if w.session_id:
|
|
1171
|
+
detail_line = f"{detail_line} #{self._short_id(w.session_id)}"
|
|
1172
|
+
if w.active:
|
|
1173
|
+
loaded_count = raw_sessions + raw_events
|
|
1174
|
+
detail_line = f"{detail_line} | loaded context: {loaded_count}"
|
|
1175
|
+
else:
|
|
1176
|
+
detail_line = (
|
|
1177
|
+
f"{detail_line} · {self._format_context_summary(raw_sessions, raw_events)}"
|
|
1178
|
+
)
|
|
1179
|
+
if w.no_track:
|
|
1180
|
+
detail_line = f"{detail_line} [NT]"
|
|
1181
|
+
details.append(detail_line, style="dim not bold")
|
|
1182
|
+
return details
|
|
1183
|
+
|
|
908
1184
|
title = titles.get(w.session_id or "", "").strip() if w.session_id else ""
|
|
909
1185
|
header = title or ("Claude" if w.session_id else "New Claude")
|
|
910
1186
|
|
|
@@ -971,7 +1247,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
971
1247
|
if agent_type == "claude":
|
|
972
1248
|
await self._create_claude_terminal(workspace, skip_permissions=skip_permissions, no_track=no_track)
|
|
973
1249
|
elif agent_type == "codex":
|
|
974
|
-
await self._create_codex_terminal(workspace)
|
|
1250
|
+
await self._create_codex_terminal(workspace, no_track=no_track)
|
|
975
1251
|
elif agent_type == "opencode":
|
|
976
1252
|
await self._create_opencode_terminal(workspace)
|
|
977
1253
|
elif agent_type == "zsh":
|
|
@@ -1064,6 +1340,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1064
1340
|
terminal_id=terminal_id,
|
|
1065
1341
|
provider="claude",
|
|
1066
1342
|
context_id=context_id,
|
|
1343
|
+
no_track=no_track,
|
|
1067
1344
|
)
|
|
1068
1345
|
if not created:
|
|
1069
1346
|
self.app.notify("Failed to open Claude terminal", title="Terminal", severity="error")
|
|
@@ -1119,16 +1396,54 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1119
1396
|
except Exception:
|
|
1120
1397
|
pass
|
|
1121
1398
|
|
|
1122
|
-
async def _create_codex_terminal(self, workspace: str) -> None:
|
|
1399
|
+
async def _create_codex_terminal(self, workspace: str, *, no_track: bool = False) -> None:
|
|
1123
1400
|
"""Create a new Codex terminal."""
|
|
1401
|
+
terminal_id = tmux_manager.new_terminal_id()
|
|
1402
|
+
context_id = tmux_manager.new_context_id("cx")
|
|
1403
|
+
|
|
1404
|
+
# Use per-terminal CODEX_HOME so sessions/config are isolated and binding is deterministic.
|
|
1405
|
+
try:
|
|
1406
|
+
from ...codex_home import prepare_codex_home
|
|
1407
|
+
|
|
1408
|
+
codex_home = prepare_codex_home(terminal_id)
|
|
1409
|
+
except Exception:
|
|
1410
|
+
codex_home = None
|
|
1411
|
+
|
|
1412
|
+
env = {
|
|
1413
|
+
tmux_manager.ENV_TERMINAL_ID: terminal_id,
|
|
1414
|
+
tmux_manager.ENV_TERMINAL_PROVIDER: "codex",
|
|
1415
|
+
tmux_manager.ENV_CONTEXT_ID: context_id,
|
|
1416
|
+
}
|
|
1417
|
+
if codex_home is not None:
|
|
1418
|
+
env["CODEX_HOME"] = str(codex_home)
|
|
1419
|
+
if no_track:
|
|
1420
|
+
env["ALINE_NO_TRACK"] = "1"
|
|
1421
|
+
|
|
1422
|
+
# Persist agent early so the watcher can bind the Codex session file back to this terminal.
|
|
1423
|
+
try:
|
|
1424
|
+
from ...db import get_database
|
|
1425
|
+
|
|
1426
|
+
db = get_database(read_only=False)
|
|
1427
|
+
db.get_or_create_agent(
|
|
1428
|
+
terminal_id,
|
|
1429
|
+
provider="codex",
|
|
1430
|
+
session_type="codex",
|
|
1431
|
+
context_id=context_id,
|
|
1432
|
+
cwd=workspace,
|
|
1433
|
+
project_dir=workspace,
|
|
1434
|
+
source="dashboard",
|
|
1435
|
+
)
|
|
1436
|
+
except Exception:
|
|
1437
|
+
pass
|
|
1438
|
+
|
|
1124
1439
|
if self._is_native_mode():
|
|
1125
1440
|
backend = await self._ensure_native_backend()
|
|
1126
1441
|
if backend:
|
|
1127
|
-
terminal_id = tmux_manager.new_terminal_id()
|
|
1128
1442
|
session_id = await backend.create_tab(
|
|
1129
1443
|
command="codex",
|
|
1130
1444
|
terminal_id=terminal_id,
|
|
1131
1445
|
name="Codex",
|
|
1446
|
+
env=env,
|
|
1132
1447
|
cwd=workspace,
|
|
1133
1448
|
)
|
|
1134
1449
|
if not session_id:
|
|
@@ -1141,7 +1456,14 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1141
1456
|
command = self._command_in_directory(
|
|
1142
1457
|
tmux_manager.zsh_run_and_keep_open("codex"), workspace
|
|
1143
1458
|
)
|
|
1144
|
-
created = tmux_manager.create_inner_window(
|
|
1459
|
+
created = tmux_manager.create_inner_window(
|
|
1460
|
+
"codex",
|
|
1461
|
+
tmux_manager.shell_command_with_env(command, env),
|
|
1462
|
+
terminal_id=terminal_id,
|
|
1463
|
+
provider="codex",
|
|
1464
|
+
context_id=context_id,
|
|
1465
|
+
no_track=no_track,
|
|
1466
|
+
)
|
|
1145
1467
|
if not created:
|
|
1146
1468
|
self.app.notify("Failed to open Codex terminal", title="Terminal", severity="error")
|
|
1147
1469
|
|
|
@@ -318,7 +318,6 @@ class WatcherPanel(Container, can_focus=True):
|
|
|
318
318
|
("Claude", "~/.claude/projects/", config.auto_detect_claude),
|
|
319
319
|
("Codex", "~/.codex/sessions/", config.auto_detect_codex),
|
|
320
320
|
("Gemini", "~/.gemini/tmp/", config.auto_detect_gemini),
|
|
321
|
-
("Antigravity", "~/.gemini/antigravity/brain/", config.auto_detect_antigravity),
|
|
322
321
|
]
|
|
323
322
|
except Exception:
|
|
324
323
|
monitored_paths = []
|
|
@@ -426,7 +425,6 @@ class WatcherPanel(Container, can_focus=True):
|
|
|
426
425
|
"claude": "Claude",
|
|
427
426
|
"codex": "Codex",
|
|
428
427
|
"gemini": "Gemini",
|
|
429
|
-
"antigravity": "Antigravity",
|
|
430
428
|
}
|
|
431
429
|
source = source_map.get(session_type, session_type)
|
|
432
430
|
project = Path(workspace).name if workspace else "-"
|
realign/db/sqlite_db.py
CHANGED
|
@@ -999,8 +999,7 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
999
999
|
# - If the caller requested regeneration (`skip_dedup=True`), we must allow requeue.
|
|
1000
1000
|
# - If the DB is missing the corresponding turn row (historical bug / manual DB edits),
|
|
1001
1001
|
# requeue so the system can self-heal instead of getting stuck in a "done but missing" state.
|
|
1002
|
-
|
|
1003
|
-
requeue_done = bool(skip_dedup) or (session_type or "").lower() in ("antigravity",)
|
|
1002
|
+
requeue_done = bool(skip_dedup)
|
|
1004
1003
|
try:
|
|
1005
1004
|
conn = self._get_connection()
|
|
1006
1005
|
row = conn.execute(
|
|
@@ -1463,6 +1462,82 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
1463
1462
|
except Exception:
|
|
1464
1463
|
return 0
|
|
1465
1464
|
|
|
1465
|
+
def requeue_failed_jobs(
|
|
1466
|
+
self,
|
|
1467
|
+
*,
|
|
1468
|
+
kinds: Optional[List[str]] = None,
|
|
1469
|
+
) -> Tuple[int, List[Dict[str, Any]]]:
|
|
1470
|
+
"""
|
|
1471
|
+
Requeue all failed jobs, optionally filtering by kind.
|
|
1472
|
+
|
|
1473
|
+
Returns:
|
|
1474
|
+
(count, jobs) - number of jobs requeued and their details
|
|
1475
|
+
"""
|
|
1476
|
+
conn = self._get_connection()
|
|
1477
|
+
try:
|
|
1478
|
+
# First, get the failed jobs
|
|
1479
|
+
where_clauses: list[str] = ["status = 'failed'"]
|
|
1480
|
+
params: list[Any] = []
|
|
1481
|
+
if kinds:
|
|
1482
|
+
placeholders = ",".join(["?"] * len(kinds))
|
|
1483
|
+
where_clauses.append(f"kind IN ({placeholders})")
|
|
1484
|
+
params.extend(kinds)
|
|
1485
|
+
|
|
1486
|
+
where_sql = "WHERE " + " AND ".join(where_clauses)
|
|
1487
|
+
|
|
1488
|
+
rows = conn.execute(
|
|
1489
|
+
f"""
|
|
1490
|
+
SELECT id, kind, dedupe_key, payload, last_error, attempts
|
|
1491
|
+
FROM jobs
|
|
1492
|
+
{where_sql}
|
|
1493
|
+
ORDER BY updated_at DESC
|
|
1494
|
+
""",
|
|
1495
|
+
tuple(params),
|
|
1496
|
+
).fetchall()
|
|
1497
|
+
|
|
1498
|
+
if not rows:
|
|
1499
|
+
return 0, []
|
|
1500
|
+
|
|
1501
|
+
jobs_info: List[Dict[str, Any]] = []
|
|
1502
|
+
for row in rows:
|
|
1503
|
+
payload_raw = row["payload"] or "{}"
|
|
1504
|
+
try:
|
|
1505
|
+
payload_obj = json.loads(payload_raw)
|
|
1506
|
+
except Exception:
|
|
1507
|
+
payload_obj = {}
|
|
1508
|
+
jobs_info.append({
|
|
1509
|
+
"id": str(row["id"]),
|
|
1510
|
+
"kind": row["kind"],
|
|
1511
|
+
"dedupe_key": row["dedupe_key"],
|
|
1512
|
+
"payload": payload_obj,
|
|
1513
|
+
"last_error": row["last_error"],
|
|
1514
|
+
"attempts": row["attempts"],
|
|
1515
|
+
})
|
|
1516
|
+
|
|
1517
|
+
# Requeue them
|
|
1518
|
+
conn.execute(
|
|
1519
|
+
f"""
|
|
1520
|
+
UPDATE jobs
|
|
1521
|
+
SET status = 'queued',
|
|
1522
|
+
attempts = 0,
|
|
1523
|
+
next_run_at = datetime('now'),
|
|
1524
|
+
last_error = NULL,
|
|
1525
|
+
locked_until = NULL,
|
|
1526
|
+
locked_by = NULL,
|
|
1527
|
+
updated_at = datetime('now')
|
|
1528
|
+
{where_sql}
|
|
1529
|
+
""",
|
|
1530
|
+
tuple(params),
|
|
1531
|
+
)
|
|
1532
|
+
conn.commit()
|
|
1533
|
+
|
|
1534
|
+
return len(jobs_info), jobs_info
|
|
1535
|
+
except sqlite3.OperationalError:
|
|
1536
|
+
return 0, []
|
|
1537
|
+
except Exception:
|
|
1538
|
+
conn.rollback()
|
|
1539
|
+
return 0, []
|
|
1540
|
+
|
|
1466
1541
|
def update_turn_summary(
|
|
1467
1542
|
self,
|
|
1468
1543
|
turn_id: str,
|