@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.
Files changed (40) hide show
  1. package/package.json +1 -1
  2. package/src/team_agent/abnormal_track.py +253 -0
  3. package/src/team_agent/cli/commands.py +17 -1
  4. package/src/team_agent/cli/parser.py +2 -2
  5. package/src/team_agent/compiler.py +1 -1
  6. package/src/team_agent/coordinator/lifecycle.py +20 -2
  7. package/src/team_agent/display/__init__.py +31 -0
  8. package/src/team_agent/display/adaptive.py +425 -0
  9. package/src/team_agent/display/backend.py +46 -0
  10. package/src/team_agent/display/close.py +6 -0
  11. package/src/team_agent/display/rebuild.py +102 -0
  12. package/src/team_agent/display/tiling.py +156 -0
  13. package/src/team_agent/display/worker_window.py +4 -0
  14. package/src/team_agent/display/workspace.py +36 -127
  15. package/src/team_agent/idle_predicate.py +200 -0
  16. package/src/team_agent/idle_takeover.py +59 -0
  17. package/src/team_agent/idle_takeover_wiring.py +111 -0
  18. package/src/team_agent/launch/core.py +13 -4
  19. package/src/team_agent/leader/__init__.py +444 -61
  20. package/src/team_agent/message_store/agent_health.py +6 -2
  21. package/src/team_agent/message_store/core.py +51 -18
  22. package/src/team_agent/message_store/leader_notification_log.py +63 -38
  23. package/src/team_agent/message_store/result_watchers.py +17 -11
  24. package/src/team_agent/message_store/schema.py +19 -2
  25. package/src/team_agent/message_store/schema_migration.py +386 -0
  26. package/src/team_agent/messaging/delivery.py +45 -2
  27. package/src/team_agent/messaging/leader_panes.py +115 -21
  28. package/src/team_agent/messaging/send.py +33 -0
  29. package/src/team_agent/messaging/tmux_io.py +49 -10
  30. package/src/team_agent/messaging/trust_auto_answer.py +11 -3
  31. package/src/team_agent/provider_state/README.md +78 -0
  32. package/src/team_agent/provider_state/__init__.py +86 -0
  33. package/src/team_agent/provider_state/claude.py +86 -0
  34. package/src/team_agent/provider_state/codex.py +84 -0
  35. package/src/team_agent/provider_state/common.py +207 -0
  36. package/src/team_agent/provider_state/registry.py +118 -0
  37. package/src/team_agent/restart/orchestration.py +9 -9
  38. package/src/team_agent/runtime.py +62 -12
  39. package/src/team_agent/spec.py +4 -3
  40. 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
- with closing(self.connect()) as conn:
36
- initialize_schema(conn)
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 * from messages order by created_at").fetchall()
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 * from messages where owner_team_id = ? or owner_team_id is null order by created_at",
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 * from messages
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 * from messages
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 * from delivery_tokens order by injected_at").fetchall()
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 * from scheduled_events
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 * from scheduled_events
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 * from results{where} order by created_at"
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 * from results where result_id = ?", (result_id,)).fetchone()
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 * from results
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
- 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()
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": prev_message_id,
65
- "notified_at": prev_ts,
66
- "envelope_content_hash": prev_hash,
67
- "leader_pane_id_at_notify": prev_pane,
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[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],
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 * from leader_notification_log order by notified_at"
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 * from leader_notification_log where owner_team_id = ? "
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("select * from result_watchers where status = 'pending' order by created_at").fetchall()
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 * from result_watchers
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 * from result_watchers where status in ('pending', 'notify_failed') order by created_at"
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 * from result_watchers order by created_at").fetchall()
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 * from result_watchers where owner_team_id = ? or owner_team_id is null order by created_at",
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[0] for row in rows]
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[0]:
162
+ if sibling and sibling["notified_message_id"]:
157
163
  conn.execute("COMMIT")
158
- return {"status": "already_notified_by", "canonical_message_id": sibling[0]}
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[0] if row else None) or None,
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[0] if row else None
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 {row[1] for row in conn.execute(f"pragma table_info({table})").fetchall()}
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)")