aline-ai 0.6.5__py3-none-any.whl → 0.6.7__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.5.dist-info → aline_ai-0.6.7.dist-info}/METADATA +1 -1
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.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 +43 -1
- realign/claude_hooks/user_prompt_submit_hook.py +3 -0
- realign/cli.py +62 -0
- realign/codex_detector.py +18 -3
- realign/codex_home.py +65 -16
- realign/codex_terminal_linker.py +18 -7
- realign/commands/agent.py +109 -0
- realign/commands/doctor.py +74 -1
- realign/commands/export_shares.py +448 -0
- realign/commands/import_shares.py +203 -1
- realign/commands/search.py +58 -29
- realign/commands/sync_agent.py +347 -0
- realign/dashboard/app.py +9 -9
- 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 +244 -0
- realign/dashboard/screens/event_detail.py +6 -27
- realign/dashboard/styles/dashboard.tcss +22 -28
- realign/dashboard/tmux_manager.py +36 -10
- realign/dashboard/widgets/__init__.py +2 -2
- realign/dashboard/widgets/agents_panel.py +1248 -0
- realign/dashboard/widgets/events_table.py +4 -27
- realign/dashboard/widgets/sessions_table.py +4 -27
- realign/db/base.py +69 -0
- realign/db/locks.py +4 -0
- realign/db/schema.py +111 -2
- realign/db/sqlite_db.py +360 -2
- realign/events/agent_summarizer.py +157 -0
- realign/events/session_summarizer.py +25 -0
- realign/watcher_core.py +193 -5
- realign/worker_core.py +59 -1
- realign/dashboard/widgets/terminal_panel.py +0 -1653
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/top_level.txt +0 -0
realign/watcher_core.py
CHANGED
|
@@ -259,8 +259,56 @@ class DialogueWatcher:
|
|
|
259
259
|
except Exception:
|
|
260
260
|
return
|
|
261
261
|
|
|
262
|
+
def _agent_info_id_for_codex_session(
|
|
263
|
+
self, session_file: Path, *, db=None
|
|
264
|
+
) -> Optional[str]:
|
|
265
|
+
"""Best-effort: resolve agent_info_id from a codex session file."""
|
|
262
266
|
try:
|
|
263
|
-
from .codex_home import
|
|
267
|
+
from .codex_home import codex_home_owner_from_session_file
|
|
268
|
+
except Exception:
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
owner = codex_home_owner_from_session_file(session_file)
|
|
273
|
+
except Exception:
|
|
274
|
+
owner = None
|
|
275
|
+
if not owner or owner[0] != "terminal":
|
|
276
|
+
return None
|
|
277
|
+
terminal_id = owner[1]
|
|
278
|
+
if not terminal_id:
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
try:
|
|
282
|
+
if db is None:
|
|
283
|
+
from .db import get_database
|
|
284
|
+
|
|
285
|
+
db = get_database(read_only=True)
|
|
286
|
+
agent = db.get_agent_by_id(terminal_id)
|
|
287
|
+
except Exception:
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
source = (agent.source or "").strip() if agent else ""
|
|
291
|
+
if source.startswith("agent:"):
|
|
292
|
+
return source[6:]
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
def _terminal_id_for_codex_session(self, session_file: Path) -> Optional[str]:
|
|
296
|
+
"""Best-effort: resolve terminal_id from codex session file path."""
|
|
297
|
+
try:
|
|
298
|
+
from .codex_home import codex_home_owner_from_session_file
|
|
299
|
+
except Exception:
|
|
300
|
+
return None
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
owner = codex_home_owner_from_session_file(session_file)
|
|
304
|
+
except Exception:
|
|
305
|
+
owner = None
|
|
306
|
+
if not owner or owner[0] != "terminal":
|
|
307
|
+
return None
|
|
308
|
+
return owner[1]
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
from .codex_home import codex_home_owner_from_session_file
|
|
264
312
|
from .codex_terminal_linker import read_codex_session_meta, select_agent_for_codex_session
|
|
265
313
|
from .db import get_database
|
|
266
314
|
|
|
@@ -271,13 +319,50 @@ class DialogueWatcher:
|
|
|
271
319
|
db = get_database(read_only=False)
|
|
272
320
|
agents = db.list_agents(status="active", limit=1000)
|
|
273
321
|
# Deterministic mapping: session file stored under ~/.aline/codex_homes/<terminal_id>/...
|
|
274
|
-
|
|
322
|
+
owner = codex_home_owner_from_session_file(session_file)
|
|
323
|
+
agent_id = None
|
|
324
|
+
agent_info_id = None
|
|
325
|
+
if owner:
|
|
326
|
+
if owner[0] == "terminal":
|
|
327
|
+
agent_id = owner[1]
|
|
328
|
+
elif owner[0] == "agent":
|
|
329
|
+
agent_info_id = owner[1]
|
|
330
|
+
scoped_agents = [
|
|
331
|
+
a
|
|
332
|
+
for a in agents
|
|
333
|
+
if getattr(a, "provider", "") == "codex"
|
|
334
|
+
and getattr(a, "status", "") == "active"
|
|
335
|
+
and (getattr(a, "source", "") or "") == f"agent:{agent_info_id}"
|
|
336
|
+
]
|
|
337
|
+
agent_id = select_agent_for_codex_session(
|
|
338
|
+
scoped_agents, session=meta, max_time_delta_seconds=None
|
|
339
|
+
)
|
|
275
340
|
if not agent_id:
|
|
276
341
|
# Fallback heuristic mapping (legacy default ~/.codex/sessions).
|
|
277
342
|
agent_id = select_agent_for_codex_session(agents, session=meta)
|
|
278
343
|
if not agent_id:
|
|
279
344
|
return
|
|
280
345
|
|
|
346
|
+
owner_agent_info_id = agent_info_id
|
|
347
|
+
# Get existing agent to preserve agent_info_id in source field
|
|
348
|
+
existing_agent = db.get_agent_by_id(agent_id)
|
|
349
|
+
agent_info_id = None
|
|
350
|
+
existing_source = None
|
|
351
|
+
if existing_agent:
|
|
352
|
+
existing_source = existing_agent.source or ""
|
|
353
|
+
if existing_source.startswith("agent:"):
|
|
354
|
+
agent_info_id = existing_source[6:]
|
|
355
|
+
|
|
356
|
+
if not agent_info_id and owner_agent_info_id:
|
|
357
|
+
agent_info_id = owner_agent_info_id
|
|
358
|
+
|
|
359
|
+
if existing_source:
|
|
360
|
+
source = existing_source
|
|
361
|
+
elif agent_info_id:
|
|
362
|
+
source = f"agent:{agent_info_id}"
|
|
363
|
+
else:
|
|
364
|
+
source = "codex:auto-link"
|
|
365
|
+
|
|
281
366
|
db.update_agent(
|
|
282
367
|
agent_id,
|
|
283
368
|
provider="codex",
|
|
@@ -286,8 +371,27 @@ class DialogueWatcher:
|
|
|
286
371
|
transcript_path=str(session_file),
|
|
287
372
|
cwd=meta.cwd,
|
|
288
373
|
project_dir=meta.cwd,
|
|
289
|
-
source=
|
|
374
|
+
source=source,
|
|
290
375
|
)
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
db.insert_window_link(
|
|
379
|
+
terminal_id=agent_id,
|
|
380
|
+
agent_id=agent_info_id,
|
|
381
|
+
session_id=session_file.stem,
|
|
382
|
+
provider="codex",
|
|
383
|
+
source="codex:watcher",
|
|
384
|
+
ts=time.time(),
|
|
385
|
+
)
|
|
386
|
+
except Exception:
|
|
387
|
+
pass
|
|
388
|
+
|
|
389
|
+
# Link session to agent_info if available (bidirectional linking)
|
|
390
|
+
if agent_info_id:
|
|
391
|
+
try:
|
|
392
|
+
db.update_session_agent_id(session_file.stem, agent_info_id)
|
|
393
|
+
except Exception:
|
|
394
|
+
pass
|
|
291
395
|
except Exception:
|
|
292
396
|
return
|
|
293
397
|
|
|
@@ -409,6 +513,7 @@ class DialogueWatcher:
|
|
|
409
513
|
project_dir = signal_data.get("project_dir", "")
|
|
410
514
|
transcript_path = signal_data.get("transcript_path", "")
|
|
411
515
|
no_track = bool(signal_data.get("no_track", False))
|
|
516
|
+
agent_id = signal_data.get("agent_id", "")
|
|
412
517
|
|
|
413
518
|
logger.info(f"Stop signal received for session {session_id}")
|
|
414
519
|
print(f"[Watcher] Stop signal received for {session_id}", file=sys.stderr)
|
|
@@ -444,6 +549,7 @@ class DialogueWatcher:
|
|
|
444
549
|
turn_number=target_turn,
|
|
445
550
|
session_type=self._detect_session_type(session_file),
|
|
446
551
|
no_track=no_track,
|
|
552
|
+
agent_id=agent_id if agent_id else None,
|
|
447
553
|
)
|
|
448
554
|
except Exception as e:
|
|
449
555
|
logger.warning(
|
|
@@ -505,6 +611,7 @@ class DialogueWatcher:
|
|
|
505
611
|
transcript_path = str(signal_data.get("transcript_path") or "")
|
|
506
612
|
project_dir = str(signal_data.get("project_dir") or "")
|
|
507
613
|
no_track = bool(signal_data.get("no_track", False))
|
|
614
|
+
agent_id = str(signal_data.get("agent_id") or "")
|
|
508
615
|
|
|
509
616
|
session_file = None
|
|
510
617
|
if transcript_path and Path(transcript_path).exists():
|
|
@@ -534,6 +641,16 @@ class DialogueWatcher:
|
|
|
534
641
|
no_track,
|
|
535
642
|
)
|
|
536
643
|
|
|
644
|
+
# Link session to agent if agent_id is provided
|
|
645
|
+
if agent_id and session_id:
|
|
646
|
+
try:
|
|
647
|
+
from .db import get_database
|
|
648
|
+
|
|
649
|
+
db = get_database()
|
|
650
|
+
db.update_session_agent_id(session_id, agent_id)
|
|
651
|
+
except Exception:
|
|
652
|
+
pass
|
|
653
|
+
|
|
537
654
|
signal_file.unlink(missing_ok=True)
|
|
538
655
|
except Exception as e:
|
|
539
656
|
logger.error(f"Error checking user prompt signals: {e}", exc_info=True)
|
|
@@ -794,6 +911,23 @@ class DialogueWatcher:
|
|
|
794
911
|
file=sys.stderr,
|
|
795
912
|
)
|
|
796
913
|
|
|
914
|
+
agent_id = None
|
|
915
|
+
if session_type == "codex":
|
|
916
|
+
agent_id = self._agent_info_id_for_codex_session(session_file, db=db)
|
|
917
|
+
terminal_id = self._terminal_id_for_codex_session(session_file)
|
|
918
|
+
if terminal_id:
|
|
919
|
+
try:
|
|
920
|
+
db.insert_window_link(
|
|
921
|
+
terminal_id=terminal_id,
|
|
922
|
+
agent_id=agent_id,
|
|
923
|
+
session_id=session_id,
|
|
924
|
+
provider="codex",
|
|
925
|
+
source="codex:watcher",
|
|
926
|
+
ts=time.time(),
|
|
927
|
+
)
|
|
928
|
+
except Exception:
|
|
929
|
+
pass
|
|
930
|
+
|
|
797
931
|
enqueued = 0
|
|
798
932
|
for turn in missing_turns:
|
|
799
933
|
try:
|
|
@@ -802,6 +936,7 @@ class DialogueWatcher:
|
|
|
802
936
|
workspace_path=project_path,
|
|
803
937
|
turn_number=turn,
|
|
804
938
|
session_type=session_type,
|
|
939
|
+
agent_id=agent_id if agent_id else None,
|
|
805
940
|
)
|
|
806
941
|
enqueued += 1
|
|
807
942
|
except Exception as e:
|
|
@@ -935,6 +1070,22 @@ class DialogueWatcher:
|
|
|
935
1070
|
continue
|
|
936
1071
|
|
|
937
1072
|
enqueued_any = False
|
|
1073
|
+
agent_id = None
|
|
1074
|
+
if session_type == "codex":
|
|
1075
|
+
agent_id = self._agent_info_id_for_codex_session(session_file, db=db)
|
|
1076
|
+
terminal_id = self._terminal_id_for_codex_session(session_file)
|
|
1077
|
+
if terminal_id:
|
|
1078
|
+
try:
|
|
1079
|
+
db.insert_window_link(
|
|
1080
|
+
terminal_id=terminal_id,
|
|
1081
|
+
agent_id=agent_id,
|
|
1082
|
+
session_id=session_id,
|
|
1083
|
+
provider="codex",
|
|
1084
|
+
source="codex:watcher",
|
|
1085
|
+
ts=time.time(),
|
|
1086
|
+
)
|
|
1087
|
+
except Exception:
|
|
1088
|
+
pass
|
|
938
1089
|
for turn_number in sorted(set(new_turns)):
|
|
939
1090
|
try:
|
|
940
1091
|
db.enqueue_turn_summary_job( # type: ignore[attr-defined]
|
|
@@ -942,6 +1093,7 @@ class DialogueWatcher:
|
|
|
942
1093
|
workspace_path=project_path,
|
|
943
1094
|
turn_number=int(turn_number),
|
|
944
1095
|
session_type=session_type,
|
|
1096
|
+
agent_id=agent_id if agent_id else None,
|
|
945
1097
|
)
|
|
946
1098
|
enqueued_any = True
|
|
947
1099
|
except Exception as e:
|
|
@@ -1192,13 +1344,31 @@ class DialogueWatcher:
|
|
|
1192
1344
|
continue
|
|
1193
1345
|
|
|
1194
1346
|
enqueued_any = False
|
|
1347
|
+
session_type = self._detect_session_type(session_file)
|
|
1348
|
+
agent_id = None
|
|
1349
|
+
if session_type == "codex":
|
|
1350
|
+
agent_id = self._agent_info_id_for_codex_session(session_file, db=db)
|
|
1351
|
+
terminal_id = self._terminal_id_for_codex_session(session_file)
|
|
1352
|
+
if terminal_id:
|
|
1353
|
+
try:
|
|
1354
|
+
db.insert_window_link(
|
|
1355
|
+
terminal_id=terminal_id,
|
|
1356
|
+
agent_id=agent_id,
|
|
1357
|
+
session_id=session_file.stem,
|
|
1358
|
+
provider="codex",
|
|
1359
|
+
source="codex:watcher",
|
|
1360
|
+
ts=time.time(),
|
|
1361
|
+
)
|
|
1362
|
+
except Exception:
|
|
1363
|
+
pass
|
|
1195
1364
|
for turn_number in new_turns:
|
|
1196
1365
|
try:
|
|
1197
1366
|
db.enqueue_turn_summary_job( # type: ignore[attr-defined]
|
|
1198
1367
|
session_file_path=session_file,
|
|
1199
1368
|
workspace_path=project_path,
|
|
1200
1369
|
turn_number=turn_number,
|
|
1201
|
-
session_type=
|
|
1370
|
+
session_type=session_type,
|
|
1371
|
+
agent_id=agent_id if agent_id else None,
|
|
1202
1372
|
)
|
|
1203
1373
|
enqueued_any = True
|
|
1204
1374
|
except Exception as e:
|
|
@@ -1248,11 +1418,29 @@ class DialogueWatcher:
|
|
|
1248
1418
|
|
|
1249
1419
|
for turn_number in new_turns:
|
|
1250
1420
|
try:
|
|
1421
|
+
session_type = self._detect_session_type(session_file)
|
|
1422
|
+
agent_id = None
|
|
1423
|
+
if session_type == "codex":
|
|
1424
|
+
agent_id = self._agent_info_id_for_codex_session(session_file, db=db)
|
|
1425
|
+
terminal_id = self._terminal_id_for_codex_session(session_file)
|
|
1426
|
+
if terminal_id:
|
|
1427
|
+
try:
|
|
1428
|
+
db.insert_window_link(
|
|
1429
|
+
terminal_id=terminal_id,
|
|
1430
|
+
agent_id=agent_id,
|
|
1431
|
+
session_id=session_file.stem,
|
|
1432
|
+
provider="codex",
|
|
1433
|
+
source="codex:watcher",
|
|
1434
|
+
ts=time.time(),
|
|
1435
|
+
)
|
|
1436
|
+
except Exception:
|
|
1437
|
+
pass
|
|
1251
1438
|
db.enqueue_turn_summary_job( # type: ignore[attr-defined]
|
|
1252
1439
|
session_file_path=session_file,
|
|
1253
1440
|
workspace_path=project_path,
|
|
1254
1441
|
turn_number=turn_number,
|
|
1255
|
-
session_type=
|
|
1442
|
+
session_type=session_type,
|
|
1443
|
+
agent_id=agent_id if agent_id else None,
|
|
1256
1444
|
)
|
|
1257
1445
|
except Exception:
|
|
1258
1446
|
continue
|
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,16 @@ 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 session_id:
|
|
228
|
+
try:
|
|
229
|
+
if agent_id:
|
|
230
|
+
self.db.update_session_agent_id(session_id, agent_id)
|
|
231
|
+
else:
|
|
232
|
+
self._maybe_link_session_from_terminal(session_id)
|
|
233
|
+
except Exception:
|
|
234
|
+
pass
|
|
235
|
+
|
|
219
236
|
if created:
|
|
220
237
|
if expected_turns:
|
|
221
238
|
self._enqueue_session_summary_if_complete(session_id, expected_turns)
|
|
@@ -247,6 +264,38 @@ class AlineWorker:
|
|
|
247
264
|
except Exception as e:
|
|
248
265
|
logger.warning(f"Failed to enqueue session summary after import for {session_id}: {e}")
|
|
249
266
|
|
|
267
|
+
def _maybe_link_session_from_terminal(self, session_id: str) -> None:
|
|
268
|
+
"""Best-effort: backfill sessions.agent_id using terminal mapping."""
|
|
269
|
+
try:
|
|
270
|
+
session = self.db.get_session_by_id(session_id)
|
|
271
|
+
if session and getattr(session, "agent_id", None):
|
|
272
|
+
return
|
|
273
|
+
except Exception:
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
agents = self.db.list_agents(status=None, limit=1000)
|
|
278
|
+
except Exception:
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
agent_info_id = None
|
|
282
|
+
for agent in agents:
|
|
283
|
+
try:
|
|
284
|
+
if (agent.session_id or "").strip() != session_id:
|
|
285
|
+
continue
|
|
286
|
+
source = (agent.source or "").strip()
|
|
287
|
+
if source.startswith("agent:"):
|
|
288
|
+
agent_info_id = source[6:]
|
|
289
|
+
break
|
|
290
|
+
except Exception:
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
if agent_info_id:
|
|
294
|
+
try:
|
|
295
|
+
self.db.update_session_agent_id(session_id, agent_info_id)
|
|
296
|
+
except Exception:
|
|
297
|
+
pass
|
|
298
|
+
|
|
250
299
|
async def _process_session_summary_job(self, payload: Dict[str, Any]) -> bool:
|
|
251
300
|
session_id = str(payload.get("session_id") or "")
|
|
252
301
|
if not session_id:
|
|
@@ -255,3 +304,12 @@ class AlineWorker:
|
|
|
255
304
|
from .events.session_summarizer import update_session_summary_now
|
|
256
305
|
|
|
257
306
|
return bool(update_session_summary_now(self.db, session_id))
|
|
307
|
+
|
|
308
|
+
async def _process_agent_description_job(self, payload: Dict[str, Any]) -> None:
|
|
309
|
+
agent_id = str(payload.get("agent_id") or "")
|
|
310
|
+
if not agent_id:
|
|
311
|
+
raise ValueError(f"Invalid agent_description payload: {payload}")
|
|
312
|
+
|
|
313
|
+
from .events.agent_summarizer import force_update_agent_description
|
|
314
|
+
|
|
315
|
+
force_update_agent_description(self.db, agent_id)
|