aline-ai 0.6.5__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.5.dist-info → aline_ai-0.6.6.dist-info}/METADATA +1 -1
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/RECORD +38 -31
- 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/export_shares.py +297 -0
- realign/commands/search.py +58 -29
- 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 +133 -0
- realign/dashboard/screens/event_detail.py +6 -27
- realign/dashboard/styles/dashboard.tcss +67 -0
- realign/dashboard/widgets/__init__.py +2 -0
- realign/dashboard/widgets/agents_panel.py +1129 -0
- realign/dashboard/widgets/events_table.py +4 -27
- realign/dashboard/widgets/sessions_table.py +4 -27
- realign/dashboard/widgets/terminal_panel.py +40 -5
- 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.5.dist-info → aline_ai-0.6.6.dist-info}/WHEEL +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.6.5.dist-info → aline_ai-0.6.6.dist-info}/top_level.txt +0 -0
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import io
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
6
|
import traceback
|
|
10
7
|
from datetime import datetime
|
|
11
8
|
from typing import Optional, Set
|
|
@@ -20,6 +17,7 @@ from textual.worker import Worker, WorkerState
|
|
|
20
17
|
from textual.widgets import Button, DataTable, Static
|
|
21
18
|
|
|
22
19
|
from ...logging_config import setup_logger
|
|
20
|
+
from ..clipboard import copy_text
|
|
23
21
|
from .openable_table import OpenableDataTable
|
|
24
22
|
|
|
25
23
|
logger = setup_logger("realign.dashboard.events", "dashboard.log")
|
|
@@ -613,33 +611,12 @@ class EventsTable(Container):
|
|
|
613
611
|
|
|
614
612
|
# Build copy text
|
|
615
613
|
if slack_message:
|
|
616
|
-
|
|
614
|
+
text_to_copy = str(slack_message) + "\n\n" + str(share_link)
|
|
617
615
|
else:
|
|
618
|
-
|
|
616
|
+
text_to_copy = str(share_link)
|
|
619
617
|
|
|
620
618
|
# Copy to clipboard
|
|
621
|
-
copied =
|
|
622
|
-
if os.environ.get("TMUX") and shutil.which("pbcopy"):
|
|
623
|
-
try:
|
|
624
|
-
copied = (
|
|
625
|
-
subprocess.run(
|
|
626
|
-
["pbcopy"],
|
|
627
|
-
input=copy_text,
|
|
628
|
-
text=True,
|
|
629
|
-
capture_output=False,
|
|
630
|
-
check=False,
|
|
631
|
-
).returncode
|
|
632
|
-
== 0
|
|
633
|
-
)
|
|
634
|
-
except Exception:
|
|
635
|
-
copied = False
|
|
636
|
-
|
|
637
|
-
if not copied:
|
|
638
|
-
try:
|
|
639
|
-
self.app.copy_to_clipboard(copy_text)
|
|
640
|
-
copied = True
|
|
641
|
-
except Exception:
|
|
642
|
-
copied = False
|
|
619
|
+
copied = copy_text(self.app, text_to_copy)
|
|
643
620
|
|
|
644
621
|
suffix = " (copied to clipboard)" if copied else ""
|
|
645
622
|
self.app.notify(f"Share link created{suffix}", title="Share", timeout=4)
|
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import io
|
|
5
5
|
import json
|
|
6
|
-
import os
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
6
|
import traceback
|
|
10
7
|
from datetime import datetime
|
|
11
8
|
from pathlib import Path
|
|
@@ -20,6 +17,7 @@ from textual.worker import Worker, WorkerState
|
|
|
20
17
|
from textual.widgets import Button, DataTable, Select, Static
|
|
21
18
|
|
|
22
19
|
from ...logging_config import setup_logger
|
|
20
|
+
from ..clipboard import copy_text
|
|
23
21
|
from .openable_table import OpenableDataTable
|
|
24
22
|
|
|
25
23
|
logger = setup_logger("realign.dashboard.sessions", "dashboard.log")
|
|
@@ -685,33 +683,12 @@ class SessionsTable(Container):
|
|
|
685
683
|
|
|
686
684
|
# Build copy text
|
|
687
685
|
if slack_message:
|
|
688
|
-
|
|
686
|
+
text_to_copy = str(slack_message) + "\n\n" + str(share_link)
|
|
689
687
|
else:
|
|
690
|
-
|
|
688
|
+
text_to_copy = str(share_link)
|
|
691
689
|
|
|
692
690
|
# Copy to clipboard
|
|
693
|
-
copied =
|
|
694
|
-
if os.environ.get("TMUX") and shutil.which("pbcopy"):
|
|
695
|
-
try:
|
|
696
|
-
copied = (
|
|
697
|
-
subprocess.run(
|
|
698
|
-
["pbcopy"],
|
|
699
|
-
input=copy_text,
|
|
700
|
-
text=True,
|
|
701
|
-
capture_output=False,
|
|
702
|
-
check=False,
|
|
703
|
-
).returncode
|
|
704
|
-
== 0
|
|
705
|
-
)
|
|
706
|
-
except Exception:
|
|
707
|
-
copied = False
|
|
708
|
-
|
|
709
|
-
if not copied:
|
|
710
|
-
try:
|
|
711
|
-
self.app.copy_to_clipboard(copy_text)
|
|
712
|
-
copied = True
|
|
713
|
-
except Exception:
|
|
714
|
-
copied = False
|
|
691
|
+
copied = copy_text(self.app, text_to_copy)
|
|
715
692
|
|
|
716
693
|
suffix = " (copied to clipboard)" if copied else ""
|
|
717
694
|
self.app.notify(f"Share link created{suffix}", title="Share", timeout=4)
|
|
@@ -456,7 +456,7 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
456
456
|
try:
|
|
457
457
|
from ...codex_terminal_linker import read_codex_session_meta
|
|
458
458
|
from ...db import get_database
|
|
459
|
-
from ...codex_home import
|
|
459
|
+
from ...codex_home import codex_sessions_dir_for_terminal_or_agent
|
|
460
460
|
except Exception:
|
|
461
461
|
return
|
|
462
462
|
|
|
@@ -474,9 +474,12 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
474
474
|
return
|
|
475
475
|
|
|
476
476
|
candidates: list[Path] = []
|
|
477
|
-
|
|
477
|
+
agent_info_id: str | None = None
|
|
478
|
+
if (agent.source or "").startswith("agent:"):
|
|
479
|
+
agent_info_id = agent.source[6:]
|
|
480
|
+
sessions_root = codex_sessions_dir_for_terminal_or_agent(terminal_id, agent_info_id)
|
|
478
481
|
if sessions_root.exists():
|
|
479
|
-
# Deterministic: isolated per-terminal CODEX_HOME.
|
|
482
|
+
# Deterministic: isolated per-terminal/per-agent CODEX_HOME.
|
|
480
483
|
try:
|
|
481
484
|
candidates = list(sessions_root.rglob("rollout-*.jsonl"))
|
|
482
485
|
except Exception:
|
|
@@ -544,6 +547,9 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
544
547
|
return
|
|
545
548
|
|
|
546
549
|
try:
|
|
550
|
+
source = "dashboard:auto-link"
|
|
551
|
+
if (agent.source or "").startswith("agent:"):
|
|
552
|
+
source = agent.source or source
|
|
547
553
|
db.update_agent(
|
|
548
554
|
terminal_id,
|
|
549
555
|
provider="codex",
|
|
@@ -552,8 +558,13 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
552
558
|
transcript_path=str(best),
|
|
553
559
|
cwd=cwd,
|
|
554
560
|
project_dir=cwd,
|
|
555
|
-
source=
|
|
561
|
+
source=source,
|
|
556
562
|
)
|
|
563
|
+
if agent_info_id:
|
|
564
|
+
try:
|
|
565
|
+
db.update_session_agent_id(best.stem, agent_info_id)
|
|
566
|
+
except Exception:
|
|
567
|
+
pass
|
|
557
568
|
except Exception:
|
|
558
569
|
return
|
|
559
570
|
|
|
@@ -592,10 +603,16 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
592
603
|
try:
|
|
593
604
|
controls_enabled = self.supported()
|
|
594
605
|
with Horizontal(classes="summary"):
|
|
606
|
+
yield Button(
|
|
607
|
+
"+ New Agent",
|
|
608
|
+
id="quick-new-agent",
|
|
609
|
+
variant="primary",
|
|
610
|
+
disabled=not controls_enabled,
|
|
611
|
+
)
|
|
595
612
|
yield Button(
|
|
596
613
|
"+ Create",
|
|
597
614
|
id="new-agent",
|
|
598
|
-
variant="
|
|
615
|
+
variant="default",
|
|
599
616
|
disabled=not controls_enabled,
|
|
600
617
|
)
|
|
601
618
|
with Vertical(id="terminals", classes="list"):
|
|
@@ -1244,6 +1261,20 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1244
1261
|
"""Wrap a command to run in a specific directory."""
|
|
1245
1262
|
return f"cd {shlex.quote(directory)} && {command}"
|
|
1246
1263
|
|
|
1264
|
+
async def _quick_create_claude_agent(self) -> None:
|
|
1265
|
+
"""Quickly create a new Claude Code terminal with default settings.
|
|
1266
|
+
|
|
1267
|
+
Uses the last workspace (or cwd) with normal permissions and tracking enabled.
|
|
1268
|
+
"""
|
|
1269
|
+
from ..screens.create_agent import _load_last_workspace
|
|
1270
|
+
|
|
1271
|
+
workspace = _load_last_workspace()
|
|
1272
|
+
self.run_worker(
|
|
1273
|
+
self._create_agent("claude", workspace, skip_permissions=False, no_track=False),
|
|
1274
|
+
group="terminal-panel-create",
|
|
1275
|
+
exclusive=True,
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1247
1278
|
def _on_create_agent_result(self, result: tuple[str, str, bool, bool] | None) -> None:
|
|
1248
1279
|
"""Handle the result from CreateAgentScreen modal."""
|
|
1249
1280
|
if result is None:
|
|
@@ -1571,6 +1602,10 @@ class TerminalPanel(Container, can_focus=True):
|
|
|
1571
1602
|
)
|
|
1572
1603
|
return
|
|
1573
1604
|
|
|
1605
|
+
if button_id == "quick-new-agent":
|
|
1606
|
+
await self._quick_create_claude_agent()
|
|
1607
|
+
return
|
|
1608
|
+
|
|
1574
1609
|
if button_id == "new-agent":
|
|
1575
1610
|
from ..screens import CreateAgentScreen
|
|
1576
1611
|
|
realign/db/base.py
CHANGED
|
@@ -57,6 +57,8 @@ class SessionRecord:
|
|
|
57
57
|
total_turns: Optional[int] = None
|
|
58
58
|
# V12: file mtime when total_turns was cached (for validation)
|
|
59
59
|
total_turns_mtime: Optional[float] = None
|
|
60
|
+
# V19: agent association
|
|
61
|
+
agent_id: Optional[str] = None
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
@dataclass
|
|
@@ -125,6 +127,18 @@ class AgentRecord:
|
|
|
125
127
|
created_by: Optional[str] = None # Creator UID
|
|
126
128
|
|
|
127
129
|
|
|
130
|
+
@dataclass
|
|
131
|
+
class AgentInfoRecord:
|
|
132
|
+
"""Agent profile/identity data (V20)."""
|
|
133
|
+
|
|
134
|
+
id: str
|
|
135
|
+
name: str
|
|
136
|
+
created_at: datetime
|
|
137
|
+
updated_at: datetime
|
|
138
|
+
description: Optional[str] = ""
|
|
139
|
+
visibility: str = "visible"
|
|
140
|
+
|
|
141
|
+
|
|
128
142
|
@dataclass
|
|
129
143
|
class AgentContextRecord:
|
|
130
144
|
"""Represents a context entry (V15: replaces load.json)."""
|
|
@@ -250,6 +264,19 @@ class DatabaseInterface(ABC):
|
|
|
250
264
|
"""
|
|
251
265
|
pass
|
|
252
266
|
|
|
267
|
+
@abstractmethod
|
|
268
|
+
def get_sessions_by_agent_id(self, agent_id: str, limit: int = 1000) -> List[SessionRecord]:
|
|
269
|
+
"""Get all sessions linked to an agent.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
agent_id: The agent_info ID
|
|
273
|
+
limit: Maximum number of sessions to return
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
List of SessionRecord objects for this agent
|
|
277
|
+
"""
|
|
278
|
+
pass
|
|
279
|
+
|
|
253
280
|
@abstractmethod
|
|
254
281
|
def get_turn_by_hash(self, session_id: str, content_hash: str) -> Optional[TurnRecord]:
|
|
255
282
|
"""Check if a turn with this content hash already exists in the session."""
|
realign/db/locks.py
CHANGED
|
@@ -31,6 +31,10 @@ def lock_key_for_event_summary(event_id: str) -> str:
|
|
|
31
31
|
return f"event_summary:{event_id}"
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def lock_key_for_agent_description(agent_id: str) -> str:
|
|
35
|
+
return f"agent_description:{agent_id}"
|
|
36
|
+
|
|
37
|
+
|
|
34
38
|
@contextmanager
|
|
35
39
|
def lease_lock(
|
|
36
40
|
db: DatabaseInterface,
|
realign/db/schema.py
CHANGED
|
@@ -76,9 +76,15 @@ Schema V18: UID refactor - created_by/shared_by with users table.
|
|
|
76
76
|
- turns: drop uid and user_name (inherit from session)
|
|
77
77
|
- agents: uid -> created_by, drop user_name
|
|
78
78
|
- Update indexes accordingly
|
|
79
|
+
|
|
80
|
+
Schema V19: Agent association for sessions.
|
|
81
|
+
- sessions.agent_id: Logical agent entity association
|
|
82
|
+
|
|
83
|
+
Schema V20: Agent identity/profile table.
|
|
84
|
+
- agent_info table: name, description for agent profiles
|
|
79
85
|
"""
|
|
80
86
|
|
|
81
|
-
SCHEMA_VERSION =
|
|
87
|
+
SCHEMA_VERSION = 21
|
|
82
88
|
|
|
83
89
|
FTS_EVENTS_SCRIPTS = [
|
|
84
90
|
# Full Text Search for Events
|
|
@@ -150,7 +156,8 @@ INIT_SCRIPTS = [
|
|
|
150
156
|
created_by TEXT, -- V18: Creator UID (FK to users.uid)
|
|
151
157
|
shared_by TEXT, -- V18: Sharer UID (who imported this)
|
|
152
158
|
total_turns INTEGER DEFAULT 0, -- V10: Cached total turn count (avoids reading files)
|
|
153
|
-
total_turns_mtime REAL
|
|
159
|
+
total_turns_mtime REAL, -- V12: File mtime when total_turns was cached (for validation)
|
|
160
|
+
agent_id TEXT -- V19: Logical agent association
|
|
154
161
|
);
|
|
155
162
|
""",
|
|
156
163
|
# Turns table (corresponds to git commits, V18: uid/user_name removed)
|
|
@@ -222,6 +229,7 @@ INIT_SCRIPTS = [
|
|
|
222
229
|
"CREATE INDEX IF NOT EXISTS idx_sessions_activity ON sessions(last_activity_at DESC);",
|
|
223
230
|
"CREATE INDEX IF NOT EXISTS idx_sessions_type ON sessions(session_type);",
|
|
224
231
|
"CREATE INDEX IF NOT EXISTS idx_sessions_created_by ON sessions(created_by);", # V18
|
|
232
|
+
"CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id);", # V19
|
|
225
233
|
"CREATE INDEX IF NOT EXISTS idx_turns_session ON turns(session_id);",
|
|
226
234
|
"CREATE INDEX IF NOT EXISTS idx_turns_timestamp ON turns(timestamp DESC);",
|
|
227
235
|
"CREATE INDEX IF NOT EXISTS idx_turns_hash ON turns(content_hash);",
|
|
@@ -335,6 +343,17 @@ INIT_SCRIPTS = [
|
|
|
335
343
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
336
344
|
);
|
|
337
345
|
""",
|
|
346
|
+
# Agent identity/profile table (V20)
|
|
347
|
+
"""
|
|
348
|
+
CREATE TABLE IF NOT EXISTS agent_info (
|
|
349
|
+
id TEXT PRIMARY KEY,
|
|
350
|
+
name TEXT NOT NULL,
|
|
351
|
+
description TEXT DEFAULT '',
|
|
352
|
+
visibility TEXT NOT NULL DEFAULT 'visible',
|
|
353
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
354
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
355
|
+
);
|
|
356
|
+
""",
|
|
338
357
|
*FTS_EVENTS_SCRIPTS,
|
|
339
358
|
]
|
|
340
359
|
|
|
@@ -652,6 +671,29 @@ MIGRATION_V17_TO_V18 = [
|
|
|
652
671
|
]
|
|
653
672
|
|
|
654
673
|
|
|
674
|
+
MIGRATION_V18_TO_V19 = [
|
|
675
|
+
"ALTER TABLE sessions ADD COLUMN agent_id TEXT;",
|
|
676
|
+
"CREATE INDEX IF NOT EXISTS idx_sessions_agent_id ON sessions(agent_id);",
|
|
677
|
+
]
|
|
678
|
+
|
|
679
|
+
MIGRATION_V19_TO_V20 = [
|
|
680
|
+
"""
|
|
681
|
+
CREATE TABLE IF NOT EXISTS agent_info (
|
|
682
|
+
id TEXT PRIMARY KEY,
|
|
683
|
+
name TEXT NOT NULL,
|
|
684
|
+
description TEXT DEFAULT '',
|
|
685
|
+
visibility TEXT NOT NULL DEFAULT 'visible',
|
|
686
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
687
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
688
|
+
);
|
|
689
|
+
""",
|
|
690
|
+
]
|
|
691
|
+
|
|
692
|
+
MIGRATION_V20_TO_V21 = [
|
|
693
|
+
"ALTER TABLE agent_info ADD COLUMN visibility TEXT NOT NULL DEFAULT 'visible';",
|
|
694
|
+
]
|
|
695
|
+
|
|
696
|
+
|
|
655
697
|
def get_migration_scripts(from_version: int, to_version: int) -> list:
|
|
656
698
|
"""Get migration scripts for upgrading between versions."""
|
|
657
699
|
scripts = []
|
|
@@ -716,4 +758,13 @@ def get_migration_scripts(from_version: int, to_version: int) -> list:
|
|
|
716
758
|
if from_version < 18 and to_version >= 18:
|
|
717
759
|
scripts.extend(MIGRATION_V17_TO_V18)
|
|
718
760
|
|
|
761
|
+
if from_version < 19 and to_version >= 19:
|
|
762
|
+
scripts.extend(MIGRATION_V18_TO_V19)
|
|
763
|
+
|
|
764
|
+
if from_version < 20 and to_version >= 20:
|
|
765
|
+
scripts.extend(MIGRATION_V19_TO_V20)
|
|
766
|
+
|
|
767
|
+
if from_version < 21 and to_version >= 21:
|
|
768
|
+
scripts.extend(MIGRATION_V20_TO_V21)
|
|
769
|
+
|
|
719
770
|
return scripts
|
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 []
|