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.
Files changed (42) hide show
  1. {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/METADATA +1 -1
  2. {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.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 +43 -1
  7. realign/claude_hooks/user_prompt_submit_hook.py +3 -0
  8. realign/cli.py +62 -0
  9. realign/codex_detector.py +18 -3
  10. realign/codex_home.py +65 -16
  11. realign/codex_terminal_linker.py +18 -7
  12. realign/commands/agent.py +109 -0
  13. realign/commands/doctor.py +74 -1
  14. realign/commands/export_shares.py +448 -0
  15. realign/commands/import_shares.py +203 -1
  16. realign/commands/search.py +58 -29
  17. realign/commands/sync_agent.py +347 -0
  18. realign/dashboard/app.py +9 -9
  19. realign/dashboard/clipboard.py +54 -0
  20. realign/dashboard/screens/__init__.py +4 -0
  21. realign/dashboard/screens/agent_detail.py +333 -0
  22. realign/dashboard/screens/create_agent_info.py +244 -0
  23. realign/dashboard/screens/event_detail.py +6 -27
  24. realign/dashboard/styles/dashboard.tcss +22 -28
  25. realign/dashboard/tmux_manager.py +36 -10
  26. realign/dashboard/widgets/__init__.py +2 -2
  27. realign/dashboard/widgets/agents_panel.py +1248 -0
  28. realign/dashboard/widgets/events_table.py +4 -27
  29. realign/dashboard/widgets/sessions_table.py +4 -27
  30. realign/db/base.py +69 -0
  31. realign/db/locks.py +4 -0
  32. realign/db/schema.py +111 -2
  33. realign/db/sqlite_db.py +360 -2
  34. realign/events/agent_summarizer.py +157 -0
  35. realign/events/session_summarizer.py +25 -0
  36. realign/watcher_core.py +193 -5
  37. realign/worker_core.py +59 -1
  38. realign/dashboard/widgets/terminal_panel.py +0 -1653
  39. {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/WHEEL +0 -0
  40. {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/entry_points.txt +0 -0
  41. {aline_ai-0.6.5.dist-info → aline_ai-0.6.7.dist-info}/licenses/LICENSE +0 -0
  42. {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 terminal_id_from_codex_session_file
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
- agent_id = terminal_id_from_codex_session_file(session_file)
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="codex:auto-link",
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=self._detect_session_type(session_file),
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=self._detect_session_type(session_file),
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)