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.
Files changed (40) hide show
  1. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/METADATA +1 -1
  2. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/RECORD +38 -37
  3. realign/__init__.py +1 -1
  4. realign/adapters/__init__.py +0 -3
  5. realign/adapters/codex.py +14 -9
  6. realign/cli.py +42 -236
  7. realign/codex_detector.py +72 -32
  8. realign/codex_home.py +85 -0
  9. realign/codex_terminal_linker.py +172 -0
  10. realign/commands/__init__.py +2 -2
  11. realign/commands/add.py +89 -9
  12. realign/commands/doctor.py +495 -0
  13. realign/commands/export_shares.py +154 -226
  14. realign/commands/init.py +66 -4
  15. realign/commands/watcher.py +30 -80
  16. realign/config.py +9 -46
  17. realign/dashboard/app.py +7 -11
  18. realign/dashboard/screens/event_detail.py +0 -3
  19. realign/dashboard/screens/session_detail.py +0 -1
  20. realign/dashboard/tmux_manager.py +129 -4
  21. realign/dashboard/widgets/config_panel.py +175 -241
  22. realign/dashboard/widgets/events_table.py +71 -128
  23. realign/dashboard/widgets/sessions_table.py +77 -136
  24. realign/dashboard/widgets/terminal_panel.py +349 -27
  25. realign/dashboard/widgets/watcher_panel.py +0 -2
  26. realign/db/sqlite_db.py +77 -2
  27. realign/events/event_summarizer.py +76 -35
  28. realign/events/session_summarizer.py +73 -32
  29. realign/hooks.py +334 -647
  30. realign/llm_client.py +201 -520
  31. realign/triggers/__init__.py +0 -2
  32. realign/triggers/next_turn_trigger.py +4 -5
  33. realign/triggers/registry.py +1 -4
  34. realign/watcher_core.py +53 -35
  35. realign/adapters/antigravity.py +0 -159
  36. realign/triggers/antigravity_trigger.py +0 -140
  37. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/WHEEL +0 -0
  38. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/entry_points.txt +0 -0
  39. {aline_ai-0.6.2.dist-info → aline_ai-0.6.4.dist-info}/licenses/LICENSE +0 -0
  40. {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
- is_claude_tagged = (w.provider == "claude") or (w.session_type == "claude")
394
- return bool(is_claude_tagged and (w.terminal_id or w.context_id))
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
- # Get Claude session IDs for title lookup
760
+ # Titles (best-effort; native terminals only expose Claude session ids today)
535
761
  claude_ids = [
536
- w.claude_session_id for w in windows
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._is_claude_window(w) or not w.context_id:
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
- claude_ids = [
587
- w.session_id for w in windows if self._is_claude_window(w) and w.session_id
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._is_claude_window(w) or not w.context_id:
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._is_claude_window(w) and w.context_id:
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._is_claude_window(w) and w.context_id and expanded:
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._is_claude_window(w) and w.context_id:
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._is_claude_window(w) and w.context_id and expanded:
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._is_claude_window(w):
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._is_claude_window(w):
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("codex", command)
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
- # - For antigravity, turns may update-in-place, so allow requeue even if done.
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,