aline-ai 0.6.4__py3-none-any.whl → 0.6.6__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 (41) hide show
  1. {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/METADATA +1 -1
  2. {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/RECORD +41 -34
  3. realign/__init__.py +1 -1
  4. realign/agent_names.py +79 -0
  5. realign/claude_hooks/stop_hook.py +3 -0
  6. realign/claude_hooks/terminal_state.py +11 -0
  7. realign/claude_hooks/user_prompt_submit_hook.py +3 -0
  8. realign/cli.py +62 -0
  9. realign/codex_detector.py +1 -1
  10. realign/codex_home.py +46 -15
  11. realign/codex_terminal_linker.py +18 -7
  12. realign/commands/agent.py +109 -0
  13. realign/commands/doctor.py +3 -1
  14. realign/commands/export_shares.py +297 -0
  15. realign/commands/search.py +58 -29
  16. realign/dashboard/app.py +9 -158
  17. realign/dashboard/clipboard.py +54 -0
  18. realign/dashboard/screens/__init__.py +4 -0
  19. realign/dashboard/screens/agent_detail.py +333 -0
  20. realign/dashboard/screens/create_agent_info.py +133 -0
  21. realign/dashboard/screens/event_detail.py +6 -27
  22. realign/dashboard/styles/dashboard.tcss +67 -0
  23. realign/dashboard/tmux_manager.py +49 -8
  24. realign/dashboard/widgets/__init__.py +2 -0
  25. realign/dashboard/widgets/agents_panel.py +1129 -0
  26. realign/dashboard/widgets/config_panel.py +17 -11
  27. realign/dashboard/widgets/events_table.py +4 -27
  28. realign/dashboard/widgets/sessions_table.py +4 -27
  29. realign/dashboard/widgets/terminal_panel.py +109 -31
  30. realign/db/base.py +27 -0
  31. realign/db/locks.py +4 -0
  32. realign/db/schema.py +53 -2
  33. realign/db/sqlite_db.py +185 -2
  34. realign/events/agent_summarizer.py +157 -0
  35. realign/events/session_summarizer.py +25 -0
  36. realign/watcher_core.py +60 -3
  37. realign/worker_core.py +24 -1
  38. {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/WHEEL +0 -0
  39. {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/entry_points.txt +0 -0
  40. {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/licenses/LICENSE +0 -0
  41. {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/top_level.txt +0 -0
@@ -393,26 +393,32 @@ class ConfigPanel(Static):
393
393
  self.app.notify(f"Error saving setting: {e}", title="Config", severity="error")
394
394
 
395
395
  def _handle_doctor(self) -> None:
396
- """Run aline doctor command in background."""
396
+ """Run aline doctor directly in background thread."""
397
397
  self.app.notify("Running Aline Doctor...", title="Doctor")
398
398
 
399
399
  def do_doctor():
400
400
  try:
401
- import subprocess
402
- result = subprocess.run(
403
- ["aline", "doctor"],
404
- capture_output=True,
405
- text=True,
406
- timeout=60,
407
- )
408
- if result.returncode == 0:
401
+ import contextlib
402
+ import io
403
+ from ...commands.doctor import run_doctor
404
+
405
+ # Suppress Rich console output (would corrupt TUI)
406
+ with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
407
+ exit_code = run_doctor(
408
+ restart_daemons=True,
409
+ start_if_not_running=False,
410
+ verbose=False,
411
+ clear_cache=True,
412
+ auto_fix=True,
413
+ )
414
+
415
+ if exit_code == 0:
409
416
  self.app.call_from_thread(
410
417
  self.app.notify, "Doctor completed successfully", title="Doctor"
411
418
  )
412
419
  else:
413
- error_msg = result.stderr.strip() if result.stderr else "Unknown error"
414
420
  self.app.call_from_thread(
415
- self.app.notify, f"Doctor failed: {error_msg}", title="Doctor", severity="error"
421
+ self.app.notify, "Doctor completed with errors", title="Doctor", severity="error"
416
422
  )
417
423
  except Exception as e:
418
424
  self.app.call_from_thread(
@@ -3,9 +3,6 @@
3
3
  import contextlib
4
4
  import io
5
5
  import json
6
- import os
7
- import shutil
8
- import subprocess
9
6
  import traceback
10
7
  from datetime import datetime
11
8
  from typing import Optional, Set
@@ -20,6 +17,7 @@ from textual.worker import Worker, WorkerState
20
17
  from textual.widgets import Button, DataTable, Static
21
18
 
22
19
  from ...logging_config import setup_logger
20
+ from ..clipboard import copy_text
23
21
  from .openable_table import OpenableDataTable
24
22
 
25
23
  logger = setup_logger("realign.dashboard.events", "dashboard.log")
@@ -613,33 +611,12 @@ class EventsTable(Container):
613
611
 
614
612
  # Build copy text
615
613
  if slack_message:
616
- copy_text = str(slack_message) + "\n\n" + str(share_link)
614
+ text_to_copy = str(slack_message) + "\n\n" + str(share_link)
617
615
  else:
618
- copy_text = str(share_link)
616
+ text_to_copy = str(share_link)
619
617
 
620
618
  # Copy to clipboard
621
- copied = False
622
- if os.environ.get("TMUX") and shutil.which("pbcopy"):
623
- try:
624
- copied = (
625
- subprocess.run(
626
- ["pbcopy"],
627
- input=copy_text,
628
- text=True,
629
- capture_output=False,
630
- check=False,
631
- ).returncode
632
- == 0
633
- )
634
- except Exception:
635
- copied = False
636
-
637
- if not copied:
638
- try:
639
- self.app.copy_to_clipboard(copy_text)
640
- copied = True
641
- except Exception:
642
- copied = False
619
+ copied = copy_text(self.app, text_to_copy)
643
620
 
644
621
  suffix = " (copied to clipboard)" if copied else ""
645
622
  self.app.notify(f"Share link created{suffix}", title="Share", timeout=4)
@@ -3,9 +3,6 @@
3
3
  import contextlib
4
4
  import io
5
5
  import json
6
- import os
7
- import shutil
8
- import subprocess
9
6
  import traceback
10
7
  from datetime import datetime
11
8
  from pathlib import Path
@@ -20,6 +17,7 @@ from textual.worker import Worker, WorkerState
20
17
  from textual.widgets import Button, DataTable, Select, Static
21
18
 
22
19
  from ...logging_config import setup_logger
20
+ from ..clipboard import copy_text
23
21
  from .openable_table import OpenableDataTable
24
22
 
25
23
  logger = setup_logger("realign.dashboard.sessions", "dashboard.log")
@@ -685,33 +683,12 @@ class SessionsTable(Container):
685
683
 
686
684
  # Build copy text
687
685
  if slack_message:
688
- copy_text = str(slack_message) + "\n\n" + str(share_link)
686
+ text_to_copy = str(slack_message) + "\n\n" + str(share_link)
689
687
  else:
690
- copy_text = str(share_link)
688
+ text_to_copy = str(share_link)
691
689
 
692
690
  # Copy to clipboard
693
- copied = False
694
- if os.environ.get("TMUX") and shutil.which("pbcopy"):
695
- try:
696
- copied = (
697
- subprocess.run(
698
- ["pbcopy"],
699
- input=copy_text,
700
- text=True,
701
- capture_output=False,
702
- check=False,
703
- ).returncode
704
- == 0
705
- )
706
- except Exception:
707
- copied = False
708
-
709
- if not copied:
710
- try:
711
- self.app.copy_to_clipboard(copy_text)
712
- copied = True
713
- except Exception:
714
- copied = False
691
+ copied = copy_text(self.app, text_to_copy)
715
692
 
716
693
  suffix = " (copied to clipboard)" if copied else ""
717
694
  self.app.notify(f"Share link created{suffix}", title="Share", timeout=4)
@@ -456,7 +456,7 @@ class TerminalPanel(Container, can_focus=True):
456
456
  try:
457
457
  from ...codex_terminal_linker import read_codex_session_meta
458
458
  from ...db import get_database
459
- from ...codex_home import codex_sessions_dir_for_terminal
459
+ from ...codex_home import codex_sessions_dir_for_terminal_or_agent
460
460
  except Exception:
461
461
  return
462
462
 
@@ -474,9 +474,12 @@ class TerminalPanel(Container, can_focus=True):
474
474
  return
475
475
 
476
476
  candidates: list[Path] = []
477
- sessions_root = codex_sessions_dir_for_terminal(terminal_id)
477
+ agent_info_id: str | None = None
478
+ if (agent.source or "").startswith("agent:"):
479
+ agent_info_id = agent.source[6:]
480
+ sessions_root = codex_sessions_dir_for_terminal_or_agent(terminal_id, agent_info_id)
478
481
  if sessions_root.exists():
479
- # Deterministic: isolated per-terminal CODEX_HOME.
482
+ # Deterministic: isolated per-terminal/per-agent CODEX_HOME.
480
483
  try:
481
484
  candidates = list(sessions_root.rglob("rollout-*.jsonl"))
482
485
  except Exception:
@@ -544,6 +547,9 @@ class TerminalPanel(Container, can_focus=True):
544
547
  return
545
548
 
546
549
  try:
550
+ source = "dashboard:auto-link"
551
+ if (agent.source or "").startswith("agent:"):
552
+ source = agent.source or source
547
553
  db.update_agent(
548
554
  terminal_id,
549
555
  provider="codex",
@@ -552,8 +558,13 @@ class TerminalPanel(Container, can_focus=True):
552
558
  transcript_path=str(best),
553
559
  cwd=cwd,
554
560
  project_dir=cwd,
555
- source="dashboard:auto-link",
561
+ source=source,
556
562
  )
563
+ if agent_info_id:
564
+ try:
565
+ db.update_session_agent_id(best.stem, agent_info_id)
566
+ except Exception:
567
+ pass
557
568
  except Exception:
558
569
  return
559
570
 
@@ -592,10 +603,16 @@ class TerminalPanel(Container, can_focus=True):
592
603
  try:
593
604
  controls_enabled = self.supported()
594
605
  with Horizontal(classes="summary"):
606
+ yield Button(
607
+ "+ New Agent",
608
+ id="quick-new-agent",
609
+ variant="primary",
610
+ disabled=not controls_enabled,
611
+ )
595
612
  yield Button(
596
613
  "+ Create",
597
614
  id="new-agent",
598
- variant="primary",
615
+ variant="default",
599
616
  disabled=not controls_enabled,
600
617
  )
601
618
  with Vertical(id="terminals", classes="list"):
@@ -668,13 +685,17 @@ class TerminalPanel(Container, can_focus=True):
668
685
 
669
686
  async def refresh_data(self) -> None:
670
687
  async with self._refresh_lock:
688
+ t_start = time.time()
671
689
  # Check and close stale terminals if enabled
672
690
  await self._close_stale_terminals_if_enabled()
691
+ logger.debug(f"[PERF] _close_stale_terminals_if_enabled: {time.time() - t_start:.3f}s")
673
692
 
693
+ t_refresh = time.time()
674
694
  if self._is_native_mode():
675
695
  await self._refresh_native_data()
676
696
  else:
677
697
  await self._refresh_tmux_data()
698
+ logger.debug(f"[PERF] total refresh: {time.time() - t_start:.3f}s")
678
699
 
679
700
  async def _close_stale_terminals_if_enabled(self) -> None:
680
701
  """Close terminals that haven't been updated for the configured hours."""
@@ -741,15 +762,13 @@ class TerminalPanel(Container, can_focus=True):
741
762
  logger.error(f"Failed to list native terminals: {e}")
742
763
  return
743
764
 
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
765
+ # Yield to event loop to keep UI responsive
766
+ await asyncio.sleep(0)
767
+
768
+ # NOTE: _maybe_link_codex_session_for_terminal is intentionally skipped here
769
+ # because it performs expensive file system scans (find_codex_sessions_for_project)
770
+ # that can take minutes with many session files. Codex session linking is handled
771
+ # by the watcher process instead.
753
772
 
754
773
  active_window_id = next(
755
774
  (w.session_id for w in windows if w.active), None
@@ -765,6 +784,9 @@ class TerminalPanel(Container, can_focus=True):
765
784
  ]
766
785
  titles = self._fetch_claude_session_titles(claude_ids)
767
786
 
787
+ # Yield to event loop after DB query
788
+ await asyncio.sleep(0)
789
+
768
790
  # Get context info
769
791
  context_info_by_context_id: dict[str, tuple[list[str], int, int]] = {}
770
792
  all_context_session_ids: set[str] = set()
@@ -793,6 +815,7 @@ class TerminalPanel(Container, can_focus=True):
793
815
 
794
816
  async def _refresh_tmux_data(self) -> None:
795
817
  """Refresh data using tmux backend."""
818
+ t0 = time.time()
796
819
  try:
797
820
  supported = self.supported()
798
821
  except Exception:
@@ -806,24 +829,29 @@ class TerminalPanel(Container, can_focus=True):
806
829
  except Exception:
807
830
  return
808
831
  windows = [w for w in windows if not self._is_internal_tmux_window(w)]
832
+ logger.debug(f"[PERF] list_inner_windows: {time.time() - t0:.3f}s")
809
833
 
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
834
+ # Yield to event loop to keep UI responsive
835
+ await asyncio.sleep(0)
836
+
837
+ # NOTE: _maybe_link_codex_session_for_terminal is intentionally skipped here
838
+ # because it performs expensive file system scans (find_codex_sessions_for_project)
839
+ # that can take minutes with many session files. Codex session linking is handled
840
+ # by the watcher process instead.
819
841
 
820
842
  active_window_id = next((w.window_id for w in windows if w.active), None)
821
843
  if self._expanded_window_id and self._expanded_window_id != active_window_id:
822
844
  self._expanded_window_id = None
823
845
 
846
+ t1 = time.time()
824
847
  session_ids = [w.session_id for w in windows if self._supports_context(w) and w.session_id]
825
848
  titles = self._fetch_claude_session_titles(session_ids)
849
+ logger.debug(f"[PERF] fetch_claude_session_titles: {time.time() - t1:.3f}s")
850
+
851
+ # Yield to event loop after DB query
852
+ await asyncio.sleep(0)
826
853
 
854
+ t2 = time.time()
827
855
  context_info_by_context_id: dict[str, tuple[list[str], int, int]] = {}
828
856
  all_context_session_ids: set[str] = set()
829
857
  for w in windows:
@@ -840,14 +868,21 @@ class TerminalPanel(Container, can_focus=True):
840
868
  event_count,
841
869
  )
842
870
  all_context_session_ids.update(session_ids)
871
+ # Yield periodically during context info gathering
872
+ await asyncio.sleep(0)
873
+ logger.debug(f"[PERF] get_loaded_context_info loop: {time.time() - t2:.3f}s")
843
874
 
875
+ t3 = time.time()
844
876
  if all_context_session_ids:
845
877
  titles.update(self._fetch_claude_session_titles(sorted(all_context_session_ids)))
878
+ logger.debug(f"[PERF] fetch context session titles: {time.time() - t3:.3f}s")
846
879
 
880
+ t4 = time.time()
847
881
  try:
848
882
  await self._render_terminals_tmux(windows, titles, context_info_by_context_id)
849
883
  except Exception:
850
884
  return
885
+ logger.debug(f"[PERF] render_terminals_tmux: {time.time() - t4:.3f}s")
851
886
 
852
887
  def _fetch_claude_session_titles(self, session_ids: list[str]) -> dict[str, str]:
853
888
  # Back-compat hook for tests and older call sites.
@@ -1226,19 +1261,43 @@ class TerminalPanel(Container, can_focus=True):
1226
1261
  """Wrap a command to run in a specific directory."""
1227
1262
  return f"cd {shlex.quote(directory)} && {command}"
1228
1263
 
1264
+ async def _quick_create_claude_agent(self) -> None:
1265
+ """Quickly create a new Claude Code terminal with default settings.
1266
+
1267
+ Uses the last workspace (or cwd) with normal permissions and tracking enabled.
1268
+ """
1269
+ from ..screens.create_agent import _load_last_workspace
1270
+
1271
+ workspace = _load_last_workspace()
1272
+ self.run_worker(
1273
+ self._create_agent("claude", workspace, skip_permissions=False, no_track=False),
1274
+ group="terminal-panel-create",
1275
+ exclusive=True,
1276
+ )
1277
+
1229
1278
  def _on_create_agent_result(self, result: tuple[str, str, bool, bool] | None) -> None:
1230
1279
  """Handle the result from CreateAgentScreen modal."""
1231
1280
  if result is None:
1232
1281
  return
1233
1282
 
1234
1283
  agent_type, workspace, skip_permissions, no_track = result
1235
- self.run_worker(
1236
- self._create_agent(
1237
- agent_type, workspace, skip_permissions=skip_permissions, no_track=no_track
1238
- ),
1239
- group="terminal-panel-create",
1240
- exclusive=True,
1241
- )
1284
+
1285
+ # Capture self reference for use in the deferred callback
1286
+ panel = self
1287
+
1288
+ # Use app.call_later to defer worker creation until after the modal is dismissed.
1289
+ # This ensures the modal screen is fully closed before the worker starts,
1290
+ # preventing UI update conflicts between modal closing and terminal panel refresh.
1291
+ def start_worker() -> None:
1292
+ panel.run_worker(
1293
+ panel._create_agent(
1294
+ agent_type, workspace, skip_permissions=skip_permissions, no_track=no_track
1295
+ ),
1296
+ group="terminal-panel-create",
1297
+ exclusive=True,
1298
+ )
1299
+
1300
+ self.app.call_later(start_worker)
1242
1301
 
1243
1302
  async def _create_agent(
1244
1303
  self, agent_type: str, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
@@ -1252,7 +1311,14 @@ class TerminalPanel(Container, can_focus=True):
1252
1311
  await self._create_opencode_terminal(workspace)
1253
1312
  elif agent_type == "zsh":
1254
1313
  await self._create_zsh_terminal(workspace)
1255
- await self.refresh_data()
1314
+ # Schedule refresh in a separate worker to avoid blocking UI.
1315
+ # The refresh involves slow synchronous operations (DB queries, file scans)
1316
+ # that would otherwise freeze the dashboard.
1317
+ self.run_worker(
1318
+ self.refresh_data(),
1319
+ group="terminal-panel-refresh",
1320
+ exclusive=True,
1321
+ )
1256
1322
 
1257
1323
  async def _create_claude_terminal(
1258
1324
  self, workspace: str, *, skip_permissions: bool = False, no_track: bool = False
@@ -1495,6 +1561,8 @@ class TerminalPanel(Container, can_focus=True):
1495
1561
 
1496
1562
  async def _create_zsh_terminal(self, workspace: str) -> None:
1497
1563
  """Create a new zsh terminal."""
1564
+ t0 = time.time()
1565
+ logger.info(f"[PERF] _create_zsh_terminal START")
1498
1566
  if self._is_native_mode():
1499
1567
  backend = await self._ensure_native_backend()
1500
1568
  if backend:
@@ -1509,13 +1577,19 @@ class TerminalPanel(Container, can_focus=True):
1509
1577
  self.app.notify(
1510
1578
  "Failed to open zsh terminal", title="Terminal", severity="error"
1511
1579
  )
1580
+ logger.info(f"[PERF] _create_zsh_terminal native END: {time.time() - t0:.3f}s")
1512
1581
  return
1513
1582
 
1514
1583
  # Tmux fallback
1584
+ t1 = time.time()
1515
1585
  command = self._command_in_directory("zsh", workspace)
1586
+ logger.info(f"[PERF] _create_zsh_terminal command ready: {time.time() - t1:.3f}s")
1587
+ t2 = time.time()
1516
1588
  created = tmux_manager.create_inner_window("zsh", command)
1589
+ logger.info(f"[PERF] _create_zsh_terminal create_inner_window: {time.time() - t2:.3f}s")
1517
1590
  if not created:
1518
1591
  self.app.notify("Failed to open zsh terminal", title="Terminal", severity="error")
1592
+ logger.info(f"[PERF] _create_zsh_terminal TOTAL: {time.time() - t0:.3f}s")
1519
1593
 
1520
1594
  async def on_button_pressed(self, event: Button.Pressed) -> None:
1521
1595
  button_id = event.button.id or ""
@@ -1528,6 +1602,10 @@ class TerminalPanel(Container, can_focus=True):
1528
1602
  )
1529
1603
  return
1530
1604
 
1605
+ if button_id == "quick-new-agent":
1606
+ await self._quick_create_claude_agent()
1607
+ return
1608
+
1531
1609
  if button_id == "new-agent":
1532
1610
  from ..screens import CreateAgentScreen
1533
1611
 
realign/db/base.py CHANGED
@@ -57,6 +57,8 @@ class SessionRecord:
57
57
  total_turns: Optional[int] = None
58
58
  # V12: file mtime when total_turns was cached (for validation)
59
59
  total_turns_mtime: Optional[float] = None
60
+ # V19: agent association
61
+ agent_id: Optional[str] = None
60
62
 
61
63
 
62
64
  @dataclass
@@ -125,6 +127,18 @@ class AgentRecord:
125
127
  created_by: Optional[str] = None # Creator UID
126
128
 
127
129
 
130
+ @dataclass
131
+ class AgentInfoRecord:
132
+ """Agent profile/identity data (V20)."""
133
+
134
+ id: str
135
+ name: str
136
+ created_at: datetime
137
+ updated_at: datetime
138
+ description: Optional[str] = ""
139
+ visibility: str = "visible"
140
+
141
+
128
142
  @dataclass
129
143
  class AgentContextRecord:
130
144
  """Represents a context entry (V15: replaces load.json)."""
@@ -250,6 +264,19 @@ class DatabaseInterface(ABC):
250
264
  """
251
265
  pass
252
266
 
267
+ @abstractmethod
268
+ def get_sessions_by_agent_id(self, agent_id: str, limit: int = 1000) -> List[SessionRecord]:
269
+ """Get all sessions linked to an agent.
270
+
271
+ Args:
272
+ agent_id: The agent_info ID
273
+ limit: Maximum number of sessions to return
274
+
275
+ Returns:
276
+ List of SessionRecord objects for this agent
277
+ """
278
+ pass
279
+
253
280
  @abstractmethod
254
281
  def get_turn_by_hash(self, session_id: str, content_hash: str) -> Optional[TurnRecord]:
255
282
  """Check if a turn with this content hash already exists in the session."""
realign/db/locks.py CHANGED
@@ -31,6 +31,10 @@ def lock_key_for_event_summary(event_id: str) -> str:
31
31
  return f"event_summary:{event_id}"
32
32
 
33
33
 
34
+ def lock_key_for_agent_description(agent_id: str) -> str:
35
+ return f"agent_description:{agent_id}"
36
+
37
+
34
38
  @contextmanager
35
39
  def lease_lock(
36
40
  db: DatabaseInterface,
realign/db/schema.py CHANGED
@@ -76,9 +76,15 @@ Schema V18: UID refactor - created_by/shared_by with users table.
76
76
  - turns: drop uid and user_name (inherit from session)
77
77
  - agents: uid -> created_by, drop user_name
78
78
  - Update indexes accordingly
79
+
80
+ Schema V19: Agent association for sessions.
81
+ - sessions.agent_id: Logical agent entity association
82
+
83
+ Schema V20: Agent identity/profile table.
84
+ - agent_info table: name, description for agent profiles
79
85
  """
80
86
 
81
- SCHEMA_VERSION = 18
87
+ SCHEMA_VERSION = 21
82
88
 
83
89
  FTS_EVENTS_SCRIPTS = [
84
90
  # Full Text Search for Events
@@ -150,7 +156,8 @@ INIT_SCRIPTS = [
150
156
  created_by TEXT, -- V18: Creator UID (FK to users.uid)
151
157
  shared_by TEXT, -- V18: Sharer UID (who imported this)
152
158
  total_turns INTEGER DEFAULT 0, -- V10: Cached total turn count (avoids reading files)
153
- total_turns_mtime REAL -- V12: File mtime when total_turns was cached (for validation)
159
+ total_turns_mtime REAL, -- V12: File mtime when total_turns was cached (for validation)
160
+ agent_id TEXT -- V19: Logical agent association
154
161
  );
155
162
  """,
156
163
  # Turns table (corresponds to git commits, V18: uid/user_name removed)
@@ -222,6 +229,7 @@ INIT_SCRIPTS = [
222
229
  "CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(last_activity_at DESC);",
223
230
  "CREATE INDEX IF NOT EXISTS idx_sessions_type ON sessions(session_type);",
224
231
  "CREATE INDEX IF NOT EXISTS idx_sessions_created_by ON sessions(created_by);", # V18
232
+ "CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id);", # V19
225
233
  "CREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id);",
226
234
  "CREATE INDEX IF NOT EXISTS idx_turns_timestamp ON turns(timestamp DESC);",
227
235
  "CREATE INDEX IF NOT EXISTS idx_turns_hash ON turns(content_hash);",
@@ -335,6 +343,17 @@ INIT_SCRIPTS = [
335
343
  updated_at TEXT DEFAULT (datetime('now'))
336
344
  );
337
345
  """,
346
+ # Agent identity/profile table (V20)
347
+ """
348
+ CREATE TABLE IF NOT EXISTS agent_info (
349
+ id TEXT PRIMARY KEY,
350
+ name TEXT NOT NULL,
351
+ description TEXT DEFAULT '',
352
+ visibility TEXT NOT NULL DEFAULT 'visible',
353
+ created_at TEXT DEFAULT (datetime('now')),
354
+ updated_at TEXT DEFAULT (datetime('now'))
355
+ );
356
+ """,
338
357
  *FTS_EVENTS_SCRIPTS,
339
358
  ]
340
359
 
@@ -652,6 +671,29 @@ MIGRATION_V17_TO_V18 = [
652
671
  ]
653
672
 
654
673
 
674
+ MIGRATION_V18_TO_V19 = [
675
+ "ALTER TABLE sessions ADD COLUMN agent_id TEXT;",
676
+ "CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id);",
677
+ ]
678
+
679
+ MIGRATION_V19_TO_V20 = [
680
+ """
681
+ CREATE TABLE IF NOT EXISTS agent_info (
682
+ id TEXT PRIMARY KEY,
683
+ name TEXT NOT NULL,
684
+ description TEXT DEFAULT '',
685
+ visibility TEXT NOT NULL DEFAULT 'visible',
686
+ created_at TEXT DEFAULT (datetime('now')),
687
+ updated_at TEXT DEFAULT (datetime('now'))
688
+ );
689
+ """,
690
+ ]
691
+
692
+ MIGRATION_V20_TO_V21 = [
693
+ "ALTER TABLE agent_info ADD COLUMN visibility TEXT NOT NULL DEFAULT 'visible';",
694
+ ]
695
+
696
+
655
697
  def get_migration_scripts(from_version: int, to_version: int) -> list:
656
698
  """Get migration scripts for upgrading between versions."""
657
699
  scripts = []
@@ -716,4 +758,13 @@ def get_migration_scripts(from_version: int, to_version: int) -> list:
716
758
  if from_version < 18 and to_version >= 18:
717
759
  scripts.extend(MIGRATION_V17_TO_V18)
718
760
 
761
+ if from_version < 19 and to_version >= 19:
762
+ scripts.extend(MIGRATION_V18_TO_V19)
763
+
764
+ if from_version < 20 and to_version >= 20:
765
+ scripts.extend(MIGRATION_V19_TO_V20)
766
+
767
+ if from_version < 21 and to_version >= 21:
768
+ scripts.extend(MIGRATION_V20_TO_V21)
769
+
719
770
  return scripts