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
realign/db/sqlite_db.py CHANGED
@@ -21,6 +21,7 @@ from .base import (
21
21
  EventRecord,
22
22
  LockRecord,
23
23
  AgentRecord,
24
+ AgentInfoRecord,
24
25
  AgentContextRecord,
25
26
  UserRecord,
26
27
  )
@@ -293,6 +294,7 @@ class SQLiteDatabase(DatabaseInterface):
293
294
  started_at: datetime,
294
295
  workspace_path: Optional[str] = None,
295
296
  metadata: Optional[Dict[str, Any]] = None,
297
+ agent_id: Optional[str] = None,
296
298
  ) -> SessionRecord:
297
299
  """Get existing session or create new one."""
298
300
  conn = self._get_connection()
@@ -317,8 +319,8 @@ class SQLiteDatabase(DatabaseInterface):
317
319
  INSERT INTO sessions (
318
320
  id, session_file_path, session_type, workspace_path,
319
321
  started_at, last_activity_at, created_at, updated_at, metadata,
320
- created_by
321
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
322
+ created_by, agent_id
323
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
322
324
  """,
323
325
  (
324
326
  session_id,
@@ -331,6 +333,7 @@ class SQLiteDatabase(DatabaseInterface):
331
333
  now,
332
334
  metadata_json,
333
335
  config.uid,
336
+ agent_id,
334
337
  ),
335
338
  )
336
339
  conn.commit()
@@ -353,8 +356,18 @@ class SQLiteDatabase(DatabaseInterface):
353
356
  metadata=metadata or {},
354
357
  workspace_path=workspace_path,
355
358
  created_by=config.uid,
359
+ agent_id=agent_id,
356
360
  )
357
361
 
362
+ def update_session_agent_id(self, session_id: str, agent_id: Optional[str]) -> None:
363
+ """Set or update the agent_id on an existing session (V19)."""
364
+ conn = self._get_connection()
365
+ conn.execute(
366
+ "UPDATE sessions SET agent_id = ?, updated_at = ? WHERE id = ?",
367
+ (agent_id, datetime.now(), session_id),
368
+ )
369
+ conn.commit()
370
+
358
371
  def update_session_activity(self, session_id: str, last_activity_at: datetime) -> None:
359
372
  """Update last activity timestamp."""
360
373
  conn = self._get_connection()
@@ -557,6 +570,30 @@ class SQLiteDatabase(DatabaseInterface):
557
570
  )
558
571
  return [self._row_to_session(row) for row in cursor.fetchall()]
559
572
 
573
+ def get_sessions_by_agent_id(self, agent_id: str, limit: int = 1000) -> List[SessionRecord]:
574
+ """Get all sessions linked to an agent.
575
+
576
+ Args:
577
+ agent_id: The agent_info ID
578
+ limit: Maximum number of sessions to return
579
+
580
+ Returns:
581
+ List of SessionRecord objects for this agent
582
+ """
583
+ conn = self._get_connection()
584
+ cursor = conn.cursor()
585
+ try:
586
+ cursor.execute(
587
+ """SELECT * FROM sessions
588
+ WHERE agent_id = ?
589
+ ORDER BY last_activity_at DESC
590
+ LIMIT ?""",
591
+ (agent_id, limit),
592
+ )
593
+ return [self._row_to_session(row) for row in cursor.fetchall()]
594
+ except sqlite3.OperationalError:
595
+ return []
596
+
560
597
  def get_turn_content(self, turn_id: str) -> Optional[str]:
561
598
  """Get the JSONL content for a turn."""
562
599
  conn = self._get_connection()
@@ -973,6 +1010,7 @@ class SQLiteDatabase(DatabaseInterface):
973
1010
  expected_turns: Optional[int] = None,
974
1011
  skip_dedup: bool = False,
975
1012
  no_track: bool = False,
1013
+ agent_id: Optional[str] = None,
976
1014
  ) -> str:
977
1015
  session_id = session_file_path.stem
978
1016
  dedupe_key = f"turn:{session_id}:{int(turn_number)}"
@@ -991,6 +1029,8 @@ class SQLiteDatabase(DatabaseInterface):
991
1029
  payload["skip_dedup"] = True
992
1030
  if no_track:
993
1031
  payload["no_track"] = True
1032
+ if agent_id:
1033
+ payload["agent_id"] = agent_id
994
1034
 
995
1035
  # For append-only session formats (Claude/Codex/Gemini), a turn is immutable once completed.
996
1036
  # Avoid re-running already-done turn jobs on repeated enqueue attempts.
@@ -1033,6 +1073,17 @@ class SQLiteDatabase(DatabaseInterface):
1033
1073
  requeue_done=True,
1034
1074
  )
1035
1075
 
1076
+ def enqueue_agent_description_job(self, *, agent_id: str) -> str:
1077
+ dedupe_key = f"agent_desc:{agent_id}"
1078
+ payload: Dict[str, Any] = {"agent_id": agent_id}
1079
+ return self.enqueue_job(
1080
+ kind="agent_description",
1081
+ dedupe_key=dedupe_key,
1082
+ payload=payload,
1083
+ priority=12,
1084
+ requeue_done=True,
1085
+ )
1086
+
1036
1087
  def claim_next_job(
1037
1088
  self,
1038
1089
  *,
@@ -2770,6 +2821,13 @@ class SQLiteDatabase(DatabaseInterface):
2770
2821
  except (IndexError, KeyError):
2771
2822
  pass
2772
2823
 
2824
+ # V19: agent association
2825
+ agent_id = None
2826
+ try:
2827
+ agent_id = row["agent_id"]
2828
+ except (IndexError, KeyError):
2829
+ pass
2830
+
2773
2831
  return SessionRecord(
2774
2832
  id=row["id"],
2775
2833
  session_file_path=Path(row["session_file_path"]),
@@ -2790,6 +2848,7 @@ class SQLiteDatabase(DatabaseInterface):
2790
2848
  shared_by=shared_by,
2791
2849
  total_turns=total_turns,
2792
2850
  total_turns_mtime=total_turns_mtime,
2851
+ agent_id=agent_id,
2793
2852
  )
2794
2853
 
2795
2854
  def _row_to_turn(self, row: sqlite3.Row) -> TurnRecord:
@@ -3015,3 +3074,127 @@ class SQLiteDatabase(DatabaseInterface):
3015
3074
  except sqlite3.OperationalError:
3016
3075
  pass
3017
3076
  return None
3077
+
3078
+ # -------------------------------------------------------------------------
3079
+ # Agent info table methods (Schema V20)
3080
+ # -------------------------------------------------------------------------
3081
+
3082
+ def _row_to_agent_info(self, row: sqlite3.Row) -> AgentInfoRecord:
3083
+ """Convert a database row to an AgentInfoRecord."""
3084
+ visibility = "visible"
3085
+ try:
3086
+ if "visibility" in row.keys():
3087
+ visibility = row["visibility"] or "visible"
3088
+ except Exception:
3089
+ visibility = "visible"
3090
+ return AgentInfoRecord(
3091
+ id=row["id"],
3092
+ name=row["name"],
3093
+ description=row["description"] or "",
3094
+ created_at=self._parse_datetime(row["created_at"]),
3095
+ updated_at=self._parse_datetime(row["updated_at"]),
3096
+ visibility=visibility,
3097
+ )
3098
+
3099
+ def get_or_create_agent_info(
3100
+ self, agent_id: str, name: Optional[str] = None
3101
+ ) -> AgentInfoRecord:
3102
+ """Get existing agent info or create a new one.
3103
+
3104
+ Args:
3105
+ agent_id: UUID for the agent.
3106
+ name: Display name (if None, caller should provide a generated name).
3107
+ """
3108
+ conn = self._get_connection()
3109
+ cursor = conn.cursor()
3110
+
3111
+ try:
3112
+ cursor.execute("SELECT * FROM agent_info WHERE id = ?", (agent_id,))
3113
+ row = cursor.fetchone()
3114
+ if row:
3115
+ return self._row_to_agent_info(row)
3116
+ except sqlite3.OperationalError:
3117
+ pass
3118
+
3119
+ display_name = name or agent_id[:8]
3120
+ cursor.execute(
3121
+ """
3122
+ INSERT INTO agent_info (id, name, description, visibility, created_at, updated_at)
3123
+ VALUES (?, ?, '', 'visible', datetime('now'), datetime('now'))
3124
+ """,
3125
+ (agent_id, display_name),
3126
+ )
3127
+ conn.commit()
3128
+
3129
+ cursor.execute("SELECT * FROM agent_info WHERE id = ?", (agent_id,))
3130
+ row = cursor.fetchone()
3131
+ return self._row_to_agent_info(row)
3132
+
3133
+ def get_agent_info(self, agent_id: str) -> Optional[AgentInfoRecord]:
3134
+ """Get agent info by ID, or None if not found."""
3135
+ conn = self._get_connection()
3136
+ try:
3137
+ cursor = conn.execute("SELECT * FROM agent_info WHERE id = ?", (agent_id,))
3138
+ row = cursor.fetchone()
3139
+ if row:
3140
+ return self._row_to_agent_info(row)
3141
+ except sqlite3.OperationalError:
3142
+ pass
3143
+ return None
3144
+
3145
+ def update_agent_info(
3146
+ self,
3147
+ agent_id: str,
3148
+ *,
3149
+ name: Optional[str] = None,
3150
+ description: Optional[str] = None,
3151
+ visibility: Optional[str] = None,
3152
+ ) -> Optional[AgentInfoRecord]:
3153
+ """Update agent info fields. Returns updated record or None if not found."""
3154
+ conn = self._get_connection()
3155
+ sets: list[str] = []
3156
+ params: list[Any] = []
3157
+
3158
+ if name is not None:
3159
+ sets.append("name = ?")
3160
+ params.append(name)
3161
+ if description is not None:
3162
+ sets.append("description = ?")
3163
+ params.append(description)
3164
+ if visibility is not None:
3165
+ sets.append("visibility = ?")
3166
+ params.append(visibility)
3167
+
3168
+ if not sets:
3169
+ return self.get_agent_info(agent_id)
3170
+
3171
+ sets.append("updated_at = datetime('now')")
3172
+ params.append(agent_id)
3173
+
3174
+ try:
3175
+ conn.execute(
3176
+ f"UPDATE agent_info SET {', '.join(sets)} WHERE id = ?",
3177
+ params,
3178
+ )
3179
+ conn.commit()
3180
+ except sqlite3.OperationalError:
3181
+ return None
3182
+
3183
+ return self.get_agent_info(agent_id)
3184
+
3185
+ def list_agent_info(self, *, include_invisible: bool = False) -> list[AgentInfoRecord]:
3186
+ """List agent info records, ordered by created_at descending."""
3187
+ conn = self._get_connection()
3188
+ try:
3189
+ if include_invisible:
3190
+ cursor = conn.execute("SELECT * FROM agent_info ORDER BY created_at DESC")
3191
+ else:
3192
+ try:
3193
+ cursor = conn.execute(
3194
+ "SELECT * FROM agent_info WHERE visibility = 'visible' ORDER BY created_at DESC"
3195
+ )
3196
+ except sqlite3.OperationalError:
3197
+ cursor = conn.execute("SELECT * FROM agent_info ORDER BY created_at DESC")
3198
+ return [self._row_to_agent_info(row) for row in cursor.fetchall()]
3199
+ except sqlite3.OperationalError:
3200
+ return []
@@ -0,0 +1,157 @@
1
+ """Agent description generation from session summaries using LLM."""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import List, Optional
6
+
7
+ from ..db.sqlite_db import SQLiteDatabase
8
+ from ..db.base import SessionRecord
9
+ from ..db.locks import lease_lock, lock_key_for_agent_description, make_lock_owner
10
+ from ..llm_client import call_llm_cloud
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def schedule_agent_description_update(db: SQLiteDatabase, agent_id: str) -> None:
16
+ """
17
+ Enqueue an agent description update job (durable).
18
+
19
+ Call this after a session summary is updated for an agent-linked session.
20
+
21
+ Args:
22
+ db: Database instance
23
+ agent_id: ID of the agent to update
24
+ """
25
+ try:
26
+ db.enqueue_agent_description_job(agent_id=agent_id)
27
+ logger.debug(f"Enqueued agent description job for agent_id={agent_id}")
28
+ except Exception as e:
29
+ logger.debug(f"Failed to enqueue agent description job, falling back: {e}")
30
+ force_update_agent_description(db, agent_id)
31
+
32
+
33
+ def force_update_agent_description(db: SQLiteDatabase, agent_id: str) -> None:
34
+ """
35
+ Immediately update agent description, bypassing the job queue.
36
+
37
+ Args:
38
+ db: Database instance
39
+ agent_id: ID of the agent to update
40
+ """
41
+ _update_agent_description(db, agent_id)
42
+
43
+
44
+ def _update_agent_description(db: SQLiteDatabase, agent_id: str) -> None:
45
+ """
46
+ Actually perform the agent description update.
47
+
48
+ Args:
49
+ db: Database instance
50
+ agent_id: ID of the agent to update
51
+ """
52
+ logger.info(f"Updating agent description for agent_id={agent_id}")
53
+
54
+ owner = make_lock_owner("agent_description")
55
+ lock_key = lock_key_for_agent_description(agent_id)
56
+
57
+ with lease_lock(
58
+ db,
59
+ lock_key,
60
+ owner=owner,
61
+ ttl_seconds=10 * 60, # 10 minutes
62
+ wait_timeout_seconds=0.0,
63
+ ) as acquired:
64
+ if not acquired:
65
+ logger.debug(f"Agent description lock held by another process: agent_id={agent_id}")
66
+ return
67
+
68
+ sessions = db.get_sessions_by_agent_id(agent_id)
69
+ if not sessions:
70
+ logger.warning(f"No sessions found for agent_id={agent_id}")
71
+ return
72
+
73
+ # Filter to sessions with non-empty title or summary
74
+ sessions_with_content = [
75
+ s for s in sessions if s.session_title or s.session_summary
76
+ ]
77
+ if not sessions_with_content:
78
+ logger.warning(f"No sessions with summaries for agent_id={agent_id}")
79
+ return
80
+
81
+ description = _generate_agent_description_llm(sessions_with_content)
82
+ db.update_agent_info(agent_id, description=description)
83
+ logger.info(
84
+ f"Agent description updated: '{description[:60]}...' for agent_id={agent_id}"
85
+ )
86
+
87
+
88
+ def _generate_agent_description_llm(sessions: List[SessionRecord]) -> str:
89
+ """
90
+ Use LLM to generate an agent description from all its sessions.
91
+
92
+ Args:
93
+ sessions: List of sessions linked to the agent
94
+
95
+ Returns:
96
+ Description string
97
+ """
98
+ if not sessions:
99
+ return ""
100
+
101
+ # Build sessions payload
102
+ sessions_data = []
103
+ for i, session in enumerate(sessions):
104
+ sessions_data.append(
105
+ {
106
+ "session_number": i + 1,
107
+ "title": session.session_title or f"Session {session.id[:8]}",
108
+ "summary": session.session_summary or "(no summary)",
109
+ }
110
+ )
111
+
112
+ # Try cloud provider
113
+ try:
114
+ from ..auth import is_logged_in
115
+
116
+ if is_logged_in():
117
+ logger.debug("Attempting cloud LLM for agent description")
118
+ custom_prompt = None
119
+ user_prompt_path = Path.home() / ".aline" / "prompts" / "agent_description.md"
120
+ try:
121
+ if user_prompt_path.exists():
122
+ custom_prompt = user_prompt_path.read_text(encoding="utf-8").strip()
123
+ except Exception:
124
+ pass
125
+
126
+ _, result = call_llm_cloud(
127
+ task="agent_description",
128
+ payload={"sessions": sessions_data},
129
+ custom_prompt=custom_prompt,
130
+ silent=True,
131
+ )
132
+
133
+ if result:
134
+ description = result.get("description", "")
135
+ logger.info(f"Cloud LLM agent description success: {description[:60]}...")
136
+ return description
137
+ else:
138
+ logger.warning("Cloud LLM agent description failed, using fallback")
139
+ return _fallback_agent_description(sessions)
140
+ except ImportError:
141
+ logger.debug("Auth module not available, skipping cloud LLM")
142
+
143
+ logger.warning("Not logged in, cannot use cloud LLM for agent description")
144
+ return _fallback_agent_description(sessions)
145
+
146
+
147
+ def _fallback_agent_description(sessions: List[SessionRecord]) -> str:
148
+ """Fallback when LLM fails: simple concatenation of recent session summaries."""
149
+ summaries = [s.session_summary for s in sessions if s.session_summary]
150
+ if not summaries:
151
+ titles = [s.session_title for s in sessions if s.session_title]
152
+ if titles:
153
+ return f"Agent with {len(sessions)} sessions. Recent work: {'; '.join(titles[-3:])}"
154
+ return f"Agent with {len(sessions)} sessions."
155
+
156
+ recent = summaries[:3] # sessions already ordered by last_activity_at DESC
157
+ return f"Agent with {len(sessions)} sessions. " + " ".join(recent)
@@ -145,6 +145,9 @@ def update_session_summary_now(db: SQLiteDatabase, session_id: str) -> bool:
145
145
 
146
146
  # Trigger event summary updates for all parent events (if any)
147
147
  _trigger_parent_event_updates(db, session_id)
148
+
149
+ # Trigger agent description update if session is linked to an agent
150
+ _trigger_agent_description_update(db, session_id)
148
151
  return True
149
152
 
150
153
 
@@ -168,6 +171,28 @@ def _trigger_parent_event_updates(db: SQLiteDatabase, session_id: str) -> None:
168
171
  logger.warning(f"Failed to trigger event updates: {e}")
169
172
 
170
173
 
174
+ def _trigger_agent_description_update(db: SQLiteDatabase, session_id: str) -> None:
175
+ """
176
+ Trigger agent description update if the session is linked to an agent.
177
+
178
+ Args:
179
+ db: Database instance
180
+ session_id: ID of the session whose agent should be updated
181
+ """
182
+ try:
183
+ session = db.get_session_by_id(session_id)
184
+ if session and getattr(session, "agent_id", None):
185
+ from .agent_summarizer import schedule_agent_description_update
186
+
187
+ schedule_agent_description_update(db, session.agent_id)
188
+ logger.debug(
189
+ f"Triggered agent description update for agent_id={session.agent_id}"
190
+ )
191
+ except Exception as e:
192
+ # Don't fail session summary update if agent trigger fails
193
+ logger.warning(f"Failed to trigger agent description update: {e}")
194
+
195
+
171
196
  def _get_session_summary_prompt() -> str:
172
197
  """
173
198
  Load session summary prompt with user customization support.
realign/watcher_core.py CHANGED
@@ -260,7 +260,7 @@ class DialogueWatcher:
260
260
  return
261
261
 
262
262
  try:
263
- from .codex_home import terminal_id_from_codex_session_file
263
+ from .codex_home import codex_home_owner_from_session_file
264
264
  from .codex_terminal_linker import read_codex_session_meta, select_agent_for_codex_session
265
265
  from .db import get_database
266
266
 
@@ -271,13 +271,50 @@ class DialogueWatcher:
271
271
  db = get_database(read_only=False)
272
272
  agents = db.list_agents(status="active", limit=1000)
273
273
  # Deterministic mapping: session file stored under ~/.aline/codex_homes/<terminal_id>/...
274
- agent_id = terminal_id_from_codex_session_file(session_file)
274
+ owner = codex_home_owner_from_session_file(session_file)
275
+ agent_id = None
276
+ agent_info_id = None
277
+ if owner:
278
+ if owner[0] == "terminal":
279
+ agent_id = owner[1]
280
+ elif owner[0] == "agent":
281
+ agent_info_id = owner[1]
282
+ scoped_agents = [
283
+ a
284
+ for a in agents
285
+ if getattr(a, "provider", "") == "codex"
286
+ and getattr(a, "status", "") == "active"
287
+ and (getattr(a, "source", "") or "") == f"agent:{agent_info_id}"
288
+ ]
289
+ agent_id = select_agent_for_codex_session(
290
+ scoped_agents, session=meta, max_time_delta_seconds=None
291
+ )
275
292
  if not agent_id:
276
293
  # Fallback heuristic mapping (legacy default ~/.codex/sessions).
277
294
  agent_id = select_agent_for_codex_session(agents, session=meta)
278
295
  if not agent_id:
279
296
  return
280
297
 
298
+ owner_agent_info_id = agent_info_id
299
+ # Get existing agent to preserve agent_info_id in source field
300
+ existing_agent = db.get_agent_by_id(agent_id)
301
+ agent_info_id = None
302
+ existing_source = None
303
+ if existing_agent:
304
+ existing_source = existing_agent.source or ""
305
+ if existing_source.startswith("agent:"):
306
+ agent_info_id = existing_source[6:]
307
+
308
+ if not agent_info_id and owner_agent_info_id:
309
+ agent_info_id = owner_agent_info_id
310
+
311
+ if existing_source:
312
+ source = existing_source
313
+ elif agent_info_id:
314
+ source = f"agent:{agent_info_id}"
315
+ else:
316
+ source = "codex:auto-link"
317
+
281
318
  db.update_agent(
282
319
  agent_id,
283
320
  provider="codex",
@@ -286,8 +323,15 @@ class DialogueWatcher:
286
323
  transcript_path=str(session_file),
287
324
  cwd=meta.cwd,
288
325
  project_dir=meta.cwd,
289
- source="codex:auto-link",
326
+ source=source,
290
327
  )
328
+
329
+ # Link session to agent_info if available (bidirectional linking)
330
+ if agent_info_id:
331
+ try:
332
+ db.update_session_agent_id(session_file.stem, agent_info_id)
333
+ except Exception:
334
+ pass
291
335
  except Exception:
292
336
  return
293
337
 
@@ -409,6 +453,7 @@ class DialogueWatcher:
409
453
  project_dir = signal_data.get("project_dir", "")
410
454
  transcript_path = signal_data.get("transcript_path", "")
411
455
  no_track = bool(signal_data.get("no_track", False))
456
+ agent_id = signal_data.get("agent_id", "")
412
457
 
413
458
  logger.info(f"Stop signal received for session {session_id}")
414
459
  print(f"[Watcher] Stop signal received for {session_id}", file=sys.stderr)
@@ -444,6 +489,7 @@ class DialogueWatcher:
444
489
  turn_number=target_turn,
445
490
  session_type=self._detect_session_type(session_file),
446
491
  no_track=no_track,
492
+ agent_id=agent_id if agent_id else None,
447
493
  )
448
494
  except Exception as e:
449
495
  logger.warning(
@@ -505,6 +551,7 @@ class DialogueWatcher:
505
551
  transcript_path = str(signal_data.get("transcript_path") or "")
506
552
  project_dir = str(signal_data.get("project_dir") or "")
507
553
  no_track = bool(signal_data.get("no_track", False))
554
+ agent_id = str(signal_data.get("agent_id") or "")
508
555
 
509
556
  session_file = None
510
557
  if transcript_path and Path(transcript_path).exists():
@@ -534,6 +581,16 @@ class DialogueWatcher:
534
581
  no_track,
535
582
  )
536
583
 
584
+ # Link session to agent if agent_id is provided
585
+ if agent_id and session_id:
586
+ try:
587
+ from .db import get_database
588
+
589
+ db = get_database()
590
+ db.update_session_agent_id(session_id, agent_id)
591
+ except Exception:
592
+ pass
593
+
537
594
  signal_file.unlink(missing_ok=True)
538
595
  except Exception as e:
539
596
  logger.error(f"Error checking user prompt signals: {e}", exc_info=True)
realign/worker_core.py CHANGED
@@ -4,6 +4,7 @@ Background worker for durable jobs queue.
4
4
  This process consumes jobs from the SQLite `jobs` table:
5
5
  - turn_summary: generate/store a turn (LLM + content snapshot)
6
6
  - session_summary: aggregate session title/summary from turns
7
+ - agent_description: regenerate agent description from session summaries
7
8
  """
8
9
 
9
10
  from __future__ import annotations
@@ -66,7 +67,7 @@ class AlineWorker:
66
67
  try:
67
68
  job = self.db.claim_next_job(
68
69
  worker_id=self.worker_id,
69
- kinds=["turn_summary", "session_summary"],
70
+ kinds=["turn_summary", "session_summary", "agent_description"],
70
71
  )
71
72
  if not job:
72
73
  await asyncio.sleep(self.poll_interval_seconds)
@@ -134,6 +135,11 @@ class AlineWorker:
134
135
  )
135
136
  return
136
137
 
138
+ if kind == "agent_description":
139
+ await self._process_agent_description_job(payload)
140
+ self.db.finish_job(job_id=job_id, worker_id=self.worker_id, success=True)
141
+ return
142
+
137
143
  # Unknown job kind: mark as permanently failed to avoid infinite loops.
138
144
  self.db.finish_job(
139
145
  job_id=job_id,
@@ -187,6 +193,7 @@ class AlineWorker:
187
193
  expected_turns = int(expected_turns_raw) if expected_turns_raw is not None else None
188
194
  skip_dedup = bool(payload.get("skip_dedup") or False)
189
195
  no_track = bool(payload.get("no_track") or False)
196
+ agent_id = str(payload.get("agent_id") or "")
190
197
 
191
198
  if not session_id or turn_number <= 0 or not session_file_path:
192
199
  raise ValueError(f"Invalid turn_summary payload: {payload}")
@@ -216,6 +223,13 @@ class AlineWorker:
216
223
  no_track=no_track,
217
224
  )
218
225
 
226
+ # Link session to agent after commit ensures session exists in DB
227
+ if agent_id and session_id:
228
+ try:
229
+ self.db.update_session_agent_id(session_id, agent_id)
230
+ except Exception:
231
+ pass
232
+
219
233
  if created:
220
234
  if expected_turns:
221
235
  self._enqueue_session_summary_if_complete(session_id, expected_turns)
@@ -255,3 +269,12 @@ class AlineWorker:
255
269
  from .events.session_summarizer import update_session_summary_now
256
270
 
257
271
  return bool(update_session_summary_now(self.db, session_id))
272
+
273
+ async def _process_agent_description_job(self, payload: Dict[str, Any]) -> None:
274
+ agent_id = str(payload.get("agent_id") or "")
275
+ if not agent_id:
276
+ raise ValueError(f"Invalid agent_description payload: {payload}")
277
+
278
+ from .events.agent_summarizer import force_update_agent_description
279
+
280
+ force_update_agent_description(self.db, agent_id)