@team-agent/installer 0.2.1 → 0.2.3
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/schemas/team.schema.json +6 -0
- package/src/team_agent/approvals/runtime_prompts.py +1 -1
- package/src/team_agent/cli/commands.py +122 -6
- package/src/team_agent/cli/parser.py +42 -1
- package/src/team_agent/coordinator/__main__.py +21 -2
- package/src/team_agent/coordinator/lifecycle.py +11 -0
- package/src/team_agent/diagnose/orphan_cleanup.py +364 -0
- package/src/team_agent/events.py +47 -0
- package/src/team_agent/launch/core.py +2 -1
- package/src/team_agent/leader/__init__.py +273 -60
- package/src/team_agent/lifecycle/agents.py +54 -2
- package/src/team_agent/lifecycle/operations.py +87 -9
- package/src/team_agent/lifecycle/start.py +1 -1
- package/src/team_agent/message_store/core.py +8 -7
- package/src/team_agent/message_store/leader_notification_log.py +132 -0
- package/src/team_agent/message_store/result_watchers.py +144 -1
- package/src/team_agent/message_store/schema.py +31 -2
- package/src/team_agent/messaging/delivery.py +293 -1
- package/src/team_agent/messaging/idle_alerts.py +109 -9
- package/src/team_agent/messaging/leader.py +179 -10
- package/src/team_agent/messaging/leader_api_errors.py +216 -0
- package/src/team_agent/messaging/leader_panes.py +393 -23
- package/src/team_agent/messaging/result_delivery.py +219 -4
- package/src/team_agent/messaging/results.py +12 -21
- package/src/team_agent/messaging/scheduler.py +24 -2
- package/src/team_agent/messaging/send.py +21 -26
- package/src/team_agent/messaging/tmux_io.py +153 -23
- package/src/team_agent/messaging/tmux_prompt.py +87 -0
- package/src/team_agent/messaging/trust_auto_answer.py +44 -0
- package/src/team_agent/restart/orchestration.py +207 -4
- package/src/team_agent/runtime.py +7 -7
- package/src/team_agent/rust_core.py +157 -3
- package/src/team_agent/sessions/capture.py +65 -15
- package/src/team_agent/spec.py +59 -0
- package/src/team_agent/state.py +153 -10
- package/src/team_agent/status/inbox.py +33 -3
- package/src/team_agent/status/queries.py +32 -1
- package/src/team_agent/watch/__init__.py +145 -0
|
@@ -331,17 +331,17 @@ class MessageStore:
|
|
|
331
331
|
return counts
|
|
332
332
|
|
|
333
333
|
def add_result(self, envelope: dict[str, Any], owner_team_id: str | None = None) -> str:
|
|
334
|
-
_ = owner_team_id
|
|
335
334
|
validate_result_envelope(envelope)
|
|
336
335
|
result_id = f"res_{uuid.uuid4().hex[:12]}"
|
|
337
336
|
with closing(self.connect()) as conn:
|
|
338
337
|
with conn:
|
|
339
338
|
conn.execute(
|
|
340
339
|
"""
|
|
341
|
-
insert into results(result_id, task_id, agent_id, envelope, status, created_at)
|
|
342
|
-
values (?, ?, ?, ?, ?, ?)
|
|
340
|
+
insert into results(owner_team_id, result_id, task_id, agent_id, envelope, status, created_at)
|
|
341
|
+
values (?, ?, ?, ?, ?, ?, ?)
|
|
343
342
|
""",
|
|
344
343
|
(
|
|
344
|
+
owner_team_id,
|
|
345
345
|
result_id,
|
|
346
346
|
envelope["task_id"],
|
|
347
347
|
envelope["agent_id"],
|
|
@@ -423,16 +423,17 @@ class MessageStore:
|
|
|
423
423
|
return dict(row) if row else None
|
|
424
424
|
|
|
425
425
|
def latest_results(self, limit: int = 5, owner_team_id: str | None = None) -> list[dict[str, Any]]:
|
|
426
|
-
|
|
426
|
+
owner_clause = "and owner_team_id = ?" if owner_team_id else ""
|
|
427
|
+
args: tuple[Any, ...] = (owner_team_id, limit) if owner_team_id else (limit,)
|
|
427
428
|
with closing(self.connect()) as conn:
|
|
428
429
|
rows = conn.execute(
|
|
429
|
-
"""
|
|
430
|
+
f"""
|
|
430
431
|
select * from results
|
|
431
|
-
where status != 'invalid'
|
|
432
|
+
where status != 'invalid' {owner_clause}
|
|
432
433
|
order by created_at desc
|
|
433
434
|
limit ?
|
|
434
435
|
""",
|
|
435
|
-
|
|
436
|
+
args,
|
|
436
437
|
).fetchall()
|
|
437
438
|
return [dict(row) for row in reversed(rows)]
|
|
438
439
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Stage 12 (Gap 26 ∩ Gap 32 roundtable consolidation 2026-05-26): atomic exactly-once
|
|
2
|
+
dedupe at the leader-pane injection boundary, keyed by (result_id, leader_session_uuid).
|
|
3
|
+
|
|
4
|
+
Replaces the bad6484 watcher-table UPSERT approach. UNIQUE primary key + SQLite
|
|
5
|
+
INSERT OR IGNORE gives an atomic claim that works across processes (CLI subprocess
|
|
6
|
+
vs coordinator daemon) and across threads without an advisory lock. Distinct
|
|
7
|
+
leader_session_uuid values (e.g. after takeover) each get their own row so a
|
|
8
|
+
re-takeover legitimately allows another delivery for the same result_id.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from contextlib import closing
|
|
13
|
+
from datetime import datetime, timedelta, timezone
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def claim_leader_notification_delivery(
|
|
18
|
+
store: Any,
|
|
19
|
+
*,
|
|
20
|
+
result_id: str,
|
|
21
|
+
leader_session_uuid: str,
|
|
22
|
+
proposed_message_id: str,
|
|
23
|
+
envelope_hash: str,
|
|
24
|
+
owner_team_id: str | None,
|
|
25
|
+
pane_id: str | None,
|
|
26
|
+
) -> dict[str, Any]:
|
|
27
|
+
"""Atomic claim. INSERT OR IGNORE → rowcount=1 means we won, fire the inject.
|
|
28
|
+
rowcount=0 means a prior row exists for (result_id, leader_session_uuid); SELECT
|
|
29
|
+
it and return so the caller can decide to suppress (same envelope_hash) or surface
|
|
30
|
+
legitimate-duplicate (different envelope_hash)."""
|
|
31
|
+
now = datetime.now(timezone.utc).isoformat()
|
|
32
|
+
with closing(store.connect()) as conn:
|
|
33
|
+
with conn:
|
|
34
|
+
cur = conn.execute(
|
|
35
|
+
"insert or ignore into leader_notification_log("
|
|
36
|
+
" result_id, leader_session_uuid, notified_message_id, notified_at,"
|
|
37
|
+
" leader_pane_id_at_notify, envelope_content_hash, owner_team_id"
|
|
38
|
+
") values (?, ?, ?, ?, ?, ?, ?)",
|
|
39
|
+
(
|
|
40
|
+
result_id, leader_session_uuid, proposed_message_id, now,
|
|
41
|
+
pane_id, envelope_hash, owner_team_id,
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
if cur.rowcount == 1:
|
|
45
|
+
return {
|
|
46
|
+
"status": "claimed_by_you",
|
|
47
|
+
"notified_message_id": proposed_message_id,
|
|
48
|
+
"notified_at": now,
|
|
49
|
+
"envelope_content_hash": envelope_hash,
|
|
50
|
+
}
|
|
51
|
+
row = conn.execute(
|
|
52
|
+
"select notified_message_id, notified_at, envelope_content_hash, "
|
|
53
|
+
"leader_pane_id_at_notify from leader_notification_log "
|
|
54
|
+
"where result_id = ? and leader_session_uuid = ?",
|
|
55
|
+
(result_id, leader_session_uuid),
|
|
56
|
+
).fetchone()
|
|
57
|
+
if row is None:
|
|
58
|
+
# Should not happen (INSERT OR IGNORE returned 0 → row must exist), but be defensive.
|
|
59
|
+
return {"status": "claimed_by_you", "notified_message_id": proposed_message_id,
|
|
60
|
+
"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
|
+
return {
|
|
63
|
+
"status": "already_notified_by",
|
|
64
|
+
"notified_message_id": prev_message_id,
|
|
65
|
+
"notified_at": prev_ts,
|
|
66
|
+
"envelope_content_hash": prev_hash,
|
|
67
|
+
"leader_pane_id_at_notify": prev_pane,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def peek_leader_notification(
|
|
72
|
+
store: Any,
|
|
73
|
+
*,
|
|
74
|
+
result_id: str,
|
|
75
|
+
leader_session_uuid: str,
|
|
76
|
+
) -> dict[str, Any] | None:
|
|
77
|
+
"""Read-only fast-path peek (Stage 12). Returns the existing log row for
|
|
78
|
+
(result_id, leader_session_uuid) or None. Used by notify_result_watchers to short-
|
|
79
|
+
circuit before calling deliver_stored_message; the authoritative atomic claim still
|
|
80
|
+
happens at the _send_to_leader_receiver injection boundary."""
|
|
81
|
+
with closing(store.connect()) as conn:
|
|
82
|
+
row = conn.execute(
|
|
83
|
+
"select notified_message_id, notified_at, envelope_content_hash, "
|
|
84
|
+
"leader_pane_id_at_notify, owner_team_id from leader_notification_log "
|
|
85
|
+
"where result_id = ? and leader_session_uuid = ?",
|
|
86
|
+
(result_id, leader_session_uuid),
|
|
87
|
+
).fetchone()
|
|
88
|
+
if row is None:
|
|
89
|
+
return None
|
|
90
|
+
return {
|
|
91
|
+
"notified_message_id": row[0],
|
|
92
|
+
"notified_at": row[1],
|
|
93
|
+
"envelope_content_hash": row[2],
|
|
94
|
+
"leader_pane_id_at_notify": row[3],
|
|
95
|
+
"owner_team_id": row[4],
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def prune_leader_notification_log(store: Any, *, max_age_hours: int = 24) -> int:
|
|
100
|
+
"""Coordinator-tick maintenance: drop rows older than max_age_hours. Cheap, bounded."""
|
|
101
|
+
cutoff = (datetime.now(timezone.utc) - timedelta(hours=max_age_hours)).isoformat()
|
|
102
|
+
with closing(store.connect()) as conn:
|
|
103
|
+
with conn:
|
|
104
|
+
cur = conn.execute(
|
|
105
|
+
"delete from leader_notification_log where notified_at < ?",
|
|
106
|
+
(cutoff,),
|
|
107
|
+
)
|
|
108
|
+
return cur.rowcount or 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def leader_notification_log_rows(store: Any, *, owner_team_id: str | None = None) -> list[dict[str, Any]]:
|
|
112
|
+
"""Test/diagnostic accessor. Returns all rows (optionally team-scoped)."""
|
|
113
|
+
with closing(store.connect()) as conn:
|
|
114
|
+
if owner_team_id is None:
|
|
115
|
+
rows = conn.execute(
|
|
116
|
+
"select * from leader_notification_log order by notified_at"
|
|
117
|
+
).fetchall()
|
|
118
|
+
else:
|
|
119
|
+
rows = conn.execute(
|
|
120
|
+
"select * from leader_notification_log where owner_team_id = ? "
|
|
121
|
+
"or owner_team_id is null order by notified_at",
|
|
122
|
+
(owner_team_id,),
|
|
123
|
+
).fetchall()
|
|
124
|
+
return [dict(row) for row in rows]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
__all__ = [
|
|
128
|
+
"claim_leader_notification_delivery",
|
|
129
|
+
"peek_leader_notification",
|
|
130
|
+
"prune_leader_notification_log",
|
|
131
|
+
"leader_notification_log_rows",
|
|
132
|
+
]
|
|
@@ -94,9 +94,152 @@ def requeue_delivery_exhausted_watchers(self) -> list[str]:
|
|
|
94
94
|
).fetchall()
|
|
95
95
|
watcher_ids = [row[0] for row in rows]
|
|
96
96
|
if watcher_ids:
|
|
97
|
+
# Phase D hotfix-3 (78055bc) cleared notified_message_id here; Gap 32 dedupe
|
|
98
|
+
# reverses that — preserve notified_message_id so the retry path can re-confirm
|
|
99
|
+
# (or skip if the same result_id was already injected on a different pane_id).
|
|
97
100
|
conn.execute(
|
|
98
101
|
"update result_watchers "
|
|
99
|
-
"set status = 'notify_failed', error = null,
|
|
102
|
+
"set status = 'notify_failed', error = null, completed_at = null "
|
|
100
103
|
"where status = 'delivery_exhausted'"
|
|
101
104
|
)
|
|
102
105
|
return watcher_ids
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def claim_leader_notification(
|
|
109
|
+
store: Any,
|
|
110
|
+
owner_team_id: str | None,
|
|
111
|
+
result_id: str | None,
|
|
112
|
+
watcher_id: str,
|
|
113
|
+
proposed_token: str,
|
|
114
|
+
) -> dict[str, Any]:
|
|
115
|
+
"""DEPRECATED (Stage 12 roundtable retirement). The watcher-table UPSERT did not
|
|
116
|
+
actually prevent duplicate leader-pane injections in Mac mini real flow because two
|
|
117
|
+
independent code paths (scheduled_event branch + result_watchers branch) emit
|
|
118
|
+
deliver_attempt without coordinating at the watcher level. Replaced by
|
|
119
|
+
leader_notification_log.claim_leader_notification_delivery consulted inside
|
|
120
|
+
_send_to_leader_receiver. Kept here as a no-op shim so legacy callers / tests that
|
|
121
|
+
still import this symbol don't crash on import — but it does NOT perform a claim and
|
|
122
|
+
should NOT be used in new code."""
|
|
123
|
+
if not result_id:
|
|
124
|
+
return {"status": "deprecated_noop", "canonical_message_id": None}
|
|
125
|
+
return {"status": "deprecated_noop", "canonical_message_id": None}
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _claim_leader_notification_disabled_impl( # legacy reference for archaeology
|
|
129
|
+
store: Any,
|
|
130
|
+
owner_team_id: str | None,
|
|
131
|
+
result_id: str | None,
|
|
132
|
+
watcher_id: str,
|
|
133
|
+
proposed_token: str,
|
|
134
|
+
) -> dict[str, Any]:
|
|
135
|
+
if not result_id:
|
|
136
|
+
return {"status": "no_result_id", "canonical_message_id": None}
|
|
137
|
+
with closing(store.connect()) as conn:
|
|
138
|
+
conn.isolation_level = None
|
|
139
|
+
try:
|
|
140
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
141
|
+
if owner_team_id is None:
|
|
142
|
+
sibling = conn.execute(
|
|
143
|
+
"select notified_message_id from result_watchers "
|
|
144
|
+
"where result_id = ? and notified_message_id is not null "
|
|
145
|
+
"order by coalesce(completed_at, created_at) limit 1",
|
|
146
|
+
(result_id,),
|
|
147
|
+
).fetchone()
|
|
148
|
+
else:
|
|
149
|
+
sibling = conn.execute(
|
|
150
|
+
"select notified_message_id from result_watchers "
|
|
151
|
+
"where result_id = ? and notified_message_id is not null "
|
|
152
|
+
"and (owner_team_id = ? or owner_team_id is null) "
|
|
153
|
+
"order by coalesce(completed_at, created_at) limit 1",
|
|
154
|
+
(result_id, owner_team_id),
|
|
155
|
+
).fetchone()
|
|
156
|
+
if sibling and sibling[0]:
|
|
157
|
+
conn.execute("COMMIT")
|
|
158
|
+
return {"status": "already_notified_by", "canonical_message_id": sibling[0]}
|
|
159
|
+
cur = conn.execute(
|
|
160
|
+
"update result_watchers "
|
|
161
|
+
"set notified_message_id = ?, result_id = coalesce(result_id, ?) "
|
|
162
|
+
"where watcher_id = ? and notified_message_id is null",
|
|
163
|
+
(proposed_token, result_id, watcher_id),
|
|
164
|
+
)
|
|
165
|
+
if cur.rowcount == 1:
|
|
166
|
+
conn.execute("COMMIT")
|
|
167
|
+
return {"status": "claimed_by_you", "canonical_message_id": proposed_token}
|
|
168
|
+
row = conn.execute(
|
|
169
|
+
"select notified_message_id from result_watchers where watcher_id = ?",
|
|
170
|
+
(watcher_id,),
|
|
171
|
+
).fetchone()
|
|
172
|
+
conn.execute("COMMIT")
|
|
173
|
+
return {
|
|
174
|
+
"status": "already_notified_by",
|
|
175
|
+
"canonical_message_id": (row[0] if row else None) or None,
|
|
176
|
+
}
|
|
177
|
+
except Exception:
|
|
178
|
+
try:
|
|
179
|
+
conn.execute("ROLLBACK")
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
raise
|
|
183
|
+
finally:
|
|
184
|
+
conn.isolation_level = "" # restore default
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def release_leader_notification_claim(
|
|
188
|
+
store: Any,
|
|
189
|
+
watcher_id: str,
|
|
190
|
+
expected_token: str,
|
|
191
|
+
) -> bool:
|
|
192
|
+
"""Release a sentinel claim after delivery failure so the next retry can re-claim.
|
|
193
|
+
Returns True iff we released the claim we owned (rowcount == 1)."""
|
|
194
|
+
with closing(store.connect()) as conn:
|
|
195
|
+
with conn:
|
|
196
|
+
cur = conn.execute(
|
|
197
|
+
"update result_watchers set notified_message_id = null "
|
|
198
|
+
"where watcher_id = ? and notified_message_id = ?",
|
|
199
|
+
(watcher_id, expected_token),
|
|
200
|
+
)
|
|
201
|
+
return cur.rowcount == 1
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def promote_leader_notification_id(
|
|
205
|
+
store: Any,
|
|
206
|
+
watcher_id: str,
|
|
207
|
+
sentinel_token: str,
|
|
208
|
+
real_message_id: str,
|
|
209
|
+
) -> bool:
|
|
210
|
+
"""After successful delivery, replace the sentinel claim with the real message_id.
|
|
211
|
+
Returns True iff the promotion succeeded (rowcount == 1)."""
|
|
212
|
+
with closing(store.connect()) as conn:
|
|
213
|
+
with conn:
|
|
214
|
+
cur = conn.execute(
|
|
215
|
+
"update result_watchers set notified_message_id = ? "
|
|
216
|
+
"where watcher_id = ? and notified_message_id = ?",
|
|
217
|
+
(real_message_id, watcher_id, sentinel_token),
|
|
218
|
+
)
|
|
219
|
+
return cur.rowcount == 1
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def leader_notified_message_id_for_result(
|
|
223
|
+
store: Any,
|
|
224
|
+
owner_team_id: str | None,
|
|
225
|
+
result_id: str | None,
|
|
226
|
+
) -> str | None:
|
|
227
|
+
if not result_id:
|
|
228
|
+
return None
|
|
229
|
+
with closing(store.connect()) as conn:
|
|
230
|
+
if owner_team_id is None:
|
|
231
|
+
row = conn.execute(
|
|
232
|
+
"select notified_message_id from result_watchers "
|
|
233
|
+
"where result_id = ? and notified_message_id is not null "
|
|
234
|
+
"order by coalesce(completed_at, created_at) limit 1",
|
|
235
|
+
(result_id,),
|
|
236
|
+
).fetchone()
|
|
237
|
+
else:
|
|
238
|
+
row = conn.execute(
|
|
239
|
+
"select notified_message_id from result_watchers "
|
|
240
|
+
"where result_id = ? and notified_message_id is not null "
|
|
241
|
+
"and (owner_team_id = ? or owner_team_id is null) "
|
|
242
|
+
"order by coalesce(completed_at, created_at) limit 1",
|
|
243
|
+
(result_id, owner_team_id),
|
|
244
|
+
).fetchone()
|
|
245
|
+
return row[0] if row else None
|
|
@@ -22,7 +22,7 @@ MESSAGE_COLUMNS = {
|
|
|
22
22
|
"error",
|
|
23
23
|
"delivery_attempts",
|
|
24
24
|
}
|
|
25
|
-
RESULT_COLUMNS = {"result_id", "task_id", "agent_id", "envelope", "status", "created_at"}
|
|
25
|
+
RESULT_COLUMNS = {"owner_team_id", "result_id", "task_id", "agent_id", "envelope", "status", "created_at"}
|
|
26
26
|
SCHEDULED_EVENT_COLUMNS = {
|
|
27
27
|
"id",
|
|
28
28
|
"owner_team_id",
|
|
@@ -125,6 +125,7 @@ def initialize_schema(conn: sqlite3.Connection) -> None:
|
|
|
125
125
|
"""
|
|
126
126
|
create table if not exists results (
|
|
127
127
|
result_id text primary key,
|
|
128
|
+
owner_team_id text,
|
|
128
129
|
task_id text not null,
|
|
129
130
|
agent_id text not null,
|
|
130
131
|
envelope text not null,
|
|
@@ -215,7 +216,12 @@ def initialize_schema(conn: sqlite3.Connection) -> None:
|
|
|
215
216
|
"owner_team_id": "alter table messages add column owner_team_id text",
|
|
216
217
|
},
|
|
217
218
|
)
|
|
218
|
-
_ensure_table_columns(
|
|
219
|
+
_ensure_table_columns(
|
|
220
|
+
conn,
|
|
221
|
+
"results",
|
|
222
|
+
RESULT_COLUMNS,
|
|
223
|
+
{"owner_team_id": "alter table results add column owner_team_id text"},
|
|
224
|
+
)
|
|
219
225
|
_ensure_table_columns(
|
|
220
226
|
conn,
|
|
221
227
|
"scheduled_events",
|
|
@@ -231,6 +237,29 @@ def initialize_schema(conn: sqlite3.Connection) -> None:
|
|
|
231
237
|
RESULT_WATCHER_COLUMNS,
|
|
232
238
|
{"owner_team_id": "alter table result_watchers add column owner_team_id text"},
|
|
233
239
|
)
|
|
240
|
+
# Stage 12 (Gap 26 ∩ Gap 32 roundtable consolidation 2026-05-26): dedupe leader
|
|
241
|
+
# notifications at the injection boundary, keyed by (result_id, leader_session_uuid).
|
|
242
|
+
# UNIQUE primary key + INSERT OR IGNORE in claim_leader_notification_delivery gives
|
|
243
|
+
# atomic exactly-once without an advisory lock. Retires the bad6484 watcher-table
|
|
244
|
+
# UPSERT approach.
|
|
245
|
+
conn.execute(
|
|
246
|
+
"""
|
|
247
|
+
create table if not exists leader_notification_log (
|
|
248
|
+
result_id text not null,
|
|
249
|
+
leader_session_uuid text not null,
|
|
250
|
+
notified_message_id text not null,
|
|
251
|
+
notified_at text not null,
|
|
252
|
+
leader_pane_id_at_notify text,
|
|
253
|
+
envelope_content_hash text,
|
|
254
|
+
owner_team_id text,
|
|
255
|
+
primary key (result_id, leader_session_uuid)
|
|
256
|
+
)
|
|
257
|
+
"""
|
|
258
|
+
)
|
|
259
|
+
conn.execute(
|
|
260
|
+
"create index if not exists idx_leader_notification_log_uuid "
|
|
261
|
+
"on leader_notification_log(leader_session_uuid, notified_at)"
|
|
262
|
+
)
|
|
234
263
|
conn.execute("create index if not exists idx_messages_owner_team_id on messages(owner_team_id)")
|
|
235
264
|
conn.execute("create index if not exists idx_scheduled_events_owner_team_id on scheduled_events(owner_team_id)")
|
|
236
265
|
conn.execute("create index if not exists idx_agent_health_owner_team_id on agent_health(owner_team_id)")
|