aline-ai 0.6.6__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.
realign/db/base.py CHANGED
@@ -129,7 +129,7 @@ class AgentRecord:
129
129
 
130
130
  @dataclass
131
131
  class AgentInfoRecord:
132
- """Agent profile/identity data (V20)."""
132
+ """Agent profile/identity data (V20, V22: sync fields)."""
133
133
 
134
134
  id: str
135
135
  name: str
@@ -137,6 +137,27 @@ class AgentInfoRecord:
137
137
  updated_at: datetime
138
138
  description: Optional[str] = ""
139
139
  visibility: str = "visible"
140
+ # V22: sync metadata
141
+ share_id: Optional[str] = None
142
+ share_url: Optional[str] = None
143
+ share_admin_token: Optional[str] = None
144
+ share_contributor_token: Optional[str] = None
145
+ share_expiry_at: Optional[str] = None
146
+ last_synced_at: Optional[str] = None
147
+ sync_version: int = 0
148
+
149
+
150
+ @dataclass
151
+ class WindowLinkRecord:
152
+ """Represents a terminal/session association (V23)."""
153
+
154
+ terminal_id: str
155
+ agent_id: Optional[str] = None
156
+ session_id: Optional[str] = None
157
+ provider: Optional[str] = None
158
+ source: Optional[str] = None
159
+ ts: Optional[float] = None
160
+ created_at: Optional[datetime] = None
140
161
 
141
162
 
142
163
  @dataclass
@@ -228,6 +249,27 @@ class DatabaseInterface(ABC):
228
249
  """Backfill total_turns for all sessions from turns table (V10 migration)."""
229
250
  pass
230
251
 
252
+ @abstractmethod
253
+ def insert_window_link(
254
+ self,
255
+ *,
256
+ terminal_id: str,
257
+ agent_id: Optional[str],
258
+ session_id: Optional[str],
259
+ provider: Optional[str],
260
+ source: Optional[str],
261
+ ts: Optional[float] = None,
262
+ ) -> None:
263
+ """Insert a window link record (V23)."""
264
+ pass
265
+
266
+ @abstractmethod
267
+ def list_latest_window_links(
268
+ self, *, agent_id: Optional[str] = None, limit: int = 1000
269
+ ) -> List[WindowLinkRecord]:
270
+ """List latest window link per terminal (V23)."""
271
+ pass
272
+
231
273
  @abstractmethod
232
274
  def list_sessions(
233
275
  self, limit: int = 100, workspace_path: Optional[str] = None
realign/db/schema.py CHANGED
@@ -82,9 +82,12 @@ Schema V19: Agent association for sessions.
82
82
 
83
83
  Schema V20: Agent identity/profile table.
84
84
  - agent_info table: name, description for agent profiles
85
+
86
+ Schema V23: WindowLink mapping for terminal/session association.
87
+ - windowlink table: terminal_id/agent_id/session_id with timestamp
85
88
  """
86
89
 
87
- SCHEMA_VERSION = 21
90
+ SCHEMA_VERSION = 23
88
91
 
89
92
  FTS_EVENTS_SCRIPTS = [
90
93
  # Full Text Search for Events
@@ -343,13 +346,20 @@ INIT_SCRIPTS = [
343
346
  updated_at TEXT DEFAULT (datetime('now'))
344
347
  );
345
348
  """,
346
- # Agent identity/profile table (V20)
349
+ # Agent identity/profile table (V20, V22: sync columns)
347
350
  """
348
351
  CREATE TABLE IF NOT EXISTS agent_info (
349
352
  id TEXT PRIMARY KEY,
350
353
  name TEXT NOT NULL,
351
354
  description TEXT DEFAULT '',
352
355
  visibility TEXT NOT NULL DEFAULT 'visible',
356
+ share_id TEXT,
357
+ share_url TEXT,
358
+ share_admin_token TEXT,
359
+ share_contributor_token TEXT,
360
+ share_expiry_at TEXT,
361
+ last_synced_at TEXT,
362
+ sync_version INTEGER DEFAULT 0,
353
363
  created_at TEXT DEFAULT (datetime('now')),
354
364
  updated_at TEXT DEFAULT (datetime('now'))
355
365
  );
@@ -405,6 +415,21 @@ MIGRATION_V1_TO_V2 = [
405
415
  """
406
416
  CREATE INDEX IF NOT EXISTS idx_sessions_type ON sessions(session_type);
407
417
  """,
418
+ # WindowLink table (V23)
419
+ """
420
+ CREATE TABLE IF NOT EXISTS windowlink (
421
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
422
+ terminal_id TEXT NOT NULL,
423
+ agent_id TEXT,
424
+ session_id TEXT,
425
+ provider TEXT,
426
+ source TEXT,
427
+ ts REAL,
428
+ created_at TEXT DEFAULT (datetime('now'))
429
+ );
430
+ """,
431
+ "CREATE INDEX IF NOT EXISTS idx_windowlink_terminal_ts ON windowlink(terminal_id, ts DESC);",
432
+ "CREATE INDEX IF NOT EXISTS idx_windowlink_agent_ts ON windowlink(agent_id, ts DESC);",
408
433
  ]
409
434
 
410
435
  # Migration scripts from V2 to V3
@@ -693,6 +718,33 @@ MIGRATION_V20_TO_V21 = [
693
718
  "ALTER TABLE agent_info ADD COLUMN visibility TEXT NOT NULL DEFAULT 'visible';",
694
719
  ]
695
720
 
721
+ MIGRATION_V21_TO_V22 = [
722
+ "ALTER TABLE agent_info ADD COLUMN share_id TEXT;",
723
+ "ALTER TABLE agent_info ADD COLUMN share_url TEXT;",
724
+ "ALTER TABLE agent_info ADD COLUMN share_admin_token TEXT;",
725
+ "ALTER TABLE agent_info ADD COLUMN share_contributor_token TEXT;",
726
+ "ALTER TABLE agent_info ADD COLUMN share_expiry_at TEXT;",
727
+ "ALTER TABLE agent_info ADD COLUMN last_synced_at TEXT;",
728
+ "ALTER TABLE agent_info ADD COLUMN sync_version INTEGER DEFAULT 0;",
729
+ ]
730
+
731
+ MIGRATION_V22_TO_V23 = [
732
+ """
733
+ CREATE TABLE IF NOT EXISTS windowlink (
734
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
735
+ terminal_id TEXT NOT NULL,
736
+ agent_id TEXT,
737
+ session_id TEXT,
738
+ provider TEXT,
739
+ source TEXT,
740
+ ts REAL,
741
+ created_at TEXT DEFAULT (datetime('now'))
742
+ );
743
+ """,
744
+ "CREATE INDEX IF NOT EXISTS idx_windowlink_terminal_ts ON windowlink(terminal_id, ts DESC);",
745
+ "CREATE INDEX IF NOT EXISTS idx_windowlink_agent_ts ON windowlink(agent_id, ts DESC);",
746
+ ]
747
+
696
748
 
697
749
  def get_migration_scripts(from_version: int, to_version: int) -> list:
698
750
  """Get migration scripts for upgrading between versions."""
@@ -767,4 +819,10 @@ def get_migration_scripts(from_version: int, to_version: int) -> list:
767
819
  if from_version < 21 and to_version >= 21:
768
820
  scripts.extend(MIGRATION_V20_TO_V21)
769
821
 
822
+ if from_version < 22 and to_version >= 22:
823
+ scripts.extend(MIGRATION_V21_TO_V22)
824
+
825
+ if from_version < 23 and to_version >= 23:
826
+ scripts.extend(MIGRATION_V22_TO_V23)
827
+
770
828
  return scripts
realign/db/sqlite_db.py CHANGED
@@ -23,6 +23,7 @@ from .base import (
23
23
  AgentRecord,
24
24
  AgentInfoRecord,
25
25
  AgentContextRecord,
26
+ WindowLinkRecord,
26
27
  UserRecord,
27
28
  )
28
29
  from .schema import (
@@ -2395,6 +2396,92 @@ class SQLiteDatabase(DatabaseInterface):
2395
2396
  except sqlite3.OperationalError:
2396
2397
  return []
2397
2398
 
2399
+ def insert_window_link(
2400
+ self,
2401
+ *,
2402
+ terminal_id: str,
2403
+ agent_id: Optional[str],
2404
+ session_id: Optional[str],
2405
+ provider: Optional[str],
2406
+ source: Optional[str],
2407
+ ts: Optional[float] = None,
2408
+ ) -> None:
2409
+ conn = self._get_connection()
2410
+ cursor = conn.cursor()
2411
+ ts_value = ts if ts is not None else time.time()
2412
+ try:
2413
+ cursor.execute(
2414
+ """
2415
+ INSERT INTO windowlink (
2416
+ terminal_id, agent_id, session_id, provider, source, ts
2417
+ ) VALUES (?, ?, ?, ?, ?, ?)
2418
+ """,
2419
+ (
2420
+ terminal_id,
2421
+ agent_id,
2422
+ session_id,
2423
+ provider,
2424
+ source,
2425
+ float(ts_value),
2426
+ ),
2427
+ )
2428
+ conn.commit()
2429
+ except sqlite3.OperationalError:
2430
+ conn.rollback()
2431
+
2432
+ def list_latest_window_links(
2433
+ self, *, agent_id: Optional[str] = None, limit: int = 1000
2434
+ ) -> List[WindowLinkRecord]:
2435
+ conn = self._get_connection()
2436
+ cursor = conn.cursor()
2437
+ params: List[Any] = []
2438
+ where_clause = ""
2439
+ if agent_id:
2440
+ where_clause = "WHERE agent_id = ?"
2441
+ params.append(agent_id)
2442
+ params.append(limit)
2443
+ try:
2444
+ cursor.execute(
2445
+ f"""
2446
+ SELECT terminal_id, agent_id, session_id, provider, source, ts, created_at
2447
+ FROM (
2448
+ SELECT terminal_id,
2449
+ agent_id,
2450
+ session_id,
2451
+ provider,
2452
+ source,
2453
+ ts,
2454
+ created_at,
2455
+ ROW_NUMBER() OVER (
2456
+ PARTITION BY terminal_id
2457
+ ORDER BY ts DESC, id DESC
2458
+ ) AS rn
2459
+ FROM windowlink
2460
+ {where_clause}
2461
+ ) AS ranked
2462
+ WHERE rn = 1
2463
+ LIMIT ?
2464
+ """,
2465
+ params,
2466
+ )
2467
+ rows = cursor.fetchall()
2468
+ out: List[WindowLinkRecord] = []
2469
+ for row in rows:
2470
+ out.append(
2471
+ WindowLinkRecord(
2472
+ terminal_id=row[0],
2473
+ agent_id=row[1],
2474
+ session_id=row[2],
2475
+ provider=row[3],
2476
+ source=row[4],
2477
+ ts=row[5],
2478
+ created_at=self._parse_datetime(row[6]) if row[6] else None,
2479
+ )
2480
+ )
2481
+ return out
2482
+ except sqlite3.OperationalError:
2483
+ return []
2484
+
2398
2485
  def delete_agent(self, agent_id: str) -> bool:
2399
2486
  """Delete an agent by ID."""
2400
2487
  conn = self._get_connection()
@@ -3081,12 +3168,20 @@ class SQLiteDatabase(DatabaseInterface):
3081
3168
 
3082
3169
  def _row_to_agent_info(self, row: sqlite3.Row) -> AgentInfoRecord:
3083
3170
  """Convert a database row to an AgentInfoRecord."""
3171
+ keys = row.keys()
3084
3172
  visibility = "visible"
3085
3173
  try:
3086
- if "visibility" in row.keys():
3174
+ if "visibility" in keys:
3087
3175
  visibility = row["visibility"] or "visible"
3088
3176
  except Exception:
3089
3177
  visibility = "visible"
3178
+
3179
+ def _safe_get(key: str, default=None):
3180
+ try:
3181
+ return row[key] if key in keys else default
3182
+ except Exception:
3183
+ return default
3184
+
3090
3185
  return AgentInfoRecord(
3091
3186
  id=row["id"],
3092
3187
  name=row["name"],
@@ -3094,6 +3189,13 @@ class SQLiteDatabase(DatabaseInterface):
3094
3189
  created_at=self._parse_datetime(row["created_at"]),
3095
3190
  updated_at=self._parse_datetime(row["updated_at"]),
3096
3191
  visibility=visibility,
3192
+ share_id=_safe_get("share_id"),
3193
+ share_url=_safe_get("share_url"),
3194
+ share_admin_token=_safe_get("share_admin_token"),
3195
+ share_contributor_token=_safe_get("share_contributor_token"),
3196
+ share_expiry_at=_safe_get("share_expiry_at"),
3197
+ last_synced_at=_safe_get("last_synced_at"),
3198
+ sync_version=_safe_get("sync_version", 0) or 0,
3097
3199
  )
3098
3200
 
3099
3201
  def get_or_create_agent_info(
@@ -3198,3 +3300,76 @@ class SQLiteDatabase(DatabaseInterface):
3198
3300
  return [self._row_to_agent_info(row) for row in cursor.fetchall()]
3199
3301
  except sqlite3.OperationalError:
3200
3302
  return []
3303
+
3304
+ def update_agent_sync_metadata(
3305
+ self,
3306
+ agent_id: str,
3307
+ *,
3308
+ share_id: Optional[str] = None,
3309
+ share_url: Optional[str] = None,
3310
+ share_admin_token: Optional[str] = None,
3311
+ share_contributor_token: Optional[str] = None,
3312
+ share_expiry_at: Optional[str] = None,
3313
+ last_synced_at: Optional[str] = None,
3314
+ sync_version: Optional[int] = None,
3315
+ ) -> Optional[AgentInfoRecord]:
3316
+ """Update sync-related metadata on an agent_info record."""
3317
+ conn = self._get_connection()
3318
+ sets: list[str] = []
3319
+ params: list[Any] = []
3320
+
3321
+ if share_id is not None:
3322
+ sets.append("share_id = ?")
3323
+ params.append(share_id)
3324
+ if share_url is not None:
3325
+ sets.append("share_url = ?")
3326
+ params.append(share_url)
3327
+ if share_admin_token is not None:
3328
+ sets.append("share_admin_token = ?")
3329
+ params.append(share_admin_token)
3330
+ if share_contributor_token is not None:
3331
+ sets.append("share_contributor_token = ?")
3332
+ params.append(share_contributor_token)
3333
+ if share_expiry_at is not None:
3334
+ sets.append("share_expiry_at = ?")
3335
+ params.append(share_expiry_at)
3336
+ if last_synced_at is not None:
3337
+ sets.append("last_synced_at = ?")
3338
+ params.append(last_synced_at)
3339
+ if sync_version is not None:
3340
+ sets.append("sync_version = ?")
3341
+ params.append(sync_version)
3342
+
3343
+ if not sets:
3344
+ return self.get_agent_info(agent_id)
3345
+
3346
+ sets.append("updated_at = datetime('now')")
3347
+ params.append(agent_id)
3348
+
3349
+ try:
3350
+ conn.execute(
3351
+ f"UPDATE agent_info SET {', '.join(sets)} WHERE id = ?",
3352
+ params,
3353
+ )
3354
+ conn.commit()
3355
+ except sqlite3.OperationalError:
3356
+ return None
3357
+
3358
+ return self.get_agent_info(agent_id)
3359
+
3360
+ def get_agent_content_hashes(self, agent_id: str) -> set[str]:
3361
+ """Return all content_hash values for turns in this agent's sessions."""
3362
+ conn = self._get_connection()
3363
+ try:
3364
+ cursor = conn.execute(
3365
+ """
3366
+ SELECT DISTINCT t.content_hash
3367
+ FROM turns t
3368
+ JOIN sessions s ON t.session_id = s.id
3369
+ WHERE s.agent_id = ?
3370
+ """,
3371
+ (agent_id,),
3372
+ )
3373
+ return {row["content_hash"] for row in cursor.fetchall()}
3374
+ except sqlite3.OperationalError:
3375
+ return set()
realign/watcher_core.py CHANGED
@@ -259,6 +259,54 @@ 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."""
266
+ try:
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
+
262
310
  try:
263
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
@@ -326,6 +374,18 @@ class DialogueWatcher:
326
374
  source=source,
327
375
  )
328
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
+
329
389
  # Link session to agent_info if available (bidirectional linking)
330
390
  if agent_info_id:
331
391
  try:
@@ -851,6 +911,23 @@ class DialogueWatcher:
851
911
  file=sys.stderr,
852
912
  )
853
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
+
854
931
  enqueued = 0
855
932
  for turn in missing_turns:
856
933
  try:
@@ -859,6 +936,7 @@ class DialogueWatcher:
859
936
  workspace_path=project_path,
860
937
  turn_number=turn,
861
938
  session_type=session_type,
939
+ agent_id=agent_id if agent_id else None,
862
940
  )
863
941
  enqueued += 1
864
942
  except Exception as e:
@@ -992,6 +1070,22 @@ class DialogueWatcher:
992
1070
  continue
993
1071
 
994
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
995
1089
  for turn_number in sorted(set(new_turns)):
996
1090
  try:
997
1091
  db.enqueue_turn_summary_job( # type: ignore[attr-defined]
@@ -999,6 +1093,7 @@ class DialogueWatcher:
999
1093
  workspace_path=project_path,
1000
1094
  turn_number=int(turn_number),
1001
1095
  session_type=session_type,
1096
+ agent_id=agent_id if agent_id else None,
1002
1097
  )
1003
1098
  enqueued_any = True
1004
1099
  except Exception as e:
@@ -1249,13 +1344,31 @@ class DialogueWatcher:
1249
1344
  continue
1250
1345
 
1251
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
1252
1364
  for turn_number in new_turns:
1253
1365
  try:
1254
1366
  db.enqueue_turn_summary_job( # type: ignore[attr-defined]
1255
1367
  session_file_path=session_file,
1256
1368
  workspace_path=project_path,
1257
1369
  turn_number=turn_number,
1258
- session_type=self._detect_session_type(session_file),
1370
+ session_type=session_type,
1371
+ agent_id=agent_id if agent_id else None,
1259
1372
  )
1260
1373
  enqueued_any = True
1261
1374
  except Exception as e:
@@ -1305,11 +1418,29 @@ class DialogueWatcher:
1305
1418
 
1306
1419
  for turn_number in new_turns:
1307
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
1308
1438
  db.enqueue_turn_summary_job( # type: ignore[attr-defined]
1309
1439
  session_file_path=session_file,
1310
1440
  workspace_path=project_path,
1311
1441
  turn_number=turn_number,
1312
- session_type=self._detect_session_type(session_file),
1442
+ session_type=session_type,
1443
+ agent_id=agent_id if agent_id else None,
1313
1444
  )
1314
1445
  except Exception:
1315
1446
  continue
realign/worker_core.py CHANGED
@@ -224,9 +224,12 @@ class AlineWorker:
224
224
  )
225
225
 
226
226
  # Link session to agent after commit ensures session exists in DB
227
- if agent_id and session_id:
227
+ if session_id:
228
228
  try:
229
- self.db.update_session_agent_id(session_id, agent_id)
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)
230
233
  except Exception:
231
234
  pass
232
235
 
@@ -261,6 +264,38 @@ class AlineWorker:
261
264
  except Exception as e:
262
265
  logger.warning(f"Failed to enqueue session summary after import for {session_id}: {e}")
263
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
+
264
299
  async def _process_session_summary_job(self, payload: Dict[str, Any]) -> bool:
265
300
  session_id = str(payload.get("session_id") or "")
266
301
  if not session_id: