aline-ai 0.5.9__py3-none-any.whl → 0.5.10__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.5.9.dist-info → aline_ai-0.5.10.dist-info}/METADATA +1 -1
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.10.dist-info}/RECORD +17 -16
- realign/__init__.py +1 -1
- realign/claude_hooks/terminal_state.py +86 -3
- realign/context.py +136 -6
- realign/dashboard/screens/create_agent.py +69 -4
- realign/dashboard/tmux_manager.py +62 -2
- realign/dashboard/widgets/terminal_panel.py +14 -7
- realign/db/__init__.py +44 -0
- realign/db/base.py +36 -0
- realign/db/migrate_agents.py +297 -0
- realign/db/schema.py +127 -1
- realign/db/sqlite_db.py +617 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.10.dist-info}/WHEEL +0 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.10.dist-info}/entry_points.txt +0 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.10.dist-info}/licenses/LICENSE +0 -0
- {aline_ai-0.5.9.dist-info → aline_ai-0.5.10.dist-info}/top_level.txt +0 -0
realign/db/sqlite_db.py
CHANGED
|
@@ -20,6 +20,8 @@ from .base import (
|
|
|
20
20
|
TurnRecord,
|
|
21
21
|
EventRecord,
|
|
22
22
|
LockRecord,
|
|
23
|
+
AgentRecord,
|
|
24
|
+
AgentContextRecord,
|
|
23
25
|
)
|
|
24
26
|
from .schema import (
|
|
25
27
|
INIT_SCRIPTS,
|
|
@@ -2024,6 +2026,572 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
2024
2026
|
self._connection.close()
|
|
2025
2027
|
self._connection = None
|
|
2026
2028
|
|
|
2029
|
+
# -------------------------------------------------------------------------
|
|
2030
|
+
# Agent methods (Schema V15 - replaces terminal.json)
|
|
2031
|
+
# -------------------------------------------------------------------------
|
|
2032
|
+
|
|
2033
|
+
def get_or_create_agent(
|
|
2034
|
+
self,
|
|
2035
|
+
agent_id: str,
|
|
2036
|
+
provider: str,
|
|
2037
|
+
session_type: str,
|
|
2038
|
+
*,
|
|
2039
|
+
session_id: Optional[str] = None,
|
|
2040
|
+
context_id: Optional[str] = None,
|
|
2041
|
+
transcript_path: Optional[str] = None,
|
|
2042
|
+
cwd: Optional[str] = None,
|
|
2043
|
+
project_dir: Optional[str] = None,
|
|
2044
|
+
source: Optional[str] = None,
|
|
2045
|
+
status: str = "active",
|
|
2046
|
+
attention: Optional[str] = None,
|
|
2047
|
+
) -> AgentRecord:
|
|
2048
|
+
"""Get existing agent or create new one."""
|
|
2049
|
+
conn = self._get_connection()
|
|
2050
|
+
cursor = conn.cursor()
|
|
2051
|
+
|
|
2052
|
+
try:
|
|
2053
|
+
cursor.execute("SELECT * FROM agents WHERE id = ?", (agent_id,))
|
|
2054
|
+
row = cursor.fetchone()
|
|
2055
|
+
if row:
|
|
2056
|
+
return self._row_to_agent(row)
|
|
2057
|
+
except sqlite3.OperationalError:
|
|
2058
|
+
# Table may not exist in older schema
|
|
2059
|
+
raise
|
|
2060
|
+
|
|
2061
|
+
# Get user identity from config
|
|
2062
|
+
try:
|
|
2063
|
+
from ..config import ReAlignConfig
|
|
2064
|
+
|
|
2065
|
+
config = ReAlignConfig.load()
|
|
2066
|
+
creator_name = config.user_name
|
|
2067
|
+
creator_id = config.user_id
|
|
2068
|
+
except Exception:
|
|
2069
|
+
creator_name = None
|
|
2070
|
+
creator_id = None
|
|
2071
|
+
|
|
2072
|
+
now = datetime.now()
|
|
2073
|
+
cursor.execute(
|
|
2074
|
+
"""
|
|
2075
|
+
INSERT INTO agents (
|
|
2076
|
+
id, provider, session_type, session_id, context_id,
|
|
2077
|
+
transcript_path, cwd, project_dir, status, attention, source,
|
|
2078
|
+
created_at, updated_at, creator_name, creator_id
|
|
2079
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2080
|
+
""",
|
|
2081
|
+
(
|
|
2082
|
+
agent_id,
|
|
2083
|
+
provider,
|
|
2084
|
+
session_type,
|
|
2085
|
+
session_id,
|
|
2086
|
+
context_id,
|
|
2087
|
+
transcript_path,
|
|
2088
|
+
cwd,
|
|
2089
|
+
project_dir,
|
|
2090
|
+
status,
|
|
2091
|
+
attention,
|
|
2092
|
+
source,
|
|
2093
|
+
now,
|
|
2094
|
+
now,
|
|
2095
|
+
creator_name,
|
|
2096
|
+
creator_id,
|
|
2097
|
+
),
|
|
2098
|
+
)
|
|
2099
|
+
conn.commit()
|
|
2100
|
+
|
|
2101
|
+
return AgentRecord(
|
|
2102
|
+
id=agent_id,
|
|
2103
|
+
provider=provider,
|
|
2104
|
+
session_type=session_type,
|
|
2105
|
+
session_id=session_id,
|
|
2106
|
+
context_id=context_id,
|
|
2107
|
+
transcript_path=transcript_path,
|
|
2108
|
+
cwd=cwd,
|
|
2109
|
+
project_dir=project_dir,
|
|
2110
|
+
status=status,
|
|
2111
|
+
attention=attention,
|
|
2112
|
+
source=source,
|
|
2113
|
+
created_at=now,
|
|
2114
|
+
updated_at=now,
|
|
2115
|
+
creator_name=creator_name,
|
|
2116
|
+
creator_id=creator_id,
|
|
2117
|
+
)
|
|
2118
|
+
|
|
2119
|
+
def update_agent(
|
|
2120
|
+
self,
|
|
2121
|
+
agent_id: str,
|
|
2122
|
+
*,
|
|
2123
|
+
provider: Optional[str] = None,
|
|
2124
|
+
session_type: Optional[str] = None,
|
|
2125
|
+
session_id: Optional[str] = None,
|
|
2126
|
+
context_id: Optional[str] = None,
|
|
2127
|
+
transcript_path: Optional[str] = None,
|
|
2128
|
+
cwd: Optional[str] = None,
|
|
2129
|
+
project_dir: Optional[str] = None,
|
|
2130
|
+
status: Optional[str] = None,
|
|
2131
|
+
attention: Optional[str] = None,
|
|
2132
|
+
source: Optional[str] = None,
|
|
2133
|
+
) -> Optional[AgentRecord]:
|
|
2134
|
+
"""Update an existing agent. Returns the updated record or None if not found."""
|
|
2135
|
+
conn = self._get_connection()
|
|
2136
|
+
cursor = conn.cursor()
|
|
2137
|
+
|
|
2138
|
+
# Build dynamic UPDATE statement
|
|
2139
|
+
updates: List[str] = []
|
|
2140
|
+
params: List[Any] = []
|
|
2141
|
+
|
|
2142
|
+
if provider is not None:
|
|
2143
|
+
updates.append("provider = ?")
|
|
2144
|
+
params.append(provider)
|
|
2145
|
+
if session_type is not None:
|
|
2146
|
+
updates.append("session_type = ?")
|
|
2147
|
+
params.append(session_type)
|
|
2148
|
+
if session_id is not None:
|
|
2149
|
+
updates.append("session_id = ?")
|
|
2150
|
+
params.append(session_id)
|
|
2151
|
+
if context_id is not None:
|
|
2152
|
+
updates.append("context_id = ?")
|
|
2153
|
+
params.append(context_id)
|
|
2154
|
+
if transcript_path is not None:
|
|
2155
|
+
updates.append("transcript_path = ?")
|
|
2156
|
+
params.append(transcript_path)
|
|
2157
|
+
if cwd is not None:
|
|
2158
|
+
updates.append("cwd = ?")
|
|
2159
|
+
params.append(cwd)
|
|
2160
|
+
if project_dir is not None:
|
|
2161
|
+
updates.append("project_dir = ?")
|
|
2162
|
+
params.append(project_dir)
|
|
2163
|
+
if status is not None:
|
|
2164
|
+
updates.append("status = ?")
|
|
2165
|
+
params.append(status)
|
|
2166
|
+
if attention is not None:
|
|
2167
|
+
updates.append("attention = ?")
|
|
2168
|
+
params.append(attention if attention != "" else None)
|
|
2169
|
+
if source is not None:
|
|
2170
|
+
updates.append("source = ?")
|
|
2171
|
+
params.append(source)
|
|
2172
|
+
|
|
2173
|
+
if not updates:
|
|
2174
|
+
# Nothing to update, just return existing
|
|
2175
|
+
return self.get_agent_by_id(agent_id)
|
|
2176
|
+
|
|
2177
|
+
updates.append("updated_at = datetime('now')")
|
|
2178
|
+
params.append(agent_id)
|
|
2179
|
+
|
|
2180
|
+
try:
|
|
2181
|
+
cursor.execute(
|
|
2182
|
+
f"UPDATE agents SET {', '.join(updates)} WHERE id = ?",
|
|
2183
|
+
params,
|
|
2184
|
+
)
|
|
2185
|
+
conn.commit()
|
|
2186
|
+
return self.get_agent_by_id(agent_id)
|
|
2187
|
+
except sqlite3.OperationalError:
|
|
2188
|
+
conn.rollback()
|
|
2189
|
+
return None
|
|
2190
|
+
|
|
2191
|
+
def get_agent_by_id(self, agent_id: str) -> Optional[AgentRecord]:
|
|
2192
|
+
"""Get an agent by its ID."""
|
|
2193
|
+
conn = self._get_connection()
|
|
2194
|
+
cursor = conn.cursor()
|
|
2195
|
+
try:
|
|
2196
|
+
cursor.execute("SELECT * FROM agents WHERE id = ?", (agent_id,))
|
|
2197
|
+
row = cursor.fetchone()
|
|
2198
|
+
if row:
|
|
2199
|
+
return self._row_to_agent(row)
|
|
2200
|
+
except sqlite3.OperationalError:
|
|
2201
|
+
pass
|
|
2202
|
+
return None
|
|
2203
|
+
|
|
2204
|
+
def list_agents(
|
|
2205
|
+
self,
|
|
2206
|
+
*,
|
|
2207
|
+
status: Optional[str] = None,
|
|
2208
|
+
context_id: Optional[str] = None,
|
|
2209
|
+
limit: int = 100,
|
|
2210
|
+
) -> List[AgentRecord]:
|
|
2211
|
+
"""List agents, optionally filtered by status or context."""
|
|
2212
|
+
conn = self._get_connection()
|
|
2213
|
+
cursor = conn.cursor()
|
|
2214
|
+
|
|
2215
|
+
where_clauses: List[str] = []
|
|
2216
|
+
params: List[Any] = []
|
|
2217
|
+
|
|
2218
|
+
if status:
|
|
2219
|
+
where_clauses.append("status = ?")
|
|
2220
|
+
params.append(status)
|
|
2221
|
+
if context_id:
|
|
2222
|
+
where_clauses.append("context_id = ?")
|
|
2223
|
+
params.append(context_id)
|
|
2224
|
+
|
|
2225
|
+
where_sql = ""
|
|
2226
|
+
if where_clauses:
|
|
2227
|
+
where_sql = "WHERE " + " AND ".join(where_clauses)
|
|
2228
|
+
|
|
2229
|
+
params.append(limit)
|
|
2230
|
+
|
|
2231
|
+
try:
|
|
2232
|
+
cursor.execute(
|
|
2233
|
+
f"""
|
|
2234
|
+
SELECT * FROM agents
|
|
2235
|
+
{where_sql}
|
|
2236
|
+
ORDER BY updated_at DESC
|
|
2237
|
+
LIMIT ?
|
|
2238
|
+
""",
|
|
2239
|
+
params,
|
|
2240
|
+
)
|
|
2241
|
+
return [self._row_to_agent(row) for row in cursor.fetchall()]
|
|
2242
|
+
except sqlite3.OperationalError:
|
|
2243
|
+
return []
|
|
2244
|
+
|
|
2245
|
+
def delete_agent(self, agent_id: str) -> bool:
|
|
2246
|
+
"""Delete an agent by ID."""
|
|
2247
|
+
conn = self._get_connection()
|
|
2248
|
+
try:
|
|
2249
|
+
cursor = conn.execute("DELETE FROM agents WHERE id = ?", (agent_id,))
|
|
2250
|
+
conn.commit()
|
|
2251
|
+
return cursor.rowcount > 0
|
|
2252
|
+
except sqlite3.OperationalError:
|
|
2253
|
+
conn.rollback()
|
|
2254
|
+
return False
|
|
2255
|
+
|
|
2256
|
+
# -------------------------------------------------------------------------
|
|
2257
|
+
# Agent Context methods (Schema V15 - replaces load.json)
|
|
2258
|
+
# -------------------------------------------------------------------------
|
|
2259
|
+
|
|
2260
|
+
def get_or_create_agent_context(
|
|
2261
|
+
self,
|
|
2262
|
+
context_id: str,
|
|
2263
|
+
*,
|
|
2264
|
+
workspace: Optional[str] = None,
|
|
2265
|
+
loaded_at: Optional[str] = None,
|
|
2266
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
2267
|
+
) -> AgentContextRecord:
|
|
2268
|
+
"""Get existing agent context or create new one."""
|
|
2269
|
+
conn = self._get_connection()
|
|
2270
|
+
cursor = conn.cursor()
|
|
2271
|
+
|
|
2272
|
+
try:
|
|
2273
|
+
cursor.execute("SELECT * FROM agent_contexts WHERE id = ?", (context_id,))
|
|
2274
|
+
row = cursor.fetchone()
|
|
2275
|
+
if row:
|
|
2276
|
+
return self._row_to_agent_context(row)
|
|
2277
|
+
except sqlite3.OperationalError:
|
|
2278
|
+
raise
|
|
2279
|
+
|
|
2280
|
+
now = datetime.now()
|
|
2281
|
+
metadata_json = json.dumps(metadata or {}, ensure_ascii=False)
|
|
2282
|
+
|
|
2283
|
+
cursor.execute(
|
|
2284
|
+
"""
|
|
2285
|
+
INSERT INTO agent_contexts (id, workspace, loaded_at, created_at, updated_at, metadata)
|
|
2286
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
2287
|
+
""",
|
|
2288
|
+
(context_id, workspace, loaded_at, now, now, metadata_json),
|
|
2289
|
+
)
|
|
2290
|
+
conn.commit()
|
|
2291
|
+
|
|
2292
|
+
return AgentContextRecord(
|
|
2293
|
+
id=context_id,
|
|
2294
|
+
workspace=workspace,
|
|
2295
|
+
loaded_at=loaded_at,
|
|
2296
|
+
created_at=now,
|
|
2297
|
+
updated_at=now,
|
|
2298
|
+
metadata=metadata,
|
|
2299
|
+
session_ids=[],
|
|
2300
|
+
event_ids=[],
|
|
2301
|
+
)
|
|
2302
|
+
|
|
2303
|
+
def update_agent_context(
|
|
2304
|
+
self,
|
|
2305
|
+
context_id: str,
|
|
2306
|
+
*,
|
|
2307
|
+
workspace: Optional[str] = None,
|
|
2308
|
+
loaded_at: Optional[str] = None,
|
|
2309
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
2310
|
+
) -> Optional[AgentContextRecord]:
|
|
2311
|
+
"""Update an existing agent context."""
|
|
2312
|
+
conn = self._get_connection()
|
|
2313
|
+
cursor = conn.cursor()
|
|
2314
|
+
|
|
2315
|
+
updates: List[str] = []
|
|
2316
|
+
params: List[Any] = []
|
|
2317
|
+
|
|
2318
|
+
if workspace is not None:
|
|
2319
|
+
updates.append("workspace = ?")
|
|
2320
|
+
params.append(workspace)
|
|
2321
|
+
if loaded_at is not None:
|
|
2322
|
+
updates.append("loaded_at = ?")
|
|
2323
|
+
params.append(loaded_at)
|
|
2324
|
+
if metadata is not None:
|
|
2325
|
+
updates.append("metadata = ?")
|
|
2326
|
+
params.append(json.dumps(metadata, ensure_ascii=False))
|
|
2327
|
+
|
|
2328
|
+
if not updates:
|
|
2329
|
+
return self.get_agent_context_by_id(context_id)
|
|
2330
|
+
|
|
2331
|
+
updates.append("updated_at = datetime('now')")
|
|
2332
|
+
params.append(context_id)
|
|
2333
|
+
|
|
2334
|
+
try:
|
|
2335
|
+
cursor.execute(
|
|
2336
|
+
f"UPDATE agent_contexts SET {', '.join(updates)} WHERE id = ?",
|
|
2337
|
+
params,
|
|
2338
|
+
)
|
|
2339
|
+
conn.commit()
|
|
2340
|
+
return self.get_agent_context_by_id(context_id)
|
|
2341
|
+
except sqlite3.OperationalError:
|
|
2342
|
+
conn.rollback()
|
|
2343
|
+
return None
|
|
2344
|
+
|
|
2345
|
+
def get_agent_context_by_id(self, context_id: str) -> Optional[AgentContextRecord]:
|
|
2346
|
+
"""Get an agent context by its ID, including linked sessions and events."""
|
|
2347
|
+
conn = self._get_connection()
|
|
2348
|
+
cursor = conn.cursor()
|
|
2349
|
+
|
|
2350
|
+
try:
|
|
2351
|
+
cursor.execute("SELECT * FROM agent_contexts WHERE id = ?", (context_id,))
|
|
2352
|
+
row = cursor.fetchone()
|
|
2353
|
+
if not row:
|
|
2354
|
+
return None
|
|
2355
|
+
|
|
2356
|
+
record = self._row_to_agent_context(row)
|
|
2357
|
+
|
|
2358
|
+
# Fetch linked session IDs
|
|
2359
|
+
cursor.execute(
|
|
2360
|
+
"SELECT session_id FROM agent_context_sessions WHERE context_id = ?",
|
|
2361
|
+
(context_id,),
|
|
2362
|
+
)
|
|
2363
|
+
record.session_ids = [r[0] for r in cursor.fetchall()]
|
|
2364
|
+
|
|
2365
|
+
# Fetch linked event IDs
|
|
2366
|
+
cursor.execute(
|
|
2367
|
+
"SELECT event_id FROM agent_context_events WHERE context_id = ?",
|
|
2368
|
+
(context_id,),
|
|
2369
|
+
)
|
|
2370
|
+
record.event_ids = [r[0] for r in cursor.fetchall()]
|
|
2371
|
+
|
|
2372
|
+
return record
|
|
2373
|
+
except sqlite3.OperationalError:
|
|
2374
|
+
return None
|
|
2375
|
+
|
|
2376
|
+
def get_agent_context_by_workspace(
|
|
2377
|
+
self, workspace: str
|
|
2378
|
+
) -> Optional[AgentContextRecord]:
|
|
2379
|
+
"""Get an agent context by workspace path."""
|
|
2380
|
+
conn = self._get_connection()
|
|
2381
|
+
cursor = conn.cursor()
|
|
2382
|
+
|
|
2383
|
+
try:
|
|
2384
|
+
cursor.execute(
|
|
2385
|
+
"SELECT * FROM agent_contexts WHERE workspace = ?", (workspace,)
|
|
2386
|
+
)
|
|
2387
|
+
row = cursor.fetchone()
|
|
2388
|
+
if not row:
|
|
2389
|
+
return None
|
|
2390
|
+
|
|
2391
|
+
record = self._row_to_agent_context(row)
|
|
2392
|
+
context_id = record.id
|
|
2393
|
+
|
|
2394
|
+
# Fetch linked session IDs
|
|
2395
|
+
cursor.execute(
|
|
2396
|
+
"SELECT session_id FROM agent_context_sessions WHERE context_id = ?",
|
|
2397
|
+
(context_id,),
|
|
2398
|
+
)
|
|
2399
|
+
record.session_ids = [r[0] for r in cursor.fetchall()]
|
|
2400
|
+
|
|
2401
|
+
# Fetch linked event IDs
|
|
2402
|
+
cursor.execute(
|
|
2403
|
+
"SELECT event_id FROM agent_context_events WHERE context_id = ?",
|
|
2404
|
+
(context_id,),
|
|
2405
|
+
)
|
|
2406
|
+
record.event_ids = [r[0] for r in cursor.fetchall()]
|
|
2407
|
+
|
|
2408
|
+
return record
|
|
2409
|
+
except sqlite3.OperationalError:
|
|
2410
|
+
return None
|
|
2411
|
+
|
|
2412
|
+
def list_agent_contexts(self, limit: int = 100) -> List[AgentContextRecord]:
|
|
2413
|
+
"""List all agent contexts."""
|
|
2414
|
+
conn = self._get_connection()
|
|
2415
|
+
cursor = conn.cursor()
|
|
2416
|
+
|
|
2417
|
+
try:
|
|
2418
|
+
cursor.execute(
|
|
2419
|
+
"""
|
|
2420
|
+
SELECT * FROM agent_contexts
|
|
2421
|
+
ORDER BY updated_at DESC
|
|
2422
|
+
LIMIT ?
|
|
2423
|
+
""",
|
|
2424
|
+
(limit,),
|
|
2425
|
+
)
|
|
2426
|
+
contexts = []
|
|
2427
|
+
for row in cursor.fetchall():
|
|
2428
|
+
record = self._row_to_agent_context(row)
|
|
2429
|
+
context_id = record.id
|
|
2430
|
+
|
|
2431
|
+
# Fetch linked session IDs
|
|
2432
|
+
cursor.execute(
|
|
2433
|
+
"SELECT session_id FROM agent_context_sessions WHERE context_id = ?",
|
|
2434
|
+
(context_id,),
|
|
2435
|
+
)
|
|
2436
|
+
record.session_ids = [r[0] for r in cursor.fetchall()]
|
|
2437
|
+
|
|
2438
|
+
# Fetch linked event IDs
|
|
2439
|
+
cursor.execute(
|
|
2440
|
+
"SELECT event_id FROM agent_context_events WHERE context_id = ?",
|
|
2441
|
+
(context_id,),
|
|
2442
|
+
)
|
|
2443
|
+
record.event_ids = [r[0] for r in cursor.fetchall()]
|
|
2444
|
+
|
|
2445
|
+
contexts.append(record)
|
|
2446
|
+
return contexts
|
|
2447
|
+
except sqlite3.OperationalError:
|
|
2448
|
+
return []
|
|
2449
|
+
|
|
2450
|
+
def delete_agent_context(self, context_id: str) -> bool:
|
|
2451
|
+
"""Delete an agent context and all its links."""
|
|
2452
|
+
conn = self._get_connection()
|
|
2453
|
+
try:
|
|
2454
|
+
conn.execute(
|
|
2455
|
+
"DELETE FROM agent_context_sessions WHERE context_id = ?", (context_id,)
|
|
2456
|
+
)
|
|
2457
|
+
conn.execute(
|
|
2458
|
+
"DELETE FROM agent_context_events WHERE context_id = ?", (context_id,)
|
|
2459
|
+
)
|
|
2460
|
+
cursor = conn.execute(
|
|
2461
|
+
"DELETE FROM agent_contexts WHERE id = ?", (context_id,)
|
|
2462
|
+
)
|
|
2463
|
+
conn.commit()
|
|
2464
|
+
return cursor.rowcount > 0
|
|
2465
|
+
except sqlite3.OperationalError:
|
|
2466
|
+
conn.rollback()
|
|
2467
|
+
return False
|
|
2468
|
+
|
|
2469
|
+
def link_session_to_agent_context(self, context_id: str, session_id: str) -> bool:
|
|
2470
|
+
"""Link a session to an agent context.
|
|
2471
|
+
|
|
2472
|
+
Silently skips if session doesn't exist (FK constraint).
|
|
2473
|
+
"""
|
|
2474
|
+
conn = self._get_connection()
|
|
2475
|
+
try:
|
|
2476
|
+
conn.execute(
|
|
2477
|
+
"""
|
|
2478
|
+
INSERT OR IGNORE INTO agent_context_sessions (context_id, session_id)
|
|
2479
|
+
VALUES (?, ?)
|
|
2480
|
+
""",
|
|
2481
|
+
(context_id, session_id),
|
|
2482
|
+
)
|
|
2483
|
+
conn.commit()
|
|
2484
|
+
return True
|
|
2485
|
+
except (sqlite3.OperationalError, sqlite3.IntegrityError):
|
|
2486
|
+
conn.rollback()
|
|
2487
|
+
return False
|
|
2488
|
+
|
|
2489
|
+
def unlink_session_from_agent_context(
|
|
2490
|
+
self, context_id: str, session_id: str
|
|
2491
|
+
) -> bool:
|
|
2492
|
+
"""Unlink a session from an agent context."""
|
|
2493
|
+
conn = self._get_connection()
|
|
2494
|
+
try:
|
|
2495
|
+
conn.execute(
|
|
2496
|
+
"DELETE FROM agent_context_sessions WHERE context_id = ? AND session_id = ?",
|
|
2497
|
+
(context_id, session_id),
|
|
2498
|
+
)
|
|
2499
|
+
conn.commit()
|
|
2500
|
+
return True
|
|
2501
|
+
except (sqlite3.OperationalError, sqlite3.IntegrityError):
|
|
2502
|
+
conn.rollback()
|
|
2503
|
+
return False
|
|
2504
|
+
|
|
2505
|
+
def link_event_to_agent_context(self, context_id: str, event_id: str) -> bool:
|
|
2506
|
+
"""Link an event to an agent context.
|
|
2507
|
+
|
|
2508
|
+
Silently skips if event doesn't exist (FK constraint).
|
|
2509
|
+
"""
|
|
2510
|
+
conn = self._get_connection()
|
|
2511
|
+
try:
|
|
2512
|
+
conn.execute(
|
|
2513
|
+
"""
|
|
2514
|
+
INSERT OR IGNORE INTO agent_context_events (context_id, event_id)
|
|
2515
|
+
VALUES (?, ?)
|
|
2516
|
+
""",
|
|
2517
|
+
(context_id, event_id),
|
|
2518
|
+
)
|
|
2519
|
+
conn.commit()
|
|
2520
|
+
return True
|
|
2521
|
+
except (sqlite3.OperationalError, sqlite3.IntegrityError):
|
|
2522
|
+
conn.rollback()
|
|
2523
|
+
return False
|
|
2524
|
+
|
|
2525
|
+
def unlink_event_from_agent_context(self, context_id: str, event_id: str) -> bool:
|
|
2526
|
+
"""Unlink an event from an agent context."""
|
|
2527
|
+
conn = self._get_connection()
|
|
2528
|
+
try:
|
|
2529
|
+
conn.execute(
|
|
2530
|
+
"DELETE FROM agent_context_events WHERE context_id = ? AND event_id = ?",
|
|
2531
|
+
(context_id, event_id),
|
|
2532
|
+
)
|
|
2533
|
+
conn.commit()
|
|
2534
|
+
return True
|
|
2535
|
+
except (sqlite3.OperationalError, sqlite3.IntegrityError):
|
|
2536
|
+
conn.rollback()
|
|
2537
|
+
return False
|
|
2538
|
+
|
|
2539
|
+
def set_agent_context_sessions(
|
|
2540
|
+
self, context_id: str, session_ids: List[str]
|
|
2541
|
+
) -> bool:
|
|
2542
|
+
"""Replace all sessions linked to a context.
|
|
2543
|
+
|
|
2544
|
+
Silently skips sessions that don't exist in DB (FK constraint).
|
|
2545
|
+
"""
|
|
2546
|
+
conn = self._get_connection()
|
|
2547
|
+
try:
|
|
2548
|
+
conn.execute(
|
|
2549
|
+
"DELETE FROM agent_context_sessions WHERE context_id = ?", (context_id,)
|
|
2550
|
+
)
|
|
2551
|
+
if session_ids:
|
|
2552
|
+
# Insert one at a time to skip FK failures
|
|
2553
|
+
for sid in session_ids:
|
|
2554
|
+
try:
|
|
2555
|
+
conn.execute(
|
|
2556
|
+
"INSERT OR IGNORE INTO agent_context_sessions (context_id, session_id) VALUES (?, ?)",
|
|
2557
|
+
(context_id, sid),
|
|
2558
|
+
)
|
|
2559
|
+
except sqlite3.IntegrityError:
|
|
2560
|
+
# Session doesn't exist in DB, skip
|
|
2561
|
+
pass
|
|
2562
|
+
conn.commit()
|
|
2563
|
+
return True
|
|
2564
|
+
except (sqlite3.OperationalError, sqlite3.IntegrityError):
|
|
2565
|
+
conn.rollback()
|
|
2566
|
+
return False
|
|
2567
|
+
|
|
2568
|
+
def set_agent_context_events(self, context_id: str, event_ids: List[str]) -> bool:
|
|
2569
|
+
"""Replace all events linked to a context.
|
|
2570
|
+
|
|
2571
|
+
Silently skips events that don't exist in DB (FK constraint).
|
|
2572
|
+
"""
|
|
2573
|
+
conn = self._get_connection()
|
|
2574
|
+
try:
|
|
2575
|
+
conn.execute(
|
|
2576
|
+
"DELETE FROM agent_context_events WHERE context_id = ?", (context_id,)
|
|
2577
|
+
)
|
|
2578
|
+
if event_ids:
|
|
2579
|
+
# Insert one at a time to skip FK failures
|
|
2580
|
+
for eid in event_ids:
|
|
2581
|
+
try:
|
|
2582
|
+
conn.execute(
|
|
2583
|
+
"INSERT OR IGNORE INTO agent_context_events (context_id, event_id) VALUES (?, ?)",
|
|
2584
|
+
(context_id, eid),
|
|
2585
|
+
)
|
|
2586
|
+
except sqlite3.IntegrityError:
|
|
2587
|
+
# Event doesn't exist in DB, skip
|
|
2588
|
+
pass
|
|
2589
|
+
conn.commit()
|
|
2590
|
+
return True
|
|
2591
|
+
except (sqlite3.OperationalError, sqlite3.IntegrityError):
|
|
2592
|
+
conn.rollback()
|
|
2593
|
+
return False
|
|
2594
|
+
|
|
2027
2595
|
# Helper methods for row mapping
|
|
2028
2596
|
def _row_to_project(self, row: sqlite3.Row) -> ProjectRecord:
|
|
2029
2597
|
return ProjectRecord(
|
|
@@ -2255,3 +2823,52 @@ class SQLiteDatabase(DatabaseInterface):
|
|
|
2255
2823
|
creator_name=creator_name,
|
|
2256
2824
|
creator_id=creator_id,
|
|
2257
2825
|
)
|
|
2826
|
+
|
|
2827
|
+
def _row_to_agent(self, row: sqlite3.Row) -> AgentRecord:
|
|
2828
|
+
"""Convert a database row to an AgentRecord."""
|
|
2829
|
+
creator_name = None
|
|
2830
|
+
creator_id = None
|
|
2831
|
+
try:
|
|
2832
|
+
creator_name = row["creator_name"]
|
|
2833
|
+
creator_id = row["creator_id"]
|
|
2834
|
+
except (IndexError, KeyError):
|
|
2835
|
+
pass
|
|
2836
|
+
|
|
2837
|
+
return AgentRecord(
|
|
2838
|
+
id=row["id"],
|
|
2839
|
+
provider=row["provider"],
|
|
2840
|
+
session_type=row["session_type"],
|
|
2841
|
+
session_id=row["session_id"],
|
|
2842
|
+
context_id=row["context_id"],
|
|
2843
|
+
transcript_path=row["transcript_path"],
|
|
2844
|
+
cwd=row["cwd"],
|
|
2845
|
+
project_dir=row["project_dir"],
|
|
2846
|
+
status=row["status"] or "active",
|
|
2847
|
+
attention=row["attention"],
|
|
2848
|
+
source=row["source"],
|
|
2849
|
+
created_at=self._parse_datetime(row["created_at"]),
|
|
2850
|
+
updated_at=self._parse_datetime(row["updated_at"]),
|
|
2851
|
+
creator_name=creator_name,
|
|
2852
|
+
creator_id=creator_id,
|
|
2853
|
+
)
|
|
2854
|
+
|
|
2855
|
+
def _row_to_agent_context(self, row: sqlite3.Row) -> AgentContextRecord:
|
|
2856
|
+
"""Convert a database row to an AgentContextRecord."""
|
|
2857
|
+
metadata = None
|
|
2858
|
+
try:
|
|
2859
|
+
metadata_raw = row["metadata"]
|
|
2860
|
+
if metadata_raw:
|
|
2861
|
+
metadata = json.loads(metadata_raw)
|
|
2862
|
+
except (IndexError, KeyError, json.JSONDecodeError):
|
|
2863
|
+
pass
|
|
2864
|
+
|
|
2865
|
+
return AgentContextRecord(
|
|
2866
|
+
id=row["id"],
|
|
2867
|
+
workspace=row["workspace"],
|
|
2868
|
+
loaded_at=row["loaded_at"],
|
|
2869
|
+
created_at=self._parse_datetime(row["created_at"]),
|
|
2870
|
+
updated_at=self._parse_datetime(row["updated_at"]),
|
|
2871
|
+
metadata=metadata,
|
|
2872
|
+
session_ids=None, # Populated separately
|
|
2873
|
+
event_ids=None, # Populated separately
|
|
2874
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|