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.
- {aline_ai-0.6.6.dist-info → aline_ai-0.6.7.dist-info}/METADATA +1 -1
- {aline_ai-0.6.6.dist-info → aline_ai-0.6.7.dist-info}/RECORD +25 -25
- realign/__init__.py +1 -1
- realign/agent_names.py +2 -2
- realign/claude_hooks/terminal_state.py +32 -1
- realign/codex_detector.py +17 -2
- realign/codex_home.py +24 -6
- realign/commands/doctor.py +74 -1
- realign/commands/export_shares.py +151 -0
- realign/commands/import_shares.py +203 -1
- realign/commands/sync_agent.py +347 -0
- realign/dashboard/screens/create_agent_info.py +131 -20
- realign/dashboard/styles/dashboard.tcss +0 -73
- realign/dashboard/tmux_manager.py +36 -10
- realign/dashboard/widgets/__init__.py +0 -2
- realign/dashboard/widgets/agents_panel.py +142 -23
- realign/db/base.py +43 -1
- realign/db/schema.py +60 -2
- realign/db/sqlite_db.py +176 -1
- realign/watcher_core.py +133 -2
- realign/worker_core.py +37 -2
- realign/dashboard/widgets/terminal_panel.py +0 -1688
- {aline_ai-0.6.6.dist-info → aline_ai-0.6.7.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.6.dist-info → aline_ai-0.6.7.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.6.dist-info → aline_ai-0.6.7.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.6.dist-info → aline_ai-0.6.7.dist-info}/top_level.txt +0 -0
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 =
|
|
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
|
|
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=
|
|
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=
|
|
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
|
|
227
|
+
if session_id:
|
|
228
228
|
try:
|
|
229
|
-
|
|
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:
|