@team-agent/installer 0.2.3 → 0.2.5
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.
- package/package.json +1 -1
- package/src/team_agent/abnormal_track.py +253 -0
- package/src/team_agent/cli/commands.py +17 -1
- package/src/team_agent/cli/parser.py +2 -2
- package/src/team_agent/compiler.py +1 -1
- package/src/team_agent/coordinator/lifecycle.py +20 -2
- package/src/team_agent/display/__init__.py +31 -0
- package/src/team_agent/display/adaptive.py +425 -0
- package/src/team_agent/display/backend.py +46 -0
- package/src/team_agent/display/close.py +6 -0
- package/src/team_agent/display/rebuild.py +102 -0
- package/src/team_agent/display/tiling.py +156 -0
- package/src/team_agent/display/worker_window.py +4 -0
- package/src/team_agent/display/workspace.py +36 -127
- package/src/team_agent/idle_predicate.py +200 -0
- package/src/team_agent/idle_takeover.py +59 -0
- package/src/team_agent/idle_takeover_wiring.py +111 -0
- package/src/team_agent/launch/core.py +13 -4
- package/src/team_agent/leader/__init__.py +444 -61
- package/src/team_agent/message_store/agent_health.py +6 -2
- package/src/team_agent/message_store/core.py +51 -18
- package/src/team_agent/message_store/leader_notification_log.py +63 -38
- package/src/team_agent/message_store/result_watchers.py +17 -11
- package/src/team_agent/message_store/schema.py +19 -2
- package/src/team_agent/message_store/schema_migration.py +386 -0
- package/src/team_agent/messaging/delivery.py +45 -2
- package/src/team_agent/messaging/leader_panes.py +115 -21
- package/src/team_agent/messaging/send.py +33 -0
- package/src/team_agent/messaging/tmux_io.py +49 -10
- package/src/team_agent/messaging/trust_auto_answer.py +11 -3
- package/src/team_agent/provider_state/README.md +78 -0
- package/src/team_agent/provider_state/__init__.py +86 -0
- package/src/team_agent/provider_state/claude.py +86 -0
- package/src/team_agent/provider_state/codex.py +84 -0
- package/src/team_agent/provider_state/common.py +207 -0
- package/src/team_agent/provider_state/registry.py +118 -0
- package/src/team_agent/restart/orchestration.py +9 -9
- package/src/team_agent/runtime.py +62 -12
- package/src/team_agent/spec.py +4 -3
- package/src/team_agent/wake.py +58 -0
|
@@ -2,19 +2,49 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import sqlite3
|
|
5
|
+
import time
|
|
5
6
|
import uuid
|
|
6
7
|
from contextlib import closing
|
|
7
8
|
from datetime import datetime, timedelta, timezone
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import Any
|
|
10
|
+
from typing import Any, Callable
|
|
10
11
|
|
|
11
12
|
from . import agent_health as _agent_health
|
|
12
13
|
from . import result_watchers as _result_watchers
|
|
13
14
|
from .schema import SCHEMA_VERSION, initialize_schema, utcnow
|
|
15
|
+
from .schema_migration import MANAGED_TABLE_LAYOUTS
|
|
14
16
|
from team_agent.paths import runtime_dir
|
|
15
17
|
from team_agent.spec import validate_result_envelope
|
|
16
18
|
|
|
17
19
|
|
|
20
|
+
MESSAGE_SELECT = ", ".join(MANAGED_TABLE_LAYOUTS["messages"])
|
|
21
|
+
RESULT_SELECT = ", ".join(MANAGED_TABLE_LAYOUTS["results"])
|
|
22
|
+
SCHEDULED_EVENT_SELECT = ", ".join(MANAGED_TABLE_LAYOUTS["scheduled_events"])
|
|
23
|
+
DELIVERY_TOKEN_SELECT = ", ".join(MANAGED_TABLE_LAYOUTS["delivery_tokens"])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_sqlite_locked(exc: sqlite3.OperationalError) -> bool:
|
|
27
|
+
message = str(exc).lower()
|
|
28
|
+
return (
|
|
29
|
+
"database is locked" in message
|
|
30
|
+
or "database table is locked" in message
|
|
31
|
+
or "database schema is locked" in message
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _with_sqlite_busy_retry(action: Callable[[], None]) -> None:
|
|
36
|
+
delay = 0.05
|
|
37
|
+
for attempt in range(6):
|
|
38
|
+
try:
|
|
39
|
+
action()
|
|
40
|
+
return
|
|
41
|
+
except sqlite3.OperationalError as exc:
|
|
42
|
+
if not _is_sqlite_locked(exc) or attempt == 5:
|
|
43
|
+
raise
|
|
44
|
+
time.sleep(delay)
|
|
45
|
+
delay *= 2
|
|
46
|
+
|
|
47
|
+
|
|
18
48
|
class MessageStore:
|
|
19
49
|
SCHEMA_VERSION = SCHEMA_VERSION
|
|
20
50
|
|
|
@@ -27,13 +57,16 @@ class MessageStore:
|
|
|
27
57
|
def connect(self) -> sqlite3.Connection:
|
|
28
58
|
conn = sqlite3.connect(self.path, timeout=30.0, isolation_level=None)
|
|
29
59
|
conn.row_factory = sqlite3.Row
|
|
30
|
-
conn.execute("PRAGMA journal_mode=WAL")
|
|
31
60
|
conn.execute("PRAGMA busy_timeout=30000")
|
|
61
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
32
62
|
return conn
|
|
33
63
|
|
|
34
64
|
def _init(self) -> None:
|
|
35
|
-
|
|
36
|
-
|
|
65
|
+
def initialize() -> None:
|
|
66
|
+
with closing(self.connect()) as conn:
|
|
67
|
+
initialize_schema(conn, self.path)
|
|
68
|
+
|
|
69
|
+
_with_sqlite_busy_retry(initialize)
|
|
37
70
|
|
|
38
71
|
def create_message(
|
|
39
72
|
self,
|
|
@@ -198,10 +231,10 @@ class MessageStore:
|
|
|
198
231
|
def messages(self, owner_team_id: str | None = None) -> list[dict[str, Any]]:
|
|
199
232
|
with closing(self.connect()) as conn:
|
|
200
233
|
if owner_team_id is None:
|
|
201
|
-
rows = conn.execute("select
|
|
234
|
+
rows = conn.execute(f"select {MESSAGE_SELECT} from messages order by created_at").fetchall()
|
|
202
235
|
else:
|
|
203
236
|
rows = conn.execute(
|
|
204
|
-
"select
|
|
237
|
+
f"select {MESSAGE_SELECT} from messages where owner_team_id = ? or owner_team_id is null order by created_at",
|
|
205
238
|
(owner_team_id,),
|
|
206
239
|
).fetchall()
|
|
207
240
|
return [dict(row) for row in rows]
|
|
@@ -210,8 +243,8 @@ class MessageStore:
|
|
|
210
243
|
with closing(self.connect()) as conn:
|
|
211
244
|
if owner_team_id is None:
|
|
212
245
|
rows = conn.execute(
|
|
213
|
-
"""
|
|
214
|
-
select
|
|
246
|
+
f"""
|
|
247
|
+
select {MESSAGE_SELECT} from messages
|
|
215
248
|
where sender = ? or recipient = ?
|
|
216
249
|
order by created_at desc
|
|
217
250
|
limit ?
|
|
@@ -220,8 +253,8 @@ class MessageStore:
|
|
|
220
253
|
).fetchall()
|
|
221
254
|
else:
|
|
222
255
|
rows = conn.execute(
|
|
223
|
-
"""
|
|
224
|
-
select
|
|
256
|
+
f"""
|
|
257
|
+
select {MESSAGE_SELECT} from messages
|
|
225
258
|
where (sender = ? or recipient = ?)
|
|
226
259
|
and (owner_team_id = ? or owner_team_id is null)
|
|
227
260
|
order by created_at desc
|
|
@@ -233,7 +266,7 @@ class MessageStore:
|
|
|
233
266
|
|
|
234
267
|
def delivery_tokens(self) -> list[dict[str, Any]]:
|
|
235
268
|
with closing(self.connect()) as conn:
|
|
236
|
-
rows = conn.execute("select
|
|
269
|
+
rows = conn.execute(f"select {DELIVERY_TOKEN_SELECT} from delivery_tokens order by injected_at").fetchall()
|
|
237
270
|
return [dict(row) for row in rows]
|
|
238
271
|
|
|
239
272
|
def add_scheduled_event(
|
|
@@ -259,8 +292,8 @@ class MessageStore:
|
|
|
259
292
|
with closing(self.connect()) as conn:
|
|
260
293
|
if owner_team_id is None:
|
|
261
294
|
rows = conn.execute(
|
|
262
|
-
"""
|
|
263
|
-
select
|
|
295
|
+
f"""
|
|
296
|
+
select {SCHEDULED_EVENT_SELECT} from scheduled_events
|
|
264
297
|
where status = 'pending' and due_at <= ?
|
|
265
298
|
order by due_at, id
|
|
266
299
|
""",
|
|
@@ -268,8 +301,8 @@ class MessageStore:
|
|
|
268
301
|
).fetchall()
|
|
269
302
|
else:
|
|
270
303
|
rows = conn.execute(
|
|
271
|
-
"""
|
|
272
|
-
select
|
|
304
|
+
f"""
|
|
305
|
+
select {SCHEDULED_EVENT_SELECT} from scheduled_events
|
|
273
306
|
where status = 'pending' and due_at <= ?
|
|
274
307
|
and (owner_team_id = ? or owner_team_id is null)
|
|
275
308
|
order by due_at, id
|
|
@@ -412,14 +445,14 @@ class MessageStore:
|
|
|
412
445
|
if uncollected_only:
|
|
413
446
|
clauses.append("status not in ('collected', 'invalid')")
|
|
414
447
|
where = " where " + " and ".join(clauses) if clauses else ""
|
|
415
|
-
query = f"select
|
|
448
|
+
query = f"select {RESULT_SELECT} from results{where} order by created_at"
|
|
416
449
|
with closing(self.connect()) as conn:
|
|
417
450
|
rows = conn.execute(query, args).fetchall()
|
|
418
451
|
return [dict(row) for row in rows]
|
|
419
452
|
|
|
420
453
|
def result_by_id(self, result_id: str) -> dict[str, Any] | None:
|
|
421
454
|
with closing(self.connect()) as conn:
|
|
422
|
-
row = conn.execute("select
|
|
455
|
+
row = conn.execute(f"select {RESULT_SELECT} from results where result_id = ?", (result_id,)).fetchone()
|
|
423
456
|
return dict(row) if row else None
|
|
424
457
|
|
|
425
458
|
def latest_results(self, limit: int = 5, owner_team_id: str | None = None) -> list[dict[str, Any]]:
|
|
@@ -428,7 +461,7 @@ class MessageStore:
|
|
|
428
461
|
with closing(self.connect()) as conn:
|
|
429
462
|
rows = conn.execute(
|
|
430
463
|
f"""
|
|
431
|
-
select
|
|
464
|
+
select {RESULT_SELECT} from results
|
|
432
465
|
where status != 'invalid' {owner_clause}
|
|
433
466
|
order by created_at desc
|
|
434
467
|
limit ?
|
|
@@ -11,8 +11,24 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
from contextlib import closing
|
|
13
13
|
from datetime import datetime, timedelta, timezone
|
|
14
|
+
import sqlite3
|
|
15
|
+
import time
|
|
14
16
|
from typing import Any
|
|
15
17
|
|
|
18
|
+
from team_agent.message_store.schema_migration import MANAGED_TABLE_LAYOUTS
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
LEADER_NOTIFICATION_SELECT = ", ".join(MANAGED_TABLE_LAYOUTS["leader_notification_log"])
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _sqlite_locked(exc: sqlite3.OperationalError) -> bool:
|
|
25
|
+
message = str(exc).lower()
|
|
26
|
+
return (
|
|
27
|
+
"database is locked" in message
|
|
28
|
+
or "database table is locked" in message
|
|
29
|
+
or "database schema is locked" in message
|
|
30
|
+
)
|
|
31
|
+
|
|
16
32
|
|
|
17
33
|
def claim_leader_notification_delivery(
|
|
18
34
|
store: Any,
|
|
@@ -28,43 +44,52 @@ def claim_leader_notification_delivery(
|
|
|
28
44
|
rowcount=0 means a prior row exists for (result_id, leader_session_uuid); SELECT
|
|
29
45
|
it and return so the caller can decide to suppress (same envelope_hash) or surface
|
|
30
46
|
legitimate-duplicate (different envelope_hash)."""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
delay = 0.05
|
|
48
|
+
row = None
|
|
49
|
+
for attempt in range(6):
|
|
50
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
51
|
+
try:
|
|
52
|
+
with closing(store.connect()) as conn:
|
|
53
|
+
with conn:
|
|
54
|
+
cur = conn.execute(
|
|
55
|
+
"insert or ignore into leader_notification_log("
|
|
56
|
+
" result_id, leader_session_uuid, notified_message_id, notified_at,"
|
|
57
|
+
" leader_pane_id_at_notify, envelope_content_hash, owner_team_id"
|
|
58
|
+
") values (?, ?, ?, ?, ?, ?, ?)",
|
|
59
|
+
(
|
|
60
|
+
result_id, leader_session_uuid, proposed_message_id, now,
|
|
61
|
+
pane_id, envelope_hash, owner_team_id,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
if cur.rowcount == 1:
|
|
65
|
+
return {
|
|
66
|
+
"status": "claimed_by_you",
|
|
67
|
+
"notified_message_id": proposed_message_id,
|
|
68
|
+
"notified_at": now,
|
|
69
|
+
"envelope_content_hash": envelope_hash,
|
|
70
|
+
}
|
|
71
|
+
row = conn.execute(
|
|
72
|
+
"select notified_message_id, notified_at, envelope_content_hash, "
|
|
73
|
+
"leader_pane_id_at_notify from leader_notification_log "
|
|
74
|
+
"where result_id = ? and leader_session_uuid = ?",
|
|
75
|
+
(result_id, leader_session_uuid),
|
|
76
|
+
).fetchone()
|
|
77
|
+
break
|
|
78
|
+
except sqlite3.OperationalError as exc:
|
|
79
|
+
if not _sqlite_locked(exc) or attempt == 5:
|
|
80
|
+
raise
|
|
81
|
+
time.sleep(delay)
|
|
82
|
+
delay *= 2
|
|
57
83
|
if row is None:
|
|
58
84
|
# Should not happen (INSERT OR IGNORE returned 0 → row must exist), but be defensive.
|
|
59
85
|
return {"status": "claimed_by_you", "notified_message_id": proposed_message_id,
|
|
60
86
|
"notified_at": now, "envelope_content_hash": envelope_hash}
|
|
61
|
-
prev_message_id, prev_ts, prev_hash, prev_pane = row[0], row[1], row[2], row[3]
|
|
62
87
|
return {
|
|
63
88
|
"status": "already_notified_by",
|
|
64
|
-
"notified_message_id":
|
|
65
|
-
"notified_at":
|
|
66
|
-
"envelope_content_hash":
|
|
67
|
-
"leader_pane_id_at_notify":
|
|
89
|
+
"notified_message_id": row["notified_message_id"],
|
|
90
|
+
"notified_at": row["notified_at"],
|
|
91
|
+
"envelope_content_hash": row["envelope_content_hash"],
|
|
92
|
+
"leader_pane_id_at_notify": row["leader_pane_id_at_notify"],
|
|
68
93
|
}
|
|
69
94
|
|
|
70
95
|
|
|
@@ -88,11 +113,11 @@ def peek_leader_notification(
|
|
|
88
113
|
if row is None:
|
|
89
114
|
return None
|
|
90
115
|
return {
|
|
91
|
-
"notified_message_id": row[
|
|
92
|
-
"notified_at": row[
|
|
93
|
-
"envelope_content_hash": row[
|
|
94
|
-
"leader_pane_id_at_notify": row[
|
|
95
|
-
"owner_team_id": row[
|
|
116
|
+
"notified_message_id": row["notified_message_id"],
|
|
117
|
+
"notified_at": row["notified_at"],
|
|
118
|
+
"envelope_content_hash": row["envelope_content_hash"],
|
|
119
|
+
"leader_pane_id_at_notify": row["leader_pane_id_at_notify"],
|
|
120
|
+
"owner_team_id": row["owner_team_id"],
|
|
96
121
|
}
|
|
97
122
|
|
|
98
123
|
|
|
@@ -113,11 +138,11 @@ def leader_notification_log_rows(store: Any, *, owner_team_id: str | None = None
|
|
|
113
138
|
with closing(store.connect()) as conn:
|
|
114
139
|
if owner_team_id is None:
|
|
115
140
|
rows = conn.execute(
|
|
116
|
-
"select
|
|
141
|
+
f"select {LEADER_NOTIFICATION_SELECT} from leader_notification_log order by notified_at"
|
|
117
142
|
).fetchall()
|
|
118
143
|
else:
|
|
119
144
|
rows = conn.execute(
|
|
120
|
-
"select
|
|
145
|
+
f"select {LEADER_NOTIFICATION_SELECT} from leader_notification_log where owner_team_id = ? "
|
|
121
146
|
"or owner_team_id is null order by notified_at",
|
|
122
147
|
(owner_team_id,),
|
|
123
148
|
).fetchall()
|
|
@@ -4,9 +4,13 @@ import uuid
|
|
|
4
4
|
from contextlib import closing
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
|
+
from team_agent.message_store.schema_migration import MANAGED_TABLE_LAYOUTS
|
|
7
8
|
from team_agent.message_store.schema import utcnow
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
RESULT_WATCHER_SELECT = ", ".join(MANAGED_TABLE_LAYOUTS["result_watchers"])
|
|
12
|
+
|
|
13
|
+
|
|
10
14
|
def create_result_watcher(
|
|
11
15
|
self,
|
|
12
16
|
task_id: str | None,
|
|
@@ -32,11 +36,13 @@ def create_result_watcher(
|
|
|
32
36
|
def pending_result_watchers(self, owner_team_id: str | None = None) -> list[dict[str, Any]]:
|
|
33
37
|
with closing(self.connect()) as conn:
|
|
34
38
|
if owner_team_id is None:
|
|
35
|
-
rows = conn.execute(
|
|
39
|
+
rows = conn.execute(
|
|
40
|
+
f"select {RESULT_WATCHER_SELECT} from result_watchers where status = 'pending' order by created_at"
|
|
41
|
+
).fetchall()
|
|
36
42
|
else:
|
|
37
43
|
rows = conn.execute(
|
|
38
|
-
"""
|
|
39
|
-
select
|
|
44
|
+
f"""
|
|
45
|
+
select {RESULT_WATCHER_SELECT} from result_watchers
|
|
40
46
|
where status = 'pending' and (owner_team_id = ? or owner_team_id is null)
|
|
41
47
|
order by created_at
|
|
42
48
|
""",
|
|
@@ -47,17 +53,17 @@ def pending_result_watchers(self, owner_team_id: str | None = None) -> list[dict
|
|
|
47
53
|
def retryable_result_watchers(self) -> list[dict[str, Any]]:
|
|
48
54
|
with closing(self.connect()) as conn:
|
|
49
55
|
rows = conn.execute(
|
|
50
|
-
"select
|
|
56
|
+
f"select {RESULT_WATCHER_SELECT} from result_watchers where status in ('pending', 'notify_failed') order by created_at"
|
|
51
57
|
).fetchall()
|
|
52
58
|
return [dict(row) for row in rows]
|
|
53
59
|
|
|
54
60
|
def result_watchers(self, owner_team_id: str | None = None) -> list[dict[str, Any]]:
|
|
55
61
|
with closing(self.connect()) as conn:
|
|
56
62
|
if owner_team_id is None:
|
|
57
|
-
rows = conn.execute("select
|
|
63
|
+
rows = conn.execute(f"select {RESULT_WATCHER_SELECT} from result_watchers order by created_at").fetchall()
|
|
58
64
|
else:
|
|
59
65
|
rows = conn.execute(
|
|
60
|
-
"select
|
|
66
|
+
f"select {RESULT_WATCHER_SELECT} from result_watchers where owner_team_id = ? or owner_team_id is null order by created_at",
|
|
61
67
|
(owner_team_id,),
|
|
62
68
|
).fetchall()
|
|
63
69
|
return [dict(row) for row in rows]
|
|
@@ -92,7 +98,7 @@ def requeue_delivery_exhausted_watchers(self) -> list[str]:
|
|
|
92
98
|
rows = conn.execute(
|
|
93
99
|
"select watcher_id from result_watchers where status = 'delivery_exhausted'"
|
|
94
100
|
).fetchall()
|
|
95
|
-
watcher_ids = [row[
|
|
101
|
+
watcher_ids = [row["watcher_id"] for row in rows]
|
|
96
102
|
if watcher_ids:
|
|
97
103
|
# Phase D hotfix-3 (78055bc) cleared notified_message_id here; Gap 32 dedupe
|
|
98
104
|
# reverses that — preserve notified_message_id so the retry path can re-confirm
|
|
@@ -153,9 +159,9 @@ def _claim_leader_notification_disabled_impl( # legacy reference for archaeolog
|
|
|
153
159
|
"order by coalesce(completed_at, created_at) limit 1",
|
|
154
160
|
(result_id, owner_team_id),
|
|
155
161
|
).fetchone()
|
|
156
|
-
if sibling and sibling[
|
|
162
|
+
if sibling and sibling["notified_message_id"]:
|
|
157
163
|
conn.execute("COMMIT")
|
|
158
|
-
return {"status": "already_notified_by", "canonical_message_id": sibling[
|
|
164
|
+
return {"status": "already_notified_by", "canonical_message_id": sibling["notified_message_id"]}
|
|
159
165
|
cur = conn.execute(
|
|
160
166
|
"update result_watchers "
|
|
161
167
|
"set notified_message_id = ?, result_id = coalesce(result_id, ?) "
|
|
@@ -172,7 +178,7 @@ def _claim_leader_notification_disabled_impl( # legacy reference for archaeolog
|
|
|
172
178
|
conn.execute("COMMIT")
|
|
173
179
|
return {
|
|
174
180
|
"status": "already_notified_by",
|
|
175
|
-
"canonical_message_id": (row[
|
|
181
|
+
"canonical_message_id": (row["notified_message_id"] if row else None) or None,
|
|
176
182
|
}
|
|
177
183
|
except Exception:
|
|
178
184
|
try:
|
|
@@ -242,4 +248,4 @@ def leader_notified_message_id_for_result(
|
|
|
242
248
|
"order by coalesce(completed_at, created_at) limit 1",
|
|
243
249
|
(result_id, owner_team_id),
|
|
244
250
|
).fetchone()
|
|
245
|
-
return row[
|
|
251
|
+
return row["notified_message_id"] if row else None
|
|
@@ -2,6 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import sqlite3
|
|
4
4
|
from datetime import datetime, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from team_agent.message_store import schema_migration as _schema_migration
|
|
8
|
+
from team_agent.message_store.schema_migration import ensure_table_layout, table_layout
|
|
5
9
|
|
|
6
10
|
|
|
7
11
|
MESSAGE_COLUMNS = {
|
|
@@ -68,16 +72,26 @@ RESULT_WATCHER_COLUMNS = {
|
|
|
68
72
|
"notified_message_id",
|
|
69
73
|
"error",
|
|
70
74
|
}
|
|
75
|
+
LEADER_NOTIFICATION_LOG_COLUMNS = {
|
|
76
|
+
"result_id",
|
|
77
|
+
"leader_session_uuid",
|
|
78
|
+
"notified_message_id",
|
|
79
|
+
"notified_at",
|
|
80
|
+
"leader_pane_id_at_notify",
|
|
81
|
+
"envelope_content_hash",
|
|
82
|
+
"owner_team_id",
|
|
83
|
+
}
|
|
71
84
|
|
|
72
85
|
|
|
73
86
|
def utcnow() -> str:
|
|
74
87
|
return datetime.now(timezone.utc).isoformat()
|
|
75
88
|
|
|
76
89
|
SCHEMA_VERSION = 3
|
|
90
|
+
SCHEMA_MIGRATIONS = _schema_migration.SCHEMA_MIGRATIONS
|
|
77
91
|
|
|
78
92
|
|
|
79
93
|
def _table_columns(conn: sqlite3.Connection, table: str) -> set[str]:
|
|
80
|
-
return
|
|
94
|
+
return set(table_layout(conn, table))
|
|
81
95
|
|
|
82
96
|
|
|
83
97
|
def _ensure_table_columns(
|
|
@@ -97,7 +111,9 @@ def _ensure_table_columns(
|
|
|
97
111
|
conn.execute(migrations[name])
|
|
98
112
|
|
|
99
113
|
|
|
100
|
-
def initialize_schema(conn: sqlite3.Connection) -> None:
|
|
114
|
+
def initialize_schema(conn: sqlite3.Connection, db_path: Path | None = None) -> None:
|
|
115
|
+
_schema_migration.SCHEMA_MIGRATIONS = SCHEMA_MIGRATIONS
|
|
116
|
+
ensure_table_layout(conn, schema_version=SCHEMA_VERSION, db_path=db_path)
|
|
101
117
|
with conn:
|
|
102
118
|
conn.execute(
|
|
103
119
|
"""
|
|
@@ -260,6 +276,7 @@ def initialize_schema(conn: sqlite3.Connection) -> None:
|
|
|
260
276
|
"create index if not exists idx_leader_notification_log_uuid "
|
|
261
277
|
"on leader_notification_log(leader_session_uuid, notified_at)"
|
|
262
278
|
)
|
|
279
|
+
_ensure_table_columns(conn, "leader_notification_log", LEADER_NOTIFICATION_LOG_COLUMNS)
|
|
263
280
|
conn.execute("create index if not exists idx_messages_owner_team_id on messages(owner_team_id)")
|
|
264
281
|
conn.execute("create index if not exists idx_scheduled_events_owner_team_id on scheduled_events(owner_team_id)")
|
|
265
282
|
conn.execute("create index if not exists idx_agent_health_owner_team_id on agent_health(owner_team_id)")
|