process-gpt-agent-sdk 0.3.15__py3-none-any.whl → 0.3.17__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.

Potentially problematic release.


This version of process-gpt-agent-sdk might be problematic. Click here for more details.

@@ -2,7 +2,6 @@ import os
2
2
  import json
3
3
  import asyncio
4
4
  import socket
5
- import uuid
6
5
  from typing import Any, Dict, List, Optional, Tuple, Callable, TypeVar
7
6
 
8
7
  from dotenv import load_dotenv
@@ -11,26 +10,22 @@ import logging
11
10
  import random
12
11
 
13
12
  T = TypeVar("T")
14
-
15
-
16
- # 모듈 전역 로거 (정상 경로는 로깅하지 않고, 오류 시에만 사용)
17
13
  logger = logging.getLogger(__name__)
18
14
 
19
-
20
- # ============================================================================
21
- # Utility: 재시도 헬퍼 및 유틸
22
- # 설명: 동기 DB 호출을 안전하게 재시도 (지수 백오프 + 지터) 및 유틸
23
- # ============================================================================
24
-
15
+ # ------------------------------ Retry & JSON utils ------------------------------
25
16
  async def _async_retry(
26
- fn: Callable[[], T],
17
+ fn: Callable[[], Any],
27
18
  *,
28
19
  name: str,
29
20
  retries: int = 3,
30
21
  base_delay: float = 0.8,
31
- fallback: Optional[Callable[[], T]] = None,
32
- ) -> Optional[T]:
33
- """지수 백오프+jitter로 재시도하고 실패 시 fallback/None 반환."""
22
+ fallback: Optional[Callable[[], Any]] = None,
23
+ ) -> Optional[Any]:
24
+ """
25
+ - 각 시도 실패: warning 로깅(시도/지연/에러 포함)
26
+ - 최종 실패: FATAL 로깅(스택 포함), 예외는 재전파하지 않고 None 반환(기존 정책 유지)
27
+ - fallback 이 있으면 실행(실패 시에도 로깅 후 None)
28
+ """
34
29
  last_err: Optional[Exception] = None
35
30
  for attempt in range(1, retries + 1):
36
31
  try:
@@ -39,32 +34,29 @@ async def _async_retry(
39
34
  last_err = e
40
35
  jitter = random.uniform(0, 0.3)
41
36
  delay = base_delay * (2 ** (attempt - 1)) + jitter
37
+ logger.warning(
38
+ "retry warn: name=%s attempt=%d/%d delay=%.2fs error=%s",
39
+ name, attempt, retries, delay, str(e),
40
+ exc_info=e
41
+ )
42
42
  await asyncio.sleep(delay)
43
+
44
+ # 최종 실패
43
45
  if last_err is not None:
44
46
  logger.error(
45
- "retry failed: name=%s retries=%s error=%s", name, retries, str(last_err),
46
- exc_info=last_err,
47
+ "FATAL: retry failed: name=%s retries=%s error=%s",
48
+ name, retries, str(last_err), exc_info=last_err
47
49
  )
50
+
48
51
  if fallback is not None:
49
52
  try:
50
- fb_val = fallback()
51
- return fb_val
53
+ return fallback()
52
54
  except Exception as fb_err:
53
55
  logger.error("fallback failed: name=%s error=%s", name, str(fb_err), exc_info=fb_err)
54
56
  return None
55
57
  return None
56
-
57
58
 
58
- def _is_valid_uuid(value: str) -> bool:
59
- """UUID 문자열 형식 검증 (v1~v8 포함)"""
60
- try:
61
- uuid.UUID(value)
62
- return True
63
- except Exception:
64
- return False
65
-
66
59
  def _to_jsonable(value: Any) -> Any:
67
- """간단한 JSON 변환: dict 재귀, list/tuple/set→list, 기본형 유지, 나머지는 repr."""
68
60
  try:
69
61
  if value is None or isinstance(value, (str, int, float, bool)):
70
62
  return value
@@ -78,16 +70,10 @@ def _to_jsonable(value: Any) -> Any:
78
70
  except Exception:
79
71
  return repr(value)
80
72
 
81
-
82
- # ============================================================================
83
- # DB 연결/클라이언트
84
- # 설명: 환경 변수 로드, Supabase 클라이언트 초기화/반환, 컨슈머 식별자
85
- # ============================================================================
73
+ # ------------------------------ DB Client ------------------------------
86
74
  _supabase_client: Optional[Client] = None
87
75
 
88
-
89
76
  def initialize_db() -> None:
90
- """환경변수 로드 및 Supabase 클라이언트 초기화"""
91
77
  global _supabase_client
92
78
  if _supabase_client is not None:
93
79
  return
@@ -103,16 +89,12 @@ def initialize_db() -> None:
103
89
  logger.error("initialize_db failed: %s", str(e), exc_info=e)
104
90
  raise
105
91
 
106
-
107
92
  def get_db_client() -> Client:
108
- """초기화된 Supabase 클라이언트 반환."""
109
93
  if _supabase_client is None:
110
94
  raise RuntimeError("DB 미초기화: initialize_db() 먼저 호출")
111
95
  return _supabase_client
112
96
 
113
-
114
97
  def get_consumer_id() -> str:
115
- """파드/프로세스 식별자 생성(CONSUMER_ID>HOST:PID)."""
116
98
  env_consumer = os.getenv("CONSUMER_ID")
117
99
  if env_consumer:
118
100
  return env_consumer
@@ -120,17 +102,9 @@ def get_consumer_id() -> str:
120
102
  pid = os.getpid()
121
103
  return f"{host}:{pid}"
122
104
 
123
-
124
- # ============================================================================
125
- # 데이터 조회
126
- # 설명: TODOLIST 테이블 조회, 완료 output 목록 조회, 이벤트 조회, 폼 조회, 테넌트 MCP 설정 조회, 사용자 및 에이전트 조회
127
- # ============================================================================
105
+ # ------------------------------ Polling ------------------------------
128
106
  async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict[str, Any]]:
129
- """TODOLIST 테이블에서 대기중인 워크아이템을 조회 (agent_orch 전달).
130
-
131
- - 정상 동작 시 로그를 남기지 않는다.
132
- - 예외 시에만 풍부한 에러 정보를 남기되, 호출자에게 None을 반환하여 폴링 루프가 중단되지 않게 한다.
133
- """
107
+ """단일 RPC(fetch_pending_task) 호출: p_env dev/prod 분기"""
134
108
  if agent_orch is None:
135
109
  agent_orch = ""
136
110
  if consumer is None:
@@ -139,319 +113,120 @@ async def polling_pending_todos(agent_orch: str, consumer: str) -> Optional[Dict
139
113
  def _call():
140
114
  client = get_db_client()
141
115
  consumer_id = consumer or socket.gethostname()
142
- env = (os.getenv("ENV") or "").lower()
143
-
144
- if env == "dev":
145
- resp = client.rpc(
146
- "fetch_pending_task_dev",
147
- {"p_agent_orch": agent_orch, "p_consumer": consumer_id, "p_limit": 1, "p_tenant_id": "uengine"},
148
- ).execute()
149
- else:
150
- resp = client.rpc(
151
- "fetch_pending_task",
152
- {"p_agent_orch": agent_orch, "p_consumer": consumer_id, "p_limit": 1},
153
- ).execute()
154
-
155
- rows = resp.data or []
156
- return rows[0] if rows else None
157
-
158
- return await _async_retry(_call, name="polling_pending_todos", fallback=lambda: None)
159
116
 
117
+ # ENV 값을 dev / (그외=prod) 로만 정규화
118
+ p_env = (os.getenv("ENV") or "").lower()
119
+ if p_env != "dev":
120
+ p_env = "prod"
121
+
122
+ resp = client.rpc(
123
+ "fetch_pending_task",
124
+ {
125
+ "p_agent_orch": agent_orch,
126
+ "p_consumer": consumer_id,
127
+ "p_limit": 1,
128
+ "p_env": p_env,
129
+ },
130
+ ).execute()
160
131
 
161
- def fetch_human_response_sync(job_id: str) -> Optional[Dict[str, Any]]:
162
- """events에서 특정 job_id의 human_response 조회"""
163
- if not job_id:
164
- return None
165
- try:
166
- client = get_db_client()
167
- resp = (
168
- client
169
- .table("events")
170
- .select("*")
171
- .eq("job_id", job_id)
172
- .eq("event_type", "human_response")
173
- .execute()
174
- )
175
132
  rows = resp.data or []
176
133
  return rows[0] if rows else None
177
- except Exception as e:
178
- logger.error("fetch_human_response_sync failed: %s", str(e), exc_info=e)
179
- return None
180
-
181
134
 
182
- async def fetch_task_status(todo_id: str) -> Optional[str]:
183
- """todo의 draft_status를 조회한다."""
184
- if not todo_id:
185
- return None
186
- def _call():
187
- client = get_db_client()
188
- return (
189
- client.table("todolist").select("draft_status").eq("id", todo_id).single().execute()
190
- )
191
-
192
- try:
193
- resp = await _async_retry(_call, name="fetch_task_status")
194
- except Exception as e:
195
- logger.error("fetch_task_status fatal: %s", str(e), exc_info=e)
196
- return None
197
- if not resp or not getattr(resp, "data", None):
198
- return None
199
- try:
200
- return resp.data.get("draft_status")
201
- except Exception as e:
202
- logger.error("fetch_task_status parse error: %s", str(e), exc_info=e)
203
- return None
204
-
205
-
206
-
207
- async def fetch_all_agents() -> List[Dict[str, Any]]:
208
- """모든 에이전트 목록을 정규화하여 반환한다."""
209
- def _call():
210
- client = get_db_client()
211
- return (
212
- client.table("users")
213
- .select("id, username, role, goal, persona, tools, profile, model, tenant_id, is_agent, endpoint")
214
- .eq("is_agent", True)
215
- .execute()
216
- )
217
-
218
- try:
219
- resp = await _async_retry(_call, name="fetch_all_agents")
220
- except Exception as e:
221
- logger.error("fetch_all_agents fatal: %s", str(e), exc_info=e)
222
- return []
223
- rows = resp.data or [] if resp else []
224
- try:
225
- normalized: List[Dict[str, Any]] = []
226
- for row in rows:
227
- normalized.append(
228
- {
229
- "id": row.get("id"),
230
- "name": row.get("username"),
231
- "role": row.get("role"),
232
- "goal": row.get("goal"),
233
- "persona": row.get("persona"),
234
- "tools": row.get("tools") or "mem0",
235
- "profile": row.get("profile"),
236
- "model": row.get("model"),
237
- "tenant_id": row.get("tenant_id"),
238
- "endpoint": row.get("endpoint"),
239
- }
240
- )
241
- return normalized
242
- except Exception as e:
243
- logger.error("fetch_all_agents parse error: %s", str(e), exc_info=e)
244
- return []
245
-
246
-
247
- async def fetch_agent_data(user_ids: str) -> List[Dict[str, Any]]:
248
- """TODOLIST의 user_id 값으로, 역할로 지정된 에이전트를 조회하고 정규화해 반환한다."""
249
-
250
- raw_ids = [x.strip() for x in (user_ids or "").split(",") if x.strip()]
251
- valid_ids = [x for x in raw_ids if _is_valid_uuid(x)]
252
-
253
- if not valid_ids:
254
- return await fetch_all_agents()
135
+ return await _async_retry(_call, name="polling_pending_todos", fallback=lambda: None)
255
136
 
137
+ # ------------------------------ Context Bundle ------------------------------
138
+ async def fetch_context_bundle(
139
+ proc_inst_id: str,
140
+ tenant_id: str,
141
+ tool_val: str,
142
+ user_ids: str,
143
+ ) -> Tuple[str, Optional[Dict[str, Any]], Tuple[Optional[str], List[Dict[str, Any]], Optional[str]], List[Dict[str, Any]]]:
256
144
  def _call():
257
145
  client = get_db_client()
258
- resp = (
259
- client
260
- .table("users")
261
- .select("id, username, role, goal, persona, tools, profile, model, tenant_id, is_agent")
262
- .in_("id", valid_ids)
263
- .eq("is_agent", True)
264
- .execute()
265
- )
146
+ resp = client.rpc(
147
+ "fetch_context_bundle",
148
+ {
149
+ "p_proc_inst_id": proc_inst_id or "",
150
+ "p_tenant_id": tenant_id or "",
151
+ "p_tool": tool_val or "",
152
+ "p_user_ids": user_ids or "",
153
+ },
154
+ ).execute()
266
155
  rows = resp.data or []
267
- normalized: List[Dict[str, Any]] = []
268
- for row in rows:
269
- normalized.append(
270
- {
271
- "id": row.get("id"),
272
- "name": row.get("username"),
273
- "role": row.get("role"),
274
- "goal": row.get("goal"),
275
- "persona": row.get("persona"),
276
- "tools": row.get("tools") or "mem0",
277
- "profile": row.get("profile"),
278
- "model": row.get("model"),
279
- "tenant_id": row.get("tenant_id"),
280
- "endpoint": row.get("endpoint"),
281
- }
282
- )
283
- return normalized
156
+ row = rows[0] if rows else {}
157
+ notify = (row.get("notify_emails") or "").strip()
158
+ mcp = row.get("tenant_mcp") or None
159
+ form_id = row.get("form_id")
160
+ form_fields = row.get("form_fields") or [{"key": form_id, "type": "default", "text": ""}]
161
+ form_html = row.get("form_html")
162
+ agents = row.get("agents") or []
163
+ return notify, mcp, (form_id, form_fields, form_html), agents
284
164
 
285
165
  try:
286
- result = await _async_retry(_call, name="fetch_agent_data", fallback=lambda: [])
166
+ return await _async_retry(_call, name="fetch_context_bundle", fallback=lambda: ("", None, (None, [], None), []))
287
167
  except Exception as e:
288
- logger.error("fetch_agent_data fatal: %s", str(e), exc_info=e)
289
- return await fetch_all_agents()
290
-
291
- if not result:
292
- return await fetch_all_agents()
293
-
294
- return result
295
-
296
-
297
- async def fetch_form_types(tool_val: str, tenant_id: str) -> Tuple[str, List[Dict[str, Any]], Optional[str]]:
298
- """폼 타입 정의를 조회해 (form_id, fields, html)로 반환한다."""
299
- if tool_val is None:
300
- tool_val = ""
301
- if tenant_id is None:
302
- tenant_id = ""
303
- form_id = tool_val[12:] if tool_val.startswith("formHandler:") else tool_val
304
-
305
- def _call():
306
- client = get_db_client()
307
- resp = (
308
- client
309
- .table("form_def")
310
- .select("fields_json, html")
311
- .eq("id", form_id)
312
- .eq("tenant_id", tenant_id)
313
- .execute()
314
- )
315
- fields_json = resp.data[0].get("fields_json") if resp.data else None
316
- form_html = resp.data[0].get("html") if resp.data else None
317
- if not fields_json:
318
- return form_id, [{"key": form_id, "type": "default", "text": ""}], form_html
319
- return form_id, fields_json, form_html
168
+ logger.error("fetch_context_bundle fatal: %s", str(e), exc_info=e)
169
+ return ("", None, (None, [], None), [])
320
170
 
321
- try:
322
- resp = await _async_retry(
323
- _call,
324
- name="fetch_form_types",
325
- fallback=lambda: (form_id, [{"key": form_id, "type": "default", "text": ""}], None),
326
- )
327
- except Exception as e:
328
- logger.error("fetch_form_types fatal: %s", str(e), exc_info=e)
329
- resp = None
330
- return resp if resp else (form_id, [{"key": form_id, "type": "default", "text": ""}], None)
171
+ # ------------------------------ Events & Results ------------------------------
172
+ async def record_events_bulk(payloads: List[Dict[str, Any]]) -> None:
173
+ """
174
+ 이벤트 다건 저장:
175
+ - 성공: 'record_events_bulk ok'
176
+ - 실패(최종): '❌ record_events_bulk failed' (개수 포함)
177
+ """
178
+ if not payloads:
179
+ return
331
180
 
181
+ safe_list: List[Dict[str, Any]] = []
182
+ for p in payloads:
183
+ sp = _to_jsonable(p)
184
+ if isinstance(sp, dict) and sp.get("status", "") == "":
185
+ sp["status"] = None
186
+ safe_list.append(sp)
332
187
 
333
- async def fetch_tenant_mcp_config(tenant_id: str) -> Optional[Dict[str, Any]]:
334
- """테넌트 MCP 설정을 조회해 반환한다."""
335
- if not tenant_id:
336
- return None
337
188
  def _call():
338
189
  client = get_db_client()
339
- return client.table("tenants").select("mcp").eq("id", tenant_id).single().execute()
340
-
341
- try:
342
- resp = await _async_retry(_call, name="fetch_tenant_mcp_config", fallback=lambda: None)
343
- except Exception as e:
344
- logger.error("fetch_tenant_mcp_config fatal: %s", str(e), exc_info=e)
345
- return None
346
- return resp.data.get("mcp") if resp and getattr(resp, "data", None) else None
347
-
348
-
349
- async def fetch_human_users_by_proc_inst_id(proc_inst_id: str) -> str:
350
- """proc_inst_id로 현재 프로세스의 모든 사용자 이메일 목록을 쉼표로 반환한다."""
351
- if not proc_inst_id:
352
- return ""
353
-
354
- def _sync():
355
- try:
356
- supabase = get_db_client()
357
-
358
- resp = (
359
- supabase
360
- .table('todolist')
361
- .select('user_id')
362
- .eq('proc_inst_id', proc_inst_id)
363
- .execute()
364
- )
365
-
366
- if not resp.data:
367
- return ""
368
-
369
- all_user_ids = set()
370
- for row in resp.data:
371
- user_id = row.get('user_id', '')
372
- if user_id:
373
- ids = [id.strip() for id in user_id.split(',') if id.strip()]
374
- all_user_ids.update(ids)
375
-
376
- if not all_user_ids:
377
- return ""
378
-
379
- human_user_emails = []
380
- for user_id in all_user_ids:
381
- if not _is_valid_uuid(user_id):
382
- continue
383
-
384
- user_resp = (
385
- supabase
386
- .table('users')
387
- .select('id, email, is_agent')
388
- .eq('id', user_id)
389
- .execute()
390
- )
391
-
392
- if user_resp.data:
393
- user = user_resp.data[0]
394
- is_agent = user.get('is_agent')
395
- if not is_agent:
396
- email = (user.get('email') or '').strip()
397
- if email:
398
- human_user_emails.append(email)
399
-
400
- return ','.join(human_user_emails)
401
-
402
- except Exception as e:
403
- logger.error("fetch_human_users_by_proc_inst_id failed: %s", str(e), exc_info=e)
404
- return ""
405
-
406
- return await asyncio.to_thread(_sync)
190
+ return client.rpc("record_events_bulk", {"p_events": safe_list}).execute()
407
191
 
192
+ res = await _async_retry(_call, name="record_events_bulk", fallback=lambda: None)
193
+ if res is None:
194
+ logger.error("❌ record_events_bulk failed: events not persisted count=%d", len(safe_list))
195
+ else:
196
+ logger.info("record_events_bulk ok: count=%d", len(safe_list))
408
197
 
409
- # ============================================================================
410
- # 데이터 저장
411
- # 설명: 이벤트/알림/작업 결과 저장
412
- # ============================================================================
413
198
  async def record_event(payload: Dict[str, Any]) -> None:
414
- """UI용 events 테이블에 이벤트 기록 (전달된 payload 그대로 저장)"""
415
- if payload is None:
416
- logger.error("record_event invalid payload: None")
199
+ """
200
+ 단건 이벤트 저장.
201
+ - 성공: 'record_event ok'
202
+ - 실패(최종): '❌ record_event failed'
203
+ - 실패해도 워크플로우는 계속(요청사항)
204
+ """
205
+ if not payload:
417
206
  return
207
+
418
208
  def _call():
419
209
  client = get_db_client()
420
210
  safe_payload = _to_jsonable(payload)
421
- # 상태값이 문자열이면 NULL로
422
- if isinstance(safe_payload, dict):
423
- status_val = safe_payload.get("status")
424
- if status_val == "":
425
- safe_payload["status"] = None
211
+ if isinstance(safe_payload, dict) and safe_payload.get("status", "") == "":
212
+ safe_payload["status"] = None
426
213
  return client.table("events").insert(safe_payload).execute()
427
214
 
428
- try:
429
- resp = await _async_retry(_call, name="record_event", fallback=lambda: None)
430
- except Exception as e:
431
- try:
432
- logger.error("record_event fatal: %s payload=%s", str(e), json.dumps(_to_jsonable(payload), ensure_ascii=False), exc_info=e)
433
- except Exception:
434
- logger.error("record_event fatal (payload dump failed): %s", str(e), exc_info=e)
435
- return
436
- if resp is None:
437
- try:
438
- logger.error("events insert 실패: payload=%s", json.dumps(_to_jsonable(payload), ensure_ascii=False))
439
- except Exception:
440
- logger.error("events insert 실패 (payload dump failed)")
441
-
442
-
215
+ res = await _async_retry(_call, name="record_event", fallback=lambda: None)
216
+ if res is None:
217
+ logger.error("❌ record_event failed =%s", payload.get("event_type"))
218
+ else:
219
+ logger.info("record_event ok: event_type=%s", payload.get("event_type"))
443
220
 
444
221
  async def save_task_result(todo_id: str, result: Any, final: bool = False) -> None:
445
- """작업 결과를 저장한다. final=True 시 최종 저장."""
446
222
  if not todo_id:
447
223
  logger.error("save_task_result invalid todo_id: %s", str(todo_id))
448
224
  return
449
- # 안전한 직렬화: 실패 시 문자열화하여 저장자가 원인 파악 가능
450
- def _safe_payload(val: Any) -> Any:
225
+
226
+ def _safe(val: Any) -> Any:
451
227
  try:
452
228
  return _to_jsonable(val)
453
- except Exception as e:
454
- logger.error("save_task_result payload serialization failed: %s", str(e), exc_info=e)
229
+ except Exception:
455
230
  try:
456
231
  return {"repr": repr(val)}
457
232
  except Exception:
@@ -459,79 +234,26 @@ async def save_task_result(todo_id: str, result: Any, final: bool = False) -> No
459
234
 
460
235
  def _call():
461
236
  client = get_db_client()
462
- payload = _safe_payload(result)
463
- return client.rpc(
464
- "save_task_result",
465
- {"p_todo_id": todo_id, "p_payload": payload, "p_final": bool(final)},
466
- ).execute()
467
-
468
- try:
469
- await _async_retry(_call, name="save_task_result", fallback=lambda: None)
470
- except Exception as e:
471
- logger.error("save_task_result fatal: %s", str(e), exc_info=e)
472
-
473
-
474
- def save_notification(
475
- *,
476
- title: str,
477
- notif_type: str,
478
- description: Optional[str] = None,
479
- user_ids_csv: Optional[str] = None,
480
- tenant_id: Optional[str] = None,
481
- url: Optional[str] = None,
482
- from_user_id: Optional[str] = None,
483
- ) -> None:
484
- """notifications 테이블에 알림 저장"""
485
- try:
486
- # 대상 사용자가 없으면 작업 생략
487
- if not user_ids_csv:
488
- return
489
-
490
- client = get_db_client()
491
-
492
- user_ids: List[str] = [uid.strip() for uid in user_ids_csv.split(',') if uid and uid.strip()]
493
- if not user_ids:
494
- return
495
-
496
- rows: List[Dict[str, Any]] = []
497
- for uid in user_ids:
498
- rows.append(
499
- {
500
- "id": str(uuid.uuid4()),
501
- "user_id": uid,
502
- "tenant_id": tenant_id,
503
- "title": title,
504
- "description": description,
505
- "type": notif_type,
506
- "url": url,
507
- "from_user_id": from_user_id,
508
- }
509
- )
510
-
511
- client.table("notifications").insert(rows).execute()
512
- except Exception as e:
513
- logger.error("save_notification failed: %s", str(e), exc_info=e)
237
+ payload = _safe(result)
238
+ return client.rpc("save_task_result", {"p_todo_id": todo_id, "p_payload": payload, "p_final": bool(final)}).execute()
514
239
 
515
- # ============================================================================
516
- # 상태 변경
517
- # 설명: 실패 작업 상태 업데이트
518
- # ============================================================================
240
+ res = await _async_retry(_call, name="save_task_result", fallback=lambda: None)
241
+ if res is None:
242
+ logger.error("❌ save_task_result failed todo_id=%s", todo_id)
243
+ else:
244
+ logger.info("save_task_result ok todo_id=%s", todo_id)
519
245
 
246
+ # ------------------------------ Failure Status ------------------------------
520
247
  async def update_task_error(todo_id: str) -> None:
521
- """실패 작업의 상태를 FAILED로 갱신한다."""
522
248
  if not todo_id:
523
249
  return
250
+
524
251
  def _call():
525
252
  client = get_db_client()
526
- return (
527
- client
528
- .table('todolist')
529
- .update({'draft_status': 'FAILED', 'consumer': None})
530
- .eq('id', todo_id)
531
- .execute()
532
- )
253
+ return client.table("todolist").update({"draft_status": "FAILED", "consumer": None}).eq("id", todo_id).execute()
533
254
 
534
- try:
535
- await _async_retry(_call, name="update_task_error", fallback=lambda: None)
536
- except Exception as e:
537
- logger.error("update_task_error fatal: %s", str(e), exc_info=e)
255
+ res = await _async_retry(_call, name="update_task_error", fallback=lambda: None)
256
+ if res is None:
257
+ logger.error("❌ update_task_error failed todo_id=%s", todo_id)
258
+ else:
259
+ logger.info("update_task_error ok todo_id=%s", todo_id)