| 125 | |
| 126 | |
| 127 | def _scan_stuck( |
| 128 | conn: sqlite3.Connection, |
| 129 | task_ids: set[str], |
| 130 | session_ids: set[str], |
| 131 | ) -> list[StuckRow]: |
| 132 | findings: list[StuckRow] = [] |
| 133 | |
| 134 | if task_ids: |
| 135 | placeholders = ",".join("?" * len(task_ids)) |
| 136 | for (tid, status, lock, locked_at, title) in conn.execute( |
| 137 | f"SELECT id, status, execution_lock, execution_locked_at, title " |
| 138 | f"FROM tasks WHERE id IN ({placeholders})", |
| 139 | tuple(task_ids), |
| 140 | ).fetchall(): |
| 141 | flags = [] |
| 142 | if status == "running": |
| 143 | flags.append("status=running") |
| 144 | if lock: |
| 145 | flags.append(f"execution_lock=1 (since {locked_at or '?'})") |
| 146 | if flags: |
| 147 | findings.append( |
| 148 | StuckRow("task", tid, status, f"'{title}' " + ", ".join(flags)) |
| 149 | ) |
| 150 | |
| 151 | combined_sessions: set[str] = set(session_ids) |
| 152 | if task_ids: |
| 153 | placeholders = ",".join("?" * len(task_ids)) |
| 154 | for (sid,) in conn.execute( |
| 155 | f"SELECT DISTINCT session_id FROM runtime_sessions " |
| 156 | f"WHERE task_id IN ({placeholders})", |
| 157 | tuple(task_ids), |
| 158 | ).fetchall(): |
| 159 | if sid: |
| 160 | combined_sessions.add(sid) |
| 161 | |
| 162 | if combined_sessions: |
| 163 | placeholders = ",".join("?" * len(combined_sessions)) |
| 164 | for (rsid, sid, tid, status, updated_at) in conn.execute( |
| 165 | f"SELECT runtime_session_id, session_id, task_id, status, updated_at " |
| 166 | f"FROM runtime_sessions WHERE session_id IN ({placeholders}) AND status = 'running'", |
| 167 | tuple(combined_sessions), |
| 168 | ).fetchall(): |
| 169 | findings.append( |
| 170 | StuckRow( |
| 171 | "runtime_session", |
| 172 | rsid, |
| 173 | status, |
| 174 | f"session={sid} task={tid} updated={updated_at}", |
| 175 | ) |
| 176 | ) |
| 177 | |
| 178 | return findings |
| 179 | |
| 180 | |
| 181 | def _apply_reset( |