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.
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/METADATA +1 -1
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/RECORD +41 -34
- realign/__init__.py +1 -1
- realign/agent_names.py +79 -0
- realign/claude_hooks/stop_hook.py +3 -0
- realign/claude_hooks/terminal_state.py +11 -0
- realign/claude_hooks/user_prompt_submit_hook.py +3 -0
- realign/cli.py +62 -0
- realign/codex_detector.py +1 -1
- realign/codex_home.py +46 -15
- realign/codex_terminal_linker.py +18 -7
- realign/commands/agent.py +109 -0
- realign/commands/doctor.py +3 -1
- realign/commands/export_shares.py +297 -0
- realign/commands/search.py +58 -29
- realign/dashboard/app.py +9 -158
- realign/dashboard/clipboard.py +54 -0
- realign/dashboard/screens/__init__.py +4 -0
- realign/dashboard/screens/agent_detail.py +333 -0
- realign/dashboard/screens/create_agent_info.py +133 -0
- realign/dashboard/screens/event_detail.py +6 -27
- realign/dashboard/styles/dashboard.tcss +67 -0
- realign/dashboard/tmux_manager.py +49 -8
- realign/dashboard/widgets/__init__.py +2 -0
- realign/dashboard/widgets/agents_panel.py +1129 -0
- realign/dashboard/widgets/config_panel.py +17 -11
- realign/dashboard/widgets/events_table.py +4 -27
- realign/dashboard/widgets/sessions_table.py +4 -27
- realign/dashboard/widgets/terminal_panel.py +109 -31
- realign/db/base.py +27 -0
- realign/db/locks.py +4 -0
- realign/db/schema.py +53 -2
- realign/db/sqlite_db.py +185 -2
- realign/events/agent_summarizer.py +157 -0
- realign/events/session_summarizer.py +25 -0
- realign/watcher_core.py +60 -3
- realign/worker_core.py +24 -1
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.4.dist-info → aline_ai-0.6.6.dist-info}/licenses/LICENSE +0 -0
- {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
|
|
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
|
-
|
|
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=
|
|
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)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|