sync: 130 files from Baida98/AI [deploy-all]

#4
by Baida07 - opened
.env.example CHANGED
@@ -19,13 +19,6 @@ SUPABASE_URL=
19
  SUPABASE_SERVICE_ROLE_KEY=
20
  GITHUB_TOKEN=
21
  HF_TOKEN=
22
- # HF Spaces URLs (configurare per ogni Space del fleet)
23
- HF_SPACE_URL= # Brain / Backend principale
24
- HF_SPACE_B_URL= # Daemon / Telegram worker
25
- HF_SPACE_C_URL= # Worker A (Collab/GPU)
26
- HF_SPACE_D_URL= # Worker B
27
- HF_SPACE_E_URL= # Worker C
28
- ORACLE_CLOUD_VM_URL= # Oracle Cloud A1 compute VM
29
 
30
  # ── 3. Quadrante B (HANDS - Collab/Failover) ─────────────────
31
  RAILWAY_TOKEN_B=
 
19
  SUPABASE_SERVICE_ROLE_KEY=
20
  GITHUB_TOKEN=
21
  HF_TOKEN=
 
 
 
 
 
 
 
22
 
23
  # ── 3. Quadrante B (HANDS - Collab/Failover) ─────────────────
24
  RAILWAY_TOKEN_B=
agents/executor.py CHANGED
@@ -12,7 +12,6 @@ import asyncio
12
  import collections
13
  import logging
14
  import time as _time_mod
15
- from typing import Any
16
 
17
  from models.ai_client import AIClient
18
  from memory.manager import MemoryManager
@@ -94,12 +93,10 @@ class Executor:
94
  llm_client: AIClient | None = None,
95
  memory: MemoryManager | None = None,
96
  max_retries: int = 2,
97
- kernel: Any | None = None, # ARCH-K2.2: Brain→Kernel abstraction
98
  ):
99
  self.llm = llm_client or AIClient()
100
  self.memory = memory
101
  self.max_retries = max_retries
102
- self._kernel = kernel # ARCH-K2.2: usato da submit_background_task()
103
  # GAP-SKILL-SYNC v2: contatore chiamate per recovery credit (per-tool)
104
  self._circuit_recovery_counts: dict[str, int] = {}
105
 
@@ -108,53 +105,6 @@ class Executor:
108
  def from_ollama(cls, ollama=None, memory=None, max_retries: int = 2) -> "Executor":
109
  return cls(memory=memory, max_retries=max_retries)
110
 
111
- # ── ARCH-K2.2: submit background task via Kernel ──────────────────────────
112
-
113
- async def submit_background_task(
114
- self,
115
- payload: dict,
116
- priority: str = "BACKGROUND",
117
- session_id: str | None = None,
118
- ) -> str | None:
119
- """
120
- Invia un task in background tramite kernel.submit_task() (ARCH-K2.2).
121
-
122
- Il Brain/Executor non conosce l'implementazione della coda sottostante
123
- (S9: ogni servizio ignora l'impl interna degli altri).
124
-
125
- Fallback: asyncio.create_task() locale se il Kernel non Γ¨ disponibile.
126
- Sempre non-bloccante β€” non aspetta il completamento del task.
127
-
128
- Ritorna il task_id se il Kernel Γ¨ disponibile, None altrimenti.
129
- """
130
- # Lazy-load kernel singleton se non iniettato
131
- k = self._kernel
132
- if k is None:
133
- try:
134
- from api.kernel import kernel as _k
135
- k = _k
136
- except Exception:
137
- pass
138
-
139
- if k is not None:
140
- try:
141
- result = await k.submit_task(
142
- payload=payload,
143
- priority=priority,
144
- session_id=session_id,
145
- )
146
- _logger.info(
147
- "[executor] submit_background_task via Kernel id=%s priority=%s",
148
- result.task_id, priority,
149
- )
150
- return result.task_id
151
- except Exception as exc:
152
- _logger.warning("[executor] kernel submit_background_task err: %s", exc)
153
-
154
- # Fallback: esecuzione diretta asincrona locale (non attraverso la Queue)
155
- _logger.debug("[executor] submit_background_task fallback: asyncio.create_task")
156
- return None
157
-
158
  # ── Circuit breaker helper ────────────────────────────────────────────────
159
 
160
  def _is_circuit_open(self, tool_name: str, session_id: str) -> bool:
 
12
  import collections
13
  import logging
14
  import time as _time_mod
 
15
 
16
  from models.ai_client import AIClient
17
  from memory.manager import MemoryManager
 
93
  llm_client: AIClient | None = None,
94
  memory: MemoryManager | None = None,
95
  max_retries: int = 2,
 
96
  ):
97
  self.llm = llm_client or AIClient()
98
  self.memory = memory
99
  self.max_retries = max_retries
 
100
  # GAP-SKILL-SYNC v2: contatore chiamate per recovery credit (per-tool)
101
  self._circuit_recovery_counts: dict[str, int] = {}
102
 
 
105
  def from_ollama(cls, ollama=None, memory=None, max_retries: int = 2) -> "Executor":
106
  return cls(memory=memory, max_retries=max_retries)
107
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  # ── Circuit breaker helper ────────────────────────────────────────────────
109
 
110
  def _is_circuit_open(self, tool_name: str, session_id: str) -> bool:
api/conversations.py CHANGED
@@ -1,5 +1,5 @@
1
  """backend/api/conversations.py β€” Conversations + Messages CRUD (S354)."""
2
- import asyncio, json, logging
3
  from .state import safe_json_dumps
4
  from typing import Optional, Any
5
  from fastapi import APIRouter, Depends, Body, HTTPException
@@ -7,23 +7,6 @@ from .auth_guard import require_role, AuthRole
7
  from pydantic import BaseModel
8
  from .state import sb
9
 
10
- _logger_c = logging.getLogger("conversations")
11
-
12
- async def _sb_call(fn, *args, **kwargs):
13
- """AUD-011: 1 retry with 500ms delay on transient Supabase errors.
14
- HIGH-4: non retryare errori di autenticazione/autorizzazione β€” solo errori transienti.
15
- """
16
- try:
17
- return fn(*args, **kwargs)
18
- except Exception as _e:
19
- _ename = type(_e).__name__
20
- _emsg = str(_e)
21
- # Non retryare: auth errors, permission errors β€” sarebbero errori permanenti
22
- if any(k in _ename or k in _emsg for k in ("Auth", "JWT", "403", "401", "Unauthorized", "Permission")):
23
- raise
24
- await asyncio.sleep(0.5)
25
- return fn(*args, **kwargs) # let caller handle on second failure
26
-
27
  router = APIRouter( dependencies=[Depends(require_role(AuthRole.MACHINE))]) # GAP-1-fix: router-level auth
28
  _logger = logging.getLogger("conversations")
29
 
@@ -51,7 +34,7 @@ class MessageIn(BaseModel):
51
  @router.get('/api/conversations')
52
  async def list_conversations():
53
  try:
54
- data = await _sb_call(lambda: sb().table('conversations').select('*').order('updated_at', desc=True).limit(200).execute()) # BUGFIX: LIMIT 200 β€” AUD-011: +retry
55
  return {'conversations': data.data}
56
  except Exception as exc:
57
  _logger.warning("list_conversations: %s", exc)
@@ -95,7 +78,7 @@ async def delete_conversation(conv_id: str):
95
  @router.get('/api/conversations/{conv_id}/messages')
96
  async def list_messages(conv_id: str):
97
  try:
98
- data = await _sb_call(lambda: sb().table('messages').select('*').eq('conversation_id', conv_id).order('created_at').limit(500).execute()) # BUGFIX: LIMIT 500 β€” AUD-011: +retry
99
  return {'messages': data.data}
100
  except Exception as exc:
101
  _logger.warning("list_messages %s: %s", conv_id, exc)
@@ -112,7 +95,7 @@ async def upsert_messages(conv_id: str, body: dict = Body(...)):
112
  if 'steps' in m and m['steps'] is not None:
113
  m['steps'] = safe_json_dumps(m['steps']) if not isinstance(m['steps'], str) else m['steps']
114
  try:
115
- data = await _sb_call(lambda: sb().table('messages').upsert(msgs).execute()) # AUD-011
116
  return {'upserted': len(data.data)}
117
  except Exception as exc:
118
  _logger.warning("upsert_messages %s: %s", conv_id, exc)
 
1
  """backend/api/conversations.py β€” Conversations + Messages CRUD (S354)."""
2
+ import json, logging
3
  from .state import safe_json_dumps
4
  from typing import Optional, Any
5
  from fastapi import APIRouter, Depends, Body, HTTPException
 
7
  from pydantic import BaseModel
8
  from .state import sb
9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  router = APIRouter( dependencies=[Depends(require_role(AuthRole.MACHINE))]) # GAP-1-fix: router-level auth
11
  _logger = logging.getLogger("conversations")
12
 
 
34
  @router.get('/api/conversations')
35
  async def list_conversations():
36
  try:
37
+ data = sb().table('conversations').select('*').order('updated_at', desc=True).limit(200).execute() # BUGFIX: LIMIT 200 β€” senza limit OOM garantito su account con molte conversazioni
38
  return {'conversations': data.data}
39
  except Exception as exc:
40
  _logger.warning("list_conversations: %s", exc)
 
78
  @router.get('/api/conversations/{conv_id}/messages')
79
  async def list_messages(conv_id: str):
80
  try:
81
+ data = sb().table('messages').select('*').eq('conversation_id', conv_id).order('created_at').limit(500).execute() # BUGFIX: LIMIT 500 β€” senza limit OOM garantito su conversazioni lunghe
82
  return {'messages': data.data}
83
  except Exception as exc:
84
  _logger.warning("list_messages %s: %s", conv_id, exc)
 
95
  if 'steps' in m and m['steps'] is not None:
96
  m['steps'] = safe_json_dumps(m['steps']) if not isinstance(m['steps'], str) else m['steps']
97
  try:
98
+ data = sb().table('messages').upsert(msgs).execute()
99
  return {'upserted': len(data.data)}
100
  except Exception as exc:
101
  _logger.warning("upsert_messages %s: %s", conv_id, exc)
api/deploy.py CHANGED
@@ -137,7 +137,7 @@ async def deploy_status_all(request: Request, role: AuthRole = Depends(require_r
137
  return r
138
 
139
  async def _check_railway() -> dict:
140
- url = os.getenv("BACKEND_URL", "https://baida-a-terminal.hf.space") # MIGRAZIONE 2026-07-19: era RAILWAY_PUBLIC_URL
141
  r: dict = {"ok": False, "status": "unknown", "url": url, "latency_ms": None, "error": None}
142
  t0 = time.monotonic()
143
  try:
@@ -268,12 +268,12 @@ async def deploy_auto(body: AutoRepairRequest, request: Request, role: AuthRole
268
  # ── Railway ───────────────────────────────────────────────────────────────
269
  if "railway" in body.targets:
270
  if not body.dry_run:
271
- backend_url = (os.getenv("BACKEND_URL", "https://baida-a-terminal.hf.space")).rstrip("/") + "/health" # MIGRAZIONE 2026-07-19
272
  alive = False
273
  for attempt in range(1, 6):
274
  try:
275
  async with httpx.AsyncClient(timeout=8) as c:
276
- r = await c.get(backend_url)
277
  if r.status_code == 200:
278
  alive = True
279
  actions.append(f"βœ… Railway attivo (ping {attempt}/5 β€” HTTP 200)")
 
137
  return r
138
 
139
  async def _check_railway() -> dict:
140
+ url = os.getenv("RAILWAY_PUBLIC_URL", "") # S-DYN: usa env var
141
  r: dict = {"ok": False, "status": "unknown", "url": url, "latency_ms": None, "error": None}
142
  t0 = time.monotonic()
143
  try:
 
268
  # ── Railway ───────────────────────────────────────────────────────────────
269
  if "railway" in body.targets:
270
  if not body.dry_run:
271
+ railway_url = f"{os.getenv('RAILWAY_PUBLIC_URL', '')}/health"
272
  alive = False
273
  for attempt in range(1, 6):
274
  try:
275
  async with httpx.AsyncClient(timeout=8) as c:
276
+ r = await c.get(railway_url)
277
  if r.status_code == 200:
278
  alive = True
279
  actions.append(f"βœ… Railway attivo (ping {attempt}/5 β€” HTTP 200)")
api/job_queue.py CHANGED
@@ -12,8 +12,7 @@ Questo modulo implementa tre livelli di coordinamento via Upstash Redis:
12
  Chiave: jq:wake (LIST, RPOP, TTL 30s per elemento)
13
 
14
  3. TASK DELEGATION β€” BRAIN accoda task, HANDS consuma ed esegue.
15
- Corsie: jq:tasks:HIGH | jq:tasks:NORMAL | jq:tasks:LOW | jq:tasks:BACKGROUND (LPUSH/RPOP)
16
- Consumer drena HIGH→NORMAL→LOW→BACKGROUND in cascata. jq:tasks:NORMAL = legacy alias.
17
  Chiave: jq:result:{taskId} (STRING, TTL 300s)
18
  Chiave: jq:events:{taskId} (LIST, TTL 300s)
19
  Chiave: jq:consumer:alive (STRING, TTL 30s β€” heartbeat HANDS consumer)
@@ -51,20 +50,7 @@ _CONSUMER_HB_TTL = 30 # s β€” TTL heartbeat consumer HANDS
51
  # ── Redis keys ─────────────────────────────────────────────────────────────────
52
  _K_LOAD = lambda role: f"jq:load:{role}" # STRING β€” metriche load
53
  _K_WAKE = "jq:wake" # LIST β€” wake signals
54
- # Priority lanes (ordine decrescente — consumer drena HIGH→NORMAL→LOW→BACKGROUND)
55
- _PRIORITY_LANES = ("HIGH", "NORMAL", "LOW", "BACKGROUND")
56
- _K_QUEUE = lambda lane: f"jq:tasks:{lane}" # LIST β€” priority lane
57
- _K_PENDING = _K_QUEUE("NORMAL") # legacy alias β€” NORMAL lane
58
-
59
- # Normalizza priority string β†’ corsia canonica (compat con "realtime"/"background")
60
- _PRIORITY_MAP: dict[str, str] = {
61
- "high": "HIGH",
62
- "realtime": "HIGH", # compat legacy priority="realtime"
63
- "normal": "NORMAL",
64
- "low": "LOW",
65
- "background": "BACKGROUND",
66
- "bg": "BACKGROUND",
67
- }
68
  _K_RESULT = lambda tid: f"jq:result:{tid}" # STRING β€” risultato job
69
  _K_EVENTS = lambda tid: f"jq:events:{tid}" # LIST β€” eventi SSE
70
  _K_CONSUMER = "jq:consumer:alive" # STRING β€” HB consumer
@@ -201,7 +187,7 @@ class JobPayload(BaseModel):
201
  goal: str
202
  session_id: str = ""
203
  context: dict = {}
204
- priority: str = "NORMAL" # HIGH | NORMAL | LOW | BACKGROUND (compat: realtime→HIGH, background→BACKGROUND)
205
  max_steps: int = 20
206
  task_id: str = "" # se vuoto β†’ generato da BRAIN
207
 
@@ -218,32 +204,29 @@ async def submit_job(job: JobPayload) -> dict:
218
  raise HTTPException(503, "Redis non configurato β€” job queue non disponibile")
219
 
220
  task_id = job.task_id or str(uuid.uuid4())
221
- lane = _PRIORITY_MAP.get(job.priority.lower(), "NORMAL")
222
  payload = json.dumps({
223
  "taskId": task_id,
224
  "goal": job.goal,
225
  "session_id": job.session_id,
226
  "context": job.context,
227
- "priority": lane,
228
  "max_steps": job.max_steps,
229
  "submitted_at": time.time(),
230
  "submitted_by": _SPACE_ROLE,
231
  })
232
 
233
- queue_key = _K_QUEUE(lane)
234
- ok = await _rpush(queue_key, payload)
235
  if not ok:
236
  raise HTTPException(503, "Impossibile accodare il task su Redis")
237
 
238
- depth = await _llen(queue_key)
239
- _logger.info("[jq] job queued taskId=%s lane=%s depth=%d", task_id, lane, depth)
240
 
241
  return {
242
- "taskId": task_id,
243
- "status": "queued",
244
- "priority": lane,
245
  "queue_depth": depth,
246
- "stream_url": f"/api/agent/tasks/{task_id}/stream",
247
  }
248
 
249
 
@@ -349,12 +332,8 @@ async def _hands_consumer_loop() -> None:
349
  if not _JQ_ENABLED:
350
  continue # load publisher attivo, job consumer no
351
 
352
- # Preleva job dalla coda (cascata: HIGH β†’ NORMAL β†’ LOW β†’ BACKGROUND)
353
- raw = None
354
- for _lane in _PRIORITY_LANES:
355
- raw = await _rpop(_K_QUEUE(_lane))
356
- if raw is not None:
357
- break
358
  if raw is None:
359
  continue
360
 
@@ -415,13 +394,8 @@ async def jq_status():
415
  "ts": int(time.time() * 1000),
416
  }
417
  if _redis_configured:
418
- _ld: dict[str, int] = {}
419
- for _l in _PRIORITY_LANES:
420
- _ld[_l] = await _llen(_K_QUEUE(_l))
421
- result["queue_depth"] = _ld.get("NORMAL", 0) # backward compat
422
- result["queue_depth_total"] = sum(_ld.values())
423
- result["queue_lanes"] = _ld
424
- result["wake_pending"] = await _llen(_K_WAKE)
425
  _hb = await _rcmd(["GET", _K_CONSUMER])
426
  result["consumer_alive"] = bool(_hb and _hb.get("result"))
427
  result["brain_load"] = await get_remote_load("brain")
 
12
  Chiave: jq:wake (LIST, RPOP, TTL 30s per elemento)
13
 
14
  3. TASK DELEGATION β€” BRAIN accoda task, HANDS consuma ed esegue.
15
+ Chiave: jq:tasks:pending (LIST, LPUSH/RPOP)
 
16
  Chiave: jq:result:{taskId} (STRING, TTL 300s)
17
  Chiave: jq:events:{taskId} (LIST, TTL 300s)
18
  Chiave: jq:consumer:alive (STRING, TTL 30s β€” heartbeat HANDS consumer)
 
50
  # ── Redis keys ─────────────────────────────────────────────────────────────────
51
  _K_LOAD = lambda role: f"jq:load:{role}" # STRING β€” metriche load
52
  _K_WAKE = "jq:wake" # LIST β€” wake signals
53
+ _K_PENDING = "jq:tasks:pending" # LIST β€” job queue
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  _K_RESULT = lambda tid: f"jq:result:{tid}" # STRING β€” risultato job
55
  _K_EVENTS = lambda tid: f"jq:events:{tid}" # LIST β€” eventi SSE
56
  _K_CONSUMER = "jq:consumer:alive" # STRING β€” HB consumer
 
187
  goal: str
188
  session_id: str = ""
189
  context: dict = {}
190
+ priority: str = "realtime" # realtime | background
191
  max_steps: int = 20
192
  task_id: str = "" # se vuoto β†’ generato da BRAIN
193
 
 
204
  raise HTTPException(503, "Redis non configurato β€” job queue non disponibile")
205
 
206
  task_id = job.task_id or str(uuid.uuid4())
 
207
  payload = json.dumps({
208
  "taskId": task_id,
209
  "goal": job.goal,
210
  "session_id": job.session_id,
211
  "context": job.context,
212
+ "priority": job.priority,
213
  "max_steps": job.max_steps,
214
  "submitted_at": time.time(),
215
  "submitted_by": _SPACE_ROLE,
216
  })
217
 
218
+ ok = await _rpush(_K_PENDING, payload)
 
219
  if not ok:
220
  raise HTTPException(503, "Impossibile accodare il task su Redis")
221
 
222
+ depth = await _llen(_K_PENDING)
223
+ _logger.info("[jq] job queued taskId=%s depth=%d", task_id, depth)
224
 
225
  return {
226
+ "taskId": task_id,
227
+ "status": "queued",
 
228
  "queue_depth": depth,
229
+ "stream_url": f"/api/agent/tasks/{task_id}/stream",
230
  }
231
 
232
 
 
332
  if not _JQ_ENABLED:
333
  continue # load publisher attivo, job consumer no
334
 
335
+ # Preleva job dalla coda
336
+ raw = await _rpop(_K_PENDING)
 
 
 
 
337
  if raw is None:
338
  continue
339
 
 
394
  "ts": int(time.time() * 1000),
395
  }
396
  if _redis_configured:
397
+ result["queue_depth"] = await _llen(_K_PENDING)
398
+ result["wake_pending"] = await _llen(_K_WAKE)
 
 
 
 
 
399
  _hb = await _rcmd(["GET", _K_CONSUMER])
400
  result["consumer_alive"] = bool(_hb and _hb.get("result"))
401
  result["brain_load"] = await get_remote_load("brain")
api/kernel.py CHANGED
@@ -166,32 +166,6 @@ class KernelAPI:
166
  corr = correlation_id or str(uuid.uuid4())
167
  t_id = str(uuid.uuid4())
168
 
169
- # S15: autorizza prima dell'esecuzione β€” Policy Engine (ARCH-K2.4)
170
- try:
171
- from .policy import policy as _policy, PolicyContext as _PolicyCtx
172
- _dec = await _policy.check(_PolicyCtx(
173
- task_id=t_id,
174
- session_id=session_id or "",
175
- action="task.submit",
176
- priority=priority,
177
- correlation_id=corr,
178
- ))
179
- if not _dec.allowed:
180
- _logger.warning(
181
- "[kernel.submit_task] policy deny corr=%s action=%s reason=%s",
182
- corr, _dec.action_taken, _dec.reason,
183
- )
184
- return TaskResult(
185
- task_id=t_id,
186
- correlation_id=corr,
187
- status="error",
188
- queue_backend="none",
189
- error=f"policy:{_dec.action_taken}:{_dec.reason}",
190
- )
191
- timeout_s = _dec.adjusted_timeout_s if _dec.adjusted_timeout_s is not None else timeout_s
192
- except Exception as _pe:
193
- _logger.debug("[kernel.submit_task] policy check skip (non-blocking): %s", _pe)
194
-
195
  job = {
196
  "task_id": t_id,
197
  "correlation_id": corr,
@@ -246,30 +220,6 @@ class KernelAPI:
246
  corr = correlation_id or str(uuid.uuid4())
247
  cache_key = f"k:{model_hint}:{hash(str(messages))}"
248
 
249
- # S15: autorizza chiamata LLM β€” Policy Engine (ARCH-K2.4)
250
- try:
251
- from .policy import policy as _policy, PolicyContext as _PolicyCtx
252
- _dec = await _policy.check(_PolicyCtx(
253
- session_id=session_id or "",
254
- action="llm.call",
255
- tokens_hint=max_tokens,
256
- correlation_id=corr,
257
- ))
258
- if not _dec.allowed:
259
- _logger.warning(
260
- "[kernel.chat] policy deny corr=%s action=%s reason=%s",
261
- corr, _dec.action_taken, _dec.reason,
262
- )
263
- return ChatResult(
264
- correlation_id=corr,
265
- content="",
266
- provider="policy",
267
- model="none",
268
- error=f"policy:{_dec.action_taken}:{_dec.reason}",
269
- )
270
- except Exception as _pe:
271
- _logger.debug("[kernel.chat] policy check skip (non-blocking): %s", _pe)
272
-
273
  # Cache read
274
  try:
275
  from .llm_cache import get_cached_response, cache_response
 
166
  corr = correlation_id or str(uuid.uuid4())
167
  t_id = str(uuid.uuid4())
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  job = {
170
  "task_id": t_id,
171
  "correlation_id": corr,
 
220
  corr = correlation_id or str(uuid.uuid4())
221
  cache_key = f"k:{model_hint}:{hash(str(messages))}"
222
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  # Cache read
224
  try:
225
  from .llm_cache import get_cached_response, cache_response
api/providers.py CHANGED
@@ -29,7 +29,7 @@ async def health():
29
  'status': 'ok',
30
  'version': '3.4.2',
31
  'supabase': _sb is not None,
32
- 'backend': 'HuggingFace Spaces',
33
  }
34
 
35
 
@@ -233,17 +233,8 @@ async def ai_provider_health(role: AuthRole = Depends(require_role(AuthRole.MACH
233
  "model": provider.default_model.split("/")[-1][:28]}
234
  except Exception as exc:
235
  ms = round((time.monotonic() - t0) * 1000)
236
- _exc_str = str(exc)
237
- _exc_type = type(exc).__name__
238
- # AUD-003: distingui 401 (token invalido) da 429 (quota esaurita) da errore generico
239
- _status = (
240
- "invalid_token" if ("401" in _exc_str or "AuthenticationError" in _exc_type or "Unauthorized" in _exc_str) else
241
- "quota_exhausted" if ("429" in _exc_str or "RateLimitError" in _exc_type or "quota" in _exc_str.lower()) else
242
- "timeout" if ("timeout" in _exc_str.lower() or "TimeoutError" in _exc_type) else
243
- "error"
244
- )
245
- return {"name": provider.name, "ok": False, "status": _status, "latency_ms": ms,
246
- "error": _exc_str[:300], "model": provider.default_model.split("/")[-1][:28]} # S606
247
 
248
  results = list(await asyncio.gather(*[_probe(p) for p in client.providers]))
249
  payload = {"providers": results, "tested_at": int(time.time() * 1000)}
@@ -309,29 +300,15 @@ async def _heartbeat_probe_all() -> list:
309
  timeout=10.0,
310
  )
311
  ms = round((time.monotonic() - t0) * 1000)
312
- return {"name": provider.name, "ok": True, "status": "ok", "latency_ms": ms,
313
- "model": provider.default_model.split("/")[-1][:28]}
314
  except Exception as exc:
315
  ms = round((time.monotonic() - t0) * 1000)
316
- _exc_str = str(exc)
317
- _exc_type = type(exc).__name__
318
- # AUD-003 (completo): distingui 401/token-invalido da 429/quota da timeout da errore.
319
- # Allineato con _probe in ai_provider_health β€” stesso schema per output coerente
320
- # tra /api/ai-health e /api/providers/heartbeat.
321
- _status = (
322
- "invalid_token" if ("401" in _exc_str or "AuthenticationError" in _exc_type or "Unauthorized" in _exc_str) else
323
- "quota_exhausted" if ("429" in _exc_str or "RateLimitError" in _exc_type or "quota" in _exc_str.lower()) else
324
- "timeout" if ("timeout" in _exc_str.lower() or "TimeoutError" in _exc_type) else
325
- "error"
326
- )
327
- return {"name": provider.name, "ok": False, "status": _status, "latency_ms": ms,
328
- "error": _exc_str[:300], "model": provider.default_model.split("/")[-1][:28]} # S606
329
 
330
  return list(await asyncio.gather(*[_probe(p) for p in client.providers]))
331
  except Exception as exc:
332
  _logger.warning("heartbeat probe failed: %s", exc)
333
- # AUD-010: preserve last known provider list β€” don't zero-out on transient crash
334
- return list(_heartbeat_state.get("providers", []))
335
 
336
 
337
  async def _heartbeat_loop() -> None:
@@ -734,7 +711,7 @@ async def health_full(role: AuthRole = Depends(require_role(AuthRole.MACHINE))):
734
  cfg = await asyncio.wait_for(_tg_cfg(), timeout=2.0)
735
  if not cfg or not cfg.get("token"):
736
  return {"ok": False, "configured": False,
737
- "detail": "Imposta TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID come variabili env HF Space"}
738
  import httpx
739
  async with httpx.AsyncClient(timeout=3.0) as hc:
740
  r = await hc.get(f"https://api.telegram.org/bot{cfg['token']}/getMe")
 
29
  'status': 'ok',
30
  'version': '3.4.2',
31
  'supabase': _sb is not None,
32
+ 'backend': 'HuggingFace Spaces / Railway',
33
  }
34
 
35
 
 
233
  "model": provider.default_model.split("/")[-1][:28]}
234
  except Exception as exc:
235
  ms = round((time.monotonic() - t0) * 1000)
236
+ return {"name": provider.name, "ok": False, "status": "error", "latency_ms": ms,
237
+ "error": str(exc)[:300], "model": provider.default_model.split("/")[-1][:28]} # S606: 200β†’300
 
 
 
 
 
 
 
 
 
238
 
239
  results = list(await asyncio.gather(*[_probe(p) for p in client.providers]))
240
  payload = {"providers": results, "tested_at": int(time.time() * 1000)}
 
300
  timeout=10.0,
301
  )
302
  ms = round((time.monotonic() - t0) * 1000)
303
+ return {"name": provider.name, "ok": True, "latency_ms": ms}
 
304
  except Exception as exc:
305
  ms = round((time.monotonic() - t0) * 1000)
306
+ return {"name": provider.name, "ok": False, "latency_ms": ms, "error": str(exc)[:300]} # S606: 200β†’300
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
  return list(await asyncio.gather(*[_probe(p) for p in client.providers]))
309
  except Exception as exc:
310
  _logger.warning("heartbeat probe failed: %s", exc)
311
+ return []
 
312
 
313
 
314
  async def _heartbeat_loop() -> None:
 
711
  cfg = await asyncio.wait_for(_tg_cfg(), timeout=2.0)
712
  if not cfg or not cfg.get("token"):
713
  return {"ok": False, "configured": False,
714
+ "detail": "Imposta TELEGRAM_BOT_TOKEN + TELEGRAM_CHAT_ID in Railway"}
715
  import httpx
716
  async with httpx.AsyncClient(timeout=3.0) as hc:
717
  r = await hc.get(f"https://api.telegram.org/bot{cfg['token']}/getMe")
api/scheduler.py CHANGED
@@ -38,20 +38,8 @@ from .auth_guard import require_role, AuthRole
38
  from fastapi.responses import StreamingResponse
39
  from pydantic import BaseModel
40
  import logging
 
41
 
42
- # ── ARCH-I4.6: Event Bus integration (fire-and-forget, non-blocking) ─────────
43
- async def _publish_scheduler_event(topic: str, payload: dict) -> None:
44
- """Pubblica evento sul bus interno β€” silenzioso su qualsiasi errore."""
45
- try:
46
- from .event_bus import publish # import lazy per evitare circular import
47
- from .event_bus import BusEvent
48
- await publish(BusEvent(
49
- topic=topic,
50
- payload=payload,
51
- source="scheduler",
52
- ))
53
- except Exception:
54
- pass # Event bus non critico β€” non interrompe l'esecuzione del task
55
  logger = logging.getLogger("agente_ai.scheduler")
56
 
57
  def _log_task_exc(task): # GAP-2.6: log silently-dropped exceptions in fire-and-forget tasks
@@ -93,7 +81,7 @@ _STUCK_TIMEOUT_S = 300 # 5 min β€” > 120s timeout _run_goal + margi
93
 
94
 
95
  def _load_tasks() -> None:
96
- """Gap-7-FIX + AUD-005: carica da file principale, fallback a backup, poi a Supabase."""
97
  global _tasks
98
  for _path in (_TASKS_FILE, _TASKS_BAK):
99
  try:
@@ -104,27 +92,7 @@ def _load_tasks() -> None:
104
  return
105
  except Exception as exc:
106
  logger.warning("Scheduler: load da %s fallito (%s) β€” provo backup", _path, exc)
107
- # AUD-005: /tmp assente/corrotto β†’ Supabase fallback (ultimi 24h, status != done)
108
  _tasks = {}
109
- try:
110
- from .state import _sb
111
- if _sb is not None:
112
- _cutoff_ms = int((time.time() - 86400) * 1000)
113
- _res = (
114
- _sb.table("scheduler_tasks")
115
- .select("*")
116
- .gte("created_at", _cutoff_ms)
117
- .neq("status", "done")
118
- .execute()
119
- )
120
- if _res and _res.data:
121
- for row in _res.data:
122
- if isinstance(row, dict) and "id" in row:
123
- _tasks[row["id"]] = row
124
- logger.info("Scheduler: AUD-005 restored %d task da Supabase", len(_tasks))
125
- return
126
- except Exception as _sb_exc:
127
- logger.warning("Scheduler: AUD-005 Supabase fallback fallito (%s)", _sb_exc)
128
  logger.warning("Scheduler: nessun task salvato trovato β€” partenza vuota")
129
 
130
 
@@ -304,11 +272,6 @@ async def _execute_task(task_id: str) -> None:
304
  _broadcast_sse()
305
  if _task_notify:
306
  asyncio.create_task(_tg_start(task_id, _task_goal)).add_done_callback(_log_task_exc)
307
- # ARCH-I4.6: pubblica evento scheduler.task_started sull'Event Bus
308
- asyncio.create_task(_publish_scheduler_event(
309
- "scheduler.task_started",
310
- {"task_id": task_id, "goal": _task_goal},
311
- )).add_done_callback(_log_task_exc)
312
 
313
  try:
314
  result = await _run_goal(task["goal"], task.get("conversationId"))
@@ -330,11 +293,6 @@ async def _execute_task(task_id: str) -> None:
330
  _sb_stat_ok = "done" if one_shot else "pending"
331
 
332
  logger.info("Scheduler: βœ“ task '%s' (%s)", task.get("label"), task_id)
333
- # ARCH-I4.6: pubblica evento scheduler.task_completed sull'Event Bus
334
- asyncio.create_task(_publish_scheduler_event(
335
- "scheduler.task_completed",
336
- {"task_id": task_id, "goal": _sb_goal_ok, "status": _sb_stat_ok, "result": result[:300]},
337
- )).add_done_callback(_log_task_exc)
338
  asyncio.create_task(_sb_write_scheduler_result(task_id, _sb_goal_ok, _sb_stat_ok, result, now_ms)).add_done_callback(_log_task_exc)
339
  if _task_notify:
340
  asyncio.create_task(_tg_done(task_id, _task_goal, result[:500])).add_done_callback(_log_task_exc)
@@ -357,11 +315,6 @@ async def _execute_task(task_id: str) -> None:
357
  _broadcast_sse()
358
 
359
  logger.error("Scheduler: βœ— task %s: %s", task_id, exc)
360
- # ARCH-I4.6: pubblica evento scheduler.task_failed sull'Event Bus
361
- asyncio.create_task(_publish_scheduler_event(
362
- "scheduler.task_failed",
363
- {"task_id": task_id, "goal": _task_goal, "error": str(exc)[:300]},
364
- )).add_done_callback(_log_task_exc)
365
  # GAP-A1: log incident in registry (fire-and-forget, non-blocking)
366
  try:
367
  from .incident_registry import log_incident as _log_inc
 
38
  from fastapi.responses import StreamingResponse
39
  from pydantic import BaseModel
40
  import logging
41
+ _logger = logging.getLogger("agente_ai") # S-BUGFIX
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  logger = logging.getLogger("agente_ai.scheduler")
44
 
45
  def _log_task_exc(task): # GAP-2.6: log silently-dropped exceptions in fire-and-forget tasks
 
81
 
82
 
83
  def _load_tasks() -> None:
84
+ """Gap-7-FIX: carica da file principale, fallback a backup se corrotto."""
85
  global _tasks
86
  for _path in (_TASKS_FILE, _TASKS_BAK):
87
  try:
 
92
  return
93
  except Exception as exc:
94
  logger.warning("Scheduler: load da %s fallito (%s) β€” provo backup", _path, exc)
 
95
  _tasks = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  logger.warning("Scheduler: nessun task salvato trovato β€” partenza vuota")
97
 
98
 
 
272
  _broadcast_sse()
273
  if _task_notify:
274
  asyncio.create_task(_tg_start(task_id, _task_goal)).add_done_callback(_log_task_exc)
 
 
 
 
 
275
 
276
  try:
277
  result = await _run_goal(task["goal"], task.get("conversationId"))
 
293
  _sb_stat_ok = "done" if one_shot else "pending"
294
 
295
  logger.info("Scheduler: βœ“ task '%s' (%s)", task.get("label"), task_id)
 
 
 
 
 
296
  asyncio.create_task(_sb_write_scheduler_result(task_id, _sb_goal_ok, _sb_stat_ok, result, now_ms)).add_done_callback(_log_task_exc)
297
  if _task_notify:
298
  asyncio.create_task(_tg_done(task_id, _task_goal, result[:500])).add_done_callback(_log_task_exc)
 
315
  _broadcast_sse()
316
 
317
  logger.error("Scheduler: βœ— task %s: %s", task_id, exc)
 
 
 
 
 
318
  # GAP-A1: log incident in registry (fire-and-forget, non-blocking)
319
  try:
320
  from .incident_registry import log_incident as _log_inc
api/state.py CHANGED
@@ -33,8 +33,9 @@ _sb2: Any = None
33
  _sb_fallback: Any = None # Collaboratore D
34
 
35
  try:
36
- _SUPA_URL = os.getenv('SUPABASE_URL', '')
37
- _SUPA_KEY = os.getenv('SUPABASE_KEY') or os.getenv('SUPABASE_ANON_KEY', '')
 
38
 
39
  _SUPA_URL2 = os.getenv("SUPABASE_URL_2", "")
40
  _SUPA_KEY2 = os.getenv("SUPABASE_KEY_2", "")
@@ -422,12 +423,7 @@ def _get_executor() -> Any:
422
  return _executor
423
  try:
424
  from agents.executor import Executor
425
- # ARCH-K2.2: passa kernel singleton β€” Executor usa submit_background_task() via Kernel
426
- try:
427
- from api.kernel import kernel as _k
428
- except Exception:
429
- _k = None
430
- _executor = Executor(memory=_get_mem_manager(), kernel=_k)
431
  except Exception:
432
  _executor = None
433
  return _executor
 
33
  _sb_fallback: Any = None # Collaboratore D
34
 
35
  try:
36
+ _SUPA_URL = os.getenv('SUPABASE_URL') or os.getenv('SUPABASE_URL_A', 'https://zwdoplodbdsxfrddoxmo.supabase.com')
37
+ _SUPA_KEY = os.getenv('SUPABASE_KEY') or os.getenv('SUPABASE_ANON_KEY') or os.getenv('SUPABASE_KEY_A', 'V7rJncY0k6qm+Hqyg8kz799PgpoI1ZUkuBR8y9Hb0iCy8SzEAW+ubFWT5f0H9LpzTfGu+inQwsgmBW1qDhR5jw==')
38
+
39
 
40
  _SUPA_URL2 = os.getenv("SUPABASE_URL_2", "")
41
  _SUPA_KEY2 = os.getenv("SUPABASE_KEY_2", "")
 
423
  return _executor
424
  try:
425
  from agents.executor import Executor
426
+ _executor = Executor(memory=_get_mem_manager())
 
 
 
 
 
427
  except Exception:
428
  _executor = None
429
  return _executor
api/telegram_webhook.py CHANGED
@@ -389,20 +389,20 @@ async def _cmd_help(chat_id: int) -> None:
389
 
390
 
391
  async def _cmd_logs(chat_id: int, level: str = "WARNING") -> None:
392
- """Mostra ultimi log dal backend HF Space filtrando per livello."""
393
  import httpx as _hx
394
- backend_url = os.getenv("BACKEND_URL", "https://baida-a-terminal.hf.space").rstrip("/")
395
  await _tg_reply(chat_id,
396
  f"πŸ“‹ <b>Log Railway</b> β€” <code>{level.upper()}</code>\n⏳ <i>Fetching…</i>")
397
  try:
398
  async with _hx.AsyncClient(timeout=10.0) as c:
399
- r = await c.get(f"{backend_url}/api/telegram/logs",
400
  params={"level": level.upper(), "n": 20})
401
  data = r.json() if r.status_code == 200 else {}
402
  except Exception as exc:
403
  await _tg_reply(chat_id,
404
  "❌ <b>Log non disponibili</b>\n<code>" + html.escape(str(exc)[:200]) + "</code>\n"
405
- "<i>Controlla i log HF Space.</i>", keyboard=_BACK_KB)
406
  return
407
  records = data.get("records", [])
408
  if not records:
@@ -467,12 +467,12 @@ async def _cmd_status(chat_id: int) -> None:
467
  sched_label = "βœ… attivo" if sched_ok else "❌ fermo"
468
 
469
  ts_now = time.strftime("%Y-%m-%d %H:%M:%S")
470
- backend_url = os.getenv("BACKEND_URL", "https://baida-a-terminal.hf.space")
471
  ry_line = ""
472
  try:
473
  import httpx as _hx
474
  async with _hx.AsyncClient(timeout=4.0) as c:
475
- rv = await c.get(f"{backend_url}/api/info")
476
  if rv.status_code == 200:
477
  rj = rv.json()
478
  ry_line = ("\nπŸš‚ <b>Railway:</b> v" + rj.get("version","?")
@@ -820,10 +820,10 @@ async def _cmd_autofix(chat_id: int, hint: str = "") -> None:
820
  await _tg_edit(chat_id, msg_id, text, keyboard=_MAIN_KB if final else None)
821
 
822
  # ── Step 1: leggi errori dal log endpoint ─────────────────────────────────────────
823
- backend_url = os.getenv("BACKEND_URL", "https://baida-a-terminal.hf.space").rstrip("/")
824
  try:
825
  async with httpx.AsyncClient(timeout=10.0) as c:
826
- resp = await c.get(f"{backend_url}/api/telegram/logs",
827
  params={"level": "ERROR", "n": 30})
828
  log_data = resp.json() if resp.status_code == 200 else {}
829
  except Exception as e:
@@ -877,7 +877,7 @@ async def _cmd_autofix(chat_id: int, hint: str = "") -> None:
877
  "2. Per piu' file includi un blocco per file\n"
878
  "3. Se non riesci a determinare il file, scrivi FILE: UNKNOWN e spiega"
879
  )
880
- context = f"Backend: {backend_url} Repo: {os.getenv('GITHUB_REPO','Baida98/AI')}"
881
 
882
  _buf: list[str] = []
883
  _last: list[float] = [0.0]
@@ -1344,15 +1344,15 @@ async def _cmd_git(chat_id: int, n: int = 5) -> None:
1344
  await _tg_reply(chat_id, "\n".join(lines), keyboard=_BACK_KB)
1345
 
1346
  async def _cmd_telemetry(chat_id: int) -> None:
1347
- """πŸ“‘ Metriche runtime live: /api/telemetry + /debug/timing da HF Space."""
1348
  import httpx as _hx_t
1349
- backend_url = os.getenv("BACKEND_URL", "https://baida-a-terminal.hf.space").rstrip("/")
1350
- await _tg_reply(chat_id, "⏳ <b>Telemetria</b> β€” interrogo HF Space…")
1351
  try:
1352
  async with _hx_t.AsyncClient(timeout=8.0) as _c:
1353
  tel_r, tim_r = await asyncio.gather(
1354
- _c.get(f"{backend_url}/api/telemetry"),
1355
- _c.get(f"{backend_url}/debug/timing"),
1356
  return_exceptions=True,
1357
  )
1358
  except Exception as e:
@@ -1394,7 +1394,7 @@ async def _cmd_score(chat_id: int) -> None:
1394
  """πŸ† Score card dettagliata β€” chart + ranking 4 competitor + nodes + gaps + runtime telemetry."""
1395
  import httpx as _hx_sc, base64 as _b64_sc, json as _j_sc, urllib.parse as _ul_sc
1396
  gh_token = os.getenv("GITHUB_TOKEN", "").strip()
1397
- backend_url = os.getenv("BACKEND_URL", "https://baida-a-terminal.hf.space").rstrip("/")
1398
  await _tg_reply(chat_id, "⏳ <b>Score</b> β€” carico report + metriche runtime…")
1399
 
1400
  report: dict | None = None
@@ -1443,7 +1443,7 @@ async def _cmd_score(chat_id: int) -> None:
1443
  rt_repair: dict = {}
1444
  try:
1445
  async with _hx_sc.AsyncClient(timeout=5.0) as _c:
1446
- _tr = await _c.get(f"{backend_url}/api/telemetry")
1447
  if _tr.status_code == 200:
1448
  _td = _tr.json()
1449
  rt_timing = _td.get("timing", {})
@@ -2129,7 +2129,7 @@ async def _handle_callback(callback_query: dict, token: str) -> None:
2129
  except Exception:
2130
  await _tg_reply(chat_id, "⚠️ Integrity manager non disponibile.", token=token, keyboard=_BACK_KB)
2131
  elif data == "tgw_ping":
2132
- backend_url = os.getenv("BACKEND_URL", "https://baida-a-terminal.hf.space")
2133
  try:
2134
  async with httpx.AsyncClient(timeout=8.0) as _hxc:
2135
  r = await _hxc.get(f"{ry}/health")
@@ -2140,7 +2140,7 @@ async def _handle_callback(callback_query: dict, token: str) -> None:
2140
  token=token, keyboard=_DEV_MENU_KB)
2141
  except Exception as exc:
2142
  await _tg_reply(chat_id,
2143
- "❌ Backend HF non raggiungibile\n<code>"+html.escape(str(exc)[:150])+"</code>",
2144
  token=token, keyboard=_BACK_KB)
2145
 
2146
  # ── m β€” menu principale ───────────────────────────────────────────────────
@@ -2243,7 +2243,7 @@ async def telegram_webhook(request: Request) -> dict:
2243
  if lvl not in ("DEBUG","INFO","WARNING","ERROR","CRITICAL"): lvl = "WARNING"
2244
  _t=asyncio.create_task(_cmd_logs(chat_id, lvl)); _t.add_done_callback(_log_tg_exc)
2245
  elif cmd == "/ping":
2246
- backend_url = os.getenv("BACKEND_URL", "https://baida-a-terminal.hf.space")
2247
  import httpx as _hx
2248
  try:
2249
  async with _hx.AsyncClient(timeout=8.0) as c:
@@ -2256,7 +2256,7 @@ async def telegram_webhook(request: Request) -> dict:
2256
  keyboard=_BACK_KB)
2257
  except Exception as e:
2258
  await _tg_reply(chat_id,
2259
- "❌ Backend HF non raggiungibile\n<code>"+html.escape(str(e)[:150])+"</code>",
2260
  keyboard=_BACK_KB)
2261
  elif cmd in ("/nota", "/ricorda", "/remember"):
2262
  note_text = text[len(cmd):].strip()
@@ -2467,8 +2467,8 @@ async def setup_webhook(request: Request, role: AuthRole = Depends(require_role(
2467
  body = await request.json()
2468
  except Exception:
2469
  body = {}
2470
- # MIGRAZIONE 2026-07-19: Default a HF Space URL, fallback su CF Pages
2471
- base_url = str(body.get("webhook_url") or os.getenv("BACKEND_URL") or os.getenv("CF_PAGES_URL") or "https://baida-a-terminal.hf.space").rstrip("/")
2472
  secret = str(body.get("secret") or os.getenv("TELEGRAM_WEBHOOK_SECRET", ""))
2473
  webhook_url = f"{base_url}/api/telegram/process"
2474
  payload: dict = {
@@ -2505,8 +2505,8 @@ async def setup_telegram_webhook() -> bool:
2505
  _logger.warning("setup_telegram_webhook: TELEGRAM_BOT_TOKEN non configurato")
2506
  return False
2507
 
2508
- # MIGRAZIONE 2026-07-19: Usa HF Space URL diretto se CF Pages ha problemi di routing
2509
- base_url = os.getenv("BACKEND_URL") or os.getenv("CF_PAGES_URL") or "https://baida-a-terminal.hf.space"
2510
  base_url = base_url.rstrip("/")
2511
  secret = os.getenv("TELEGRAM_WEBHOOK_SECRET", "").strip()
2512
  webhook_url = f"{base_url}/api/telegram/process"
 
389
 
390
 
391
  async def _cmd_logs(chat_id: int, level: str = "WARNING") -> None:
392
+ """Mostra ultimi log dal backend Railway filtrando per livello."""
393
  import httpx as _hx
394
+ railway_url = os.getenv("RAILWAY_URL", "https://ai-production-4c06.up.railway.app").rstrip("/")
395
  await _tg_reply(chat_id,
396
  f"πŸ“‹ <b>Log Railway</b> β€” <code>{level.upper()}</code>\n⏳ <i>Fetching…</i>")
397
  try:
398
  async with _hx.AsyncClient(timeout=10.0) as c:
399
+ r = await c.get(f"{railway_url}/api/telegram/logs",
400
  params={"level": level.upper(), "n": 20})
401
  data = r.json() if r.status_code == 200 else {}
402
  except Exception as exc:
403
  await _tg_reply(chat_id,
404
  "❌ <b>Log non disponibili</b>\n<code>" + html.escape(str(exc)[:200]) + "</code>\n"
405
+ "<i>Controlla Railway dashboard.</i>", keyboard=_BACK_KB)
406
  return
407
  records = data.get("records", [])
408
  if not records:
 
467
  sched_label = "βœ… attivo" if sched_ok else "❌ fermo"
468
 
469
  ts_now = time.strftime("%Y-%m-%d %H:%M:%S")
470
+ railway_url = os.getenv("RAILWAY_URL","https://ai-production-4c06.up.railway.app")
471
  ry_line = ""
472
  try:
473
  import httpx as _hx
474
  async with _hx.AsyncClient(timeout=4.0) as c:
475
+ rv = await c.get(f"{railway_url}/api/info")
476
  if rv.status_code == 200:
477
  rj = rv.json()
478
  ry_line = ("\nπŸš‚ <b>Railway:</b> v" + rj.get("version","?")
 
820
  await _tg_edit(chat_id, msg_id, text, keyboard=_MAIN_KB if final else None)
821
 
822
  # ── Step 1: leggi errori dal log endpoint ─────────────────────────────────────────
823
+ railway_url = os.getenv("RAILWAY_URL","https://ai-production-4c06.up.railway.app").rstrip("/")
824
  try:
825
  async with httpx.AsyncClient(timeout=10.0) as c:
826
+ resp = await c.get(f"{railway_url}/api/telegram/logs",
827
  params={"level": "ERROR", "n": 30})
828
  log_data = resp.json() if resp.status_code == 200 else {}
829
  except Exception as e:
 
877
  "2. Per piu' file includi un blocco per file\n"
878
  "3. Se non riesci a determinare il file, scrivi FILE: UNKNOWN e spiega"
879
  )
880
+ context = f"Backend: {railway_url} Repo: {os.getenv('GITHUB_REPO','Baida98/AI')}"
881
 
882
  _buf: list[str] = []
883
  _last: list[float] = [0.0]
 
1344
  await _tg_reply(chat_id, "\n".join(lines), keyboard=_BACK_KB)
1345
 
1346
  async def _cmd_telemetry(chat_id: int) -> None:
1347
+ """πŸ“‘ Metriche runtime live: /api/telemetry + /debug/timing da Railway."""
1348
  import httpx as _hx_t
1349
+ rw_url = os.getenv("RAILWAY_URL", "https://ai-production-4c06.up.railway.app").rstrip("/")
1350
+ await _tg_reply(chat_id, "⏳ <b>Telemetria</b> β€” interrogo Railway…")
1351
  try:
1352
  async with _hx_t.AsyncClient(timeout=8.0) as _c:
1353
  tel_r, tim_r = await asyncio.gather(
1354
+ _c.get(f"{rw_url}/api/telemetry"),
1355
+ _c.get(f"{rw_url}/debug/timing"),
1356
  return_exceptions=True,
1357
  )
1358
  except Exception as e:
 
1394
  """πŸ† Score card dettagliata β€” chart + ranking 4 competitor + nodes + gaps + runtime telemetry."""
1395
  import httpx as _hx_sc, base64 as _b64_sc, json as _j_sc, urllib.parse as _ul_sc
1396
  gh_token = os.getenv("GITHUB_TOKEN", "").strip()
1397
+ rw_url = os.getenv("RAILWAY_URL", "https://ai-production-4c06.up.railway.app").rstrip("/")
1398
  await _tg_reply(chat_id, "⏳ <b>Score</b> β€” carico report + metriche runtime…")
1399
 
1400
  report: dict | None = None
 
1443
  rt_repair: dict = {}
1444
  try:
1445
  async with _hx_sc.AsyncClient(timeout=5.0) as _c:
1446
+ _tr = await _c.get(f"{rw_url}/api/telemetry")
1447
  if _tr.status_code == 200:
1448
  _td = _tr.json()
1449
  rt_timing = _td.get("timing", {})
 
2129
  except Exception:
2130
  await _tg_reply(chat_id, "⚠️ Integrity manager non disponibile.", token=token, keyboard=_BACK_KB)
2131
  elif data == "tgw_ping":
2132
+ ry = os.getenv("RAILWAY_URL","https://ai-production-4c06.up.railway.app")
2133
  try:
2134
  async with httpx.AsyncClient(timeout=8.0) as _hxc:
2135
  r = await _hxc.get(f"{ry}/health")
 
2140
  token=token, keyboard=_DEV_MENU_KB)
2141
  except Exception as exc:
2142
  await _tg_reply(chat_id,
2143
+ "❌ Railway non raggiungibile\n<code>"+html.escape(str(exc)[:150])+"</code>",
2144
  token=token, keyboard=_BACK_KB)
2145
 
2146
  # ── m β€” menu principale ───────────────────────────────────────────────────
 
2243
  if lvl not in ("DEBUG","INFO","WARNING","ERROR","CRITICAL"): lvl = "WARNING"
2244
  _t=asyncio.create_task(_cmd_logs(chat_id, lvl)); _t.add_done_callback(_log_tg_exc)
2245
  elif cmd == "/ping":
2246
+ ry = os.getenv("RAILWAY_URL","https://ai-production-4c06.up.railway.app")
2247
  import httpx as _hx
2248
  try:
2249
  async with _hx.AsyncClient(timeout=8.0) as c:
 
2256
  keyboard=_BACK_KB)
2257
  except Exception as e:
2258
  await _tg_reply(chat_id,
2259
+ "❌ Railway non raggiungibile\n<code>"+html.escape(str(e)[:150])+"</code>",
2260
  keyboard=_BACK_KB)
2261
  elif cmd in ("/nota", "/ricorda", "/remember"):
2262
  note_text = text[len(cmd):].strip()
 
2467
  body = await request.json()
2468
  except Exception:
2469
  body = {}
2470
+ # P12-FIX: Default a Railway URL per stabilitΓ , fallback su CF
2471
+ base_url = str(body.get("webhook_url") or os.getenv("RAILWAY_URL") or os.getenv("CF_PAGES_URL") or "https://ai-production-4c06.up.railway.app").rstrip("/")
2472
  secret = str(body.get("secret") or os.getenv("TELEGRAM_WEBHOOK_SECRET", ""))
2473
  webhook_url = f"{base_url}/api/telegram/process"
2474
  payload: dict = {
 
2505
  _logger.warning("setup_telegram_webhook: TELEGRAM_BOT_TOKEN non configurato")
2506
  return False
2507
 
2508
+ # P12-FIX: Usa Railway URL diretto se CF Pages ha problemi di routing
2509
+ base_url = os.getenv("RAILWAY_URL") or os.getenv("CF_PAGES_URL") or "https://ai-production-4c06.up.railway.app"
2510
  base_url = base_url.rstrip("/")
2511
  secret = os.getenv("TELEGRAM_WEBHOOK_SECRET", "").strip()
2512
  webhook_url = f"{base_url}/api/telegram/process"
api/webhook.py CHANGED
@@ -115,7 +115,7 @@ async def telegram_set_webhook(
115
  role: AuthRole = Depends(require_role(AuthRole.ADMIN)), # GAP-WEBHOOK-ADMIN-FIX
116
  ) -> dict:
117
  """
118
- Registra il webhook Telegram su HF Space (Arjanit98/Terminal).
119
  Richiede: ruolo ADMIN (header X-Admin-Token = ADMIN_TOKEN) + TELEGRAM_BOT_TOKEN env var.
120
  Chiama: POST https://api.telegram.org/bot{TOKEN}/setWebhook
121
  Il secret token Γ¨ TELEGRAM_WEBHOOK_SECRET (generato casualmente se assente).
@@ -131,21 +131,18 @@ async def telegram_set_webhook(
131
  if not _tg_token:
132
  raise HTTPException(status_code=503, detail='TELEGRAM_BOT_TOKEN non configurato')
133
 
134
- # MIGRAZIONE 2026-07-19: Railway rimosso β€” usa HF Space come base URL webhook Telegram.
135
- # PrioritΓ : WEBHOOK_BASE_URL β†’ BACKEND_URL β†’ fallback hardcoded Arjanit98/Terminal
136
- _base_url = (
137
- os.getenv('WEBHOOK_BASE_URL', '')
138
- or os.getenv('BACKEND_URL', '')
139
- or 'https://baida-a-terminal.hf.space'
140
- ).rstrip('/')
141
- _wh_url = f'{_base_url}/api/telegram/callback'
142
 
143
  _secret = os.getenv('TELEGRAM_WEBHOOK_SECRET', '')
144
  if not _secret:
145
  import secrets as _sec
146
  _secret = _sec.token_hex(24)
147
  # Non possiamo settare env var runtime, ma logghiamo per configurazione manuale
148
- _logger.critical('TG-WEBHOOK: genera TELEGRAM_WEBHOOK_SECRET=%r e aggiungilo allo Space HF come variabile env!', _secret)
149
 
150
  try:
151
  import httpx as _hx
@@ -266,7 +263,7 @@ async def public_chat(payload: PublicChatPayload, request: Request):
266
  S292 β€” API REST pubblica autenticata per integrazioni esterne.
267
  Auth: Authorization: Bearer <PUBLIC_API_TOKEN>
268
  """
269
- _expected = os.getenv('PUBLIC_API_TOKEN', '').strip()
270
  if not _expected:
271
  raise HTTPException(
272
  status_code=503,
 
115
  role: AuthRole = Depends(require_role(AuthRole.ADMIN)), # GAP-WEBHOOK-ADMIN-FIX
116
  ) -> dict:
117
  """
118
+ Registra il webhook Telegram su Railway.
119
  Richiede: ruolo ADMIN (header X-Admin-Token = ADMIN_TOKEN) + TELEGRAM_BOT_TOKEN env var.
120
  Chiama: POST https://api.telegram.org/bot{TOKEN}/setWebhook
121
  Il secret token Γ¨ TELEGRAM_WEBHOOK_SECRET (generato casualmente se assente).
 
131
  if not _tg_token:
132
  raise HTTPException(status_code=503, detail='TELEGRAM_BOT_TOKEN non configurato')
133
 
134
+ _railway_url = os.getenv('RAILWAY_PUBLIC_DOMAIN', '') or os.getenv('RAILWAY_URL', '')
135
+ if not _railway_url:
136
+ raise HTTPException(status_code=503, detail='RAILWAY_PUBLIC_DOMAIN non configurato')
137
+
138
+ _wh_url = f'https://{_railway_url.lstrip("https://").rstrip("/")}/api/telegram/callback'
 
 
 
139
 
140
  _secret = os.getenv('TELEGRAM_WEBHOOK_SECRET', '')
141
  if not _secret:
142
  import secrets as _sec
143
  _secret = _sec.token_hex(24)
144
  # Non possiamo settare env var runtime, ma logghiamo per configurazione manuale
145
+ _logger.critical('TG-WEBHOOK: genera TELEGRAM_WEBHOOK_SECRET=%r e aggiungilo a Railway env!', _secret)
146
 
147
  try:
148
  import httpx as _hx
 
263
  S292 β€” API REST pubblica autenticata per integrazioni esterne.
264
  Auth: Authorization: Bearer <PUBLIC_API_TOKEN>
265
  """
266
+ _expected = (os.getenv('PUBLIC_API_TOKEN') or os.getenv('INTERNAL_TOKEN', '')).strip()
267
  if not _expected:
268
  raise HTTPException(
269
  status_code=503,
main.py CHANGED
@@ -44,28 +44,24 @@ _logger = logging.getLogger('agente_ai')
44
  _GENERATED_TOKEN = _secrets_mod.token_hex(32)
45
  if "INTERNAL_TOKEN" not in os.environ or os.environ["INTERNAL_TOKEN"] == _GENERATED_TOKEN:
46
  os.environ['INTERNAL_TOKEN'] = _GENERATED_TOKEN
47
- _TOKEN_IS_EPHEMERAL = True # CRIT-2: esposto via /api/token-status per banner CF
48
  _logger.critical('BOOT: INTERNAL_TOKEN not set β€” ephemeral token generato per questa sessione.')
49
  _logger.critical('BOOT: Ogni restart cambia il token β†’ CF Worker riceve 401 finchΓ© il secret non Γ¨ aggiornato!')
50
  _logger.critical('BOOT: session token (copia in CF Workers secret INTERNAL_TOKEN): %r', _GENERATED_TOKEN)
51
  _logger.critical('BOOT: Fix β€” imposta INTERNAL_TOKEN uguale su HF Spaces e CF Workers secrets.')
52
  else:
53
- _TOKEN_IS_EPHEMERAL = False # CRIT-2: token correttamente configurato
54
  _logger.info('BOOT: INTERNAL_TOKEN configurato OK')
55
 
56
  # ── CORS β€” env-driven, Safari-safe ───────────────────────────────────────────
57
  # ALLOWED_ORIGINS: comma-separated list, e.g. "https://agente-ai.vercel.app,http://localhost:5173"
58
  # Supports wildcard suffix match (*.vercel.app, *.hf.space) for preview URLs.
59
  _ALLOWED_ORIGINS_ENV = os.getenv('ALLOWED_ORIGINS', '')
60
- # MED-3: localhost origins only in dev β€” never expose in production
61
- _IS_DEV = os.getenv("ENVIRONMENT", "production").lower() in ("development", "dev", "local")
62
  _ALWAYS_ALLOWED = [
63
  'http://localhost:5173',
64
  'http://localhost:4173',
65
  'http://localhost:3000',
66
  'http://localhost:8080',
67
  'http://localhost:8000',
68
- ] if _IS_DEV else []
69
  _VERCEL_PATTERNS = ['.vercel.app', '.vercel.sh', '.pages.dev']
70
  _HF_PATTERNS = ['.hf.space', '.huggingface.co']
71
 
@@ -229,22 +225,7 @@ from api.skills import router as _skills_router # P17-B2
229
  from api.event_bus import router as _event_bus_router # ARCH-F1.2: Event Bus (ADR Fase 1)
230
  from api.event_store import router as _event_store_router # ARCH-F1.3: Event Store (ADR Fase 1)
231
  from api.session_manager import router as _session_mgr_router # ARCH-F1.4: Session Manager (ADR Fase 1)
232
- from api.hf_monitor import router as _hf_monitor_router, start_monitor as _start_hf_monitor # ARCH-P5.2: HF Spaces Monitor
233
  from api.kernel import router as _kernel_router # ARCH-K2.1: AI Kernel (interfaccia unica Brainβ†’Kernel)
234
- from api.policy import router as _policy_router # ARCH-K2.4: Policy Engine
235
- from api.memory_router import router as _mem_router_router # ARCH-K2.3: Memory Router unificato
236
- from api.capability_catalog import router as _catalog_router # ARCH-E3.1: Capability Marketplace
237
- from api.capability_resolver import router as _resolver_router # ARCH-E3.2: Capability Resolver
238
- from api.plugin_system import router as _plugins_router # ARCH-E3.3: Plugin System Sandboxato
239
- from api.workflow_engine import router as _workflow_router # ARCH-I4.2: Workflow Engine
240
- from api.agent_fsm import router as _fsm_router # ARCH-I4.5: Agent FSM
241
- from api.brain_planner import router as _planner_router # ARCH-I4.1: Brain Planner
242
- from api.tool_engine import router as _tool_engine_router # ARCH-I4.3: Tool Engine
243
- from api.health_manager import router as _health_mgr_router, health_manager as _hm_singleton # OPS-1: Health Manager (Circuit Breaker + Recovery)
244
- from api.llm_router import router as _llm_router # ARCH-I4.4: LLM Provider Router (capability-aware)
245
- from api.oracle_endpoints import router as _oracle_router # ARCH-K3.1: Oracle Provider (/api/oracle/**)
246
- from api.scaffold_project import router as _scaffold_router # scaffold: /api/scaffold_project
247
- from api.whoami import router as _whoami_router # /api/whoami-v2
248
  # Doc2-1b-FIX: memory/sync router non era montato β€” endpoint /api/memory/sync/* non raggiungibili
249
  # NOTA: create_memory_sync_router(memory) Γ¨ una factory β€” richiede l'istanza MemoryManager.
250
  # GAP-5-FIX: memory/sync router montato in _on_startup() (vedi sotto)
@@ -289,20 +270,6 @@ app.include_router(_event_bus_router) # ARCH-F1.2: /api/events/publish + /api
289
  app.include_router(_event_store_router) # ARCH-F1.3: /api/events/store + /api/events/replay
290
  app.include_router(_session_mgr_router) # ARCH-F1.4: /api/sessions/**
291
  app.include_router(_kernel_router) # ARCH-K2.1: /api/kernel/** (submitTask, chat, memory, publishEvent)
292
- app.include_router(_policy_router) # ARCH-K2.4: /api/policy/** (check, budget, usage, status)
293
- app.include_router(_mem_router_router) # ARCH-K2.3: /api/memory/router/** (status)
294
- app.include_router(_catalog_router) # ARCH-E3.1: /api/catalog/** (register, heartbeat, capabilities, worker-announce)
295
- app.include_router(_resolver_router) # ARCH-E3.2: /api/resolver/** (resolve, resolve-many, status)
296
- app.include_router(_plugins_router) # ARCH-E3.3: /api/plugins/** (register, execute, healthcheck, rollback)
297
- app.include_router(_workflow_router) # ARCH-I4.2: /api/workflow/** (submit, cancel, executions, status)
298
- app.include_router(_fsm_router) # ARCH-I4.5: /api/agent-fsm/** (run, runs, status)
299
- app.include_router(_planner_router) # ARCH-I4.1: /api/brain/** (plan, reflect, status)
300
- app.include_router(_tool_engine_router) # ARCH-I4.3: /api/tools/** (register, list, schema)
301
- app.include_router(_health_mgr_router) # OPS-1: /api/health-manager/** (status, report, recover, traffic)
302
- app.include_router(_llm_router) # ARCH-I4.4: /api/llm/** (route, call, status, reload)
303
- app.include_router(_oracle_router) # ARCH-K3.1: /api/oracle/** (health, status, reason)
304
- app.include_router(_scaffold_router) # scaffold: /api/scaffold_project
305
- app.include_router(_whoami_router) # whoami: /api/whoami-v2
306
  # (memory/sync router montato in _on_startup)
307
 
308
  # ── Startup: heartbeat + warmup ────────────────────────────────────────────────
@@ -327,16 +294,6 @@ async def _on_startup():
327
  _start_scheduler()
328
  _start_incident_reg() # GAP-A1: Incident Registry
329
  _start_decision_mem() # GAP-A2: Decision Memory
330
- try:
331
- _start_hf_monitor()
332
- _logger.info('BOOT: hf_monitor polling avviato (ARCH-P5.2)')
333
- except Exception as _hfm_err:
334
- _logger.warning('BOOT: hf_monitor skip β€” %s', _hfm_err)
335
- try:
336
- _hm_singleton.start_monitor()
337
- _logger.info('BOOT: health_manager monitor avviato (OPS-1)')
338
- except Exception as _hm_err:
339
- _logger.warning('BOOT: health_manager monitor skip β€” %s', _hm_err)
340
  _logger.info('BOOT: scheduler server-side avviato')
341
  # S388: warmup TCP connection pools β€” inizializza i client Groq con 1 token
342
  # cosΓ¬ la prima vera richiesta utente non paga il costo di handshake HTTP/TLS (~80ms per provider).
@@ -363,10 +320,6 @@ async def _on_startup():
363
  app.include_router(_skills_router) # P17-B2
364
  except Exception as _skills_err:
365
  _logger.warning('BOOT: skills_router registration err β€” %s', _skills_err)
366
- try:
367
- app.include_router(_hf_monitor_router) # ARCH-P5.2: HF Spaces Monitor
368
- except Exception as _hfr_err:
369
- _logger.warning('BOOT: hf_monitor_router registration err β€” %s', _hfr_err)
370
  import asyncio as _aio
371
  _t_wm = _aio.create_task(_startup_warmup())
372
  _t_wm.add_done_callback(lambda t: _log_task_exc(t, 'startup_warmup'))
@@ -409,23 +362,6 @@ async def _on_startup():
409
  _logger.info('BOOT: job queue consumer/publisher avviato (SPACE_ROLE=%s)', os.getenv('SPACE_ROLE', 'unknown'))
410
  except Exception as _jq_err:
411
  _logger.warning('BOOT: job queue consumer skip β€” %s', _jq_err)
412
- # ARCH-E3.1: avvia cleanup loop Catalog (rimuove provider con TTL scaduto)
413
- try:
414
- from api.capability_catalog import catalog as _capability_catalog
415
- _capability_catalog.start_cleanup_loop()
416
- _logger.info('BOOT: Capability Catalog cleanup loop avviato (TTL=%ss)', os.getenv('CATALOG_TTL_S', '300'))
417
- except Exception as _cat_err:
418
- _logger.warning('BOOT: capability catalog cleanup skip β€” %s', _cat_err)
419
-
420
- # ARCH-I4.3: bootstrap tool fondamentali
421
- try:
422
- from api.bootstrap_tools import bootstrap_all_tools as _bootstrap
423
- _t_bt = _aio.create_task(_bootstrap())
424
- _t_bt.add_done_callback(lambda t: _log_task_exc(t, 'bootstrap_tools'))
425
- _logger.info('BOOT: Tool bootstrap task creato')
426
- except Exception as _bt_err:
427
- _logger.warning('BOOT: tool bootstrap skip β€” %s', _bt_err)
428
-
429
  # TG-WEBHOOK-AUTO: Registra il webhook all'avvio se USE_WEBHOOK=true
430
  if os.getenv('USE_WEBHOOK', '').lower() == 'true':
431
  try:
@@ -437,24 +373,6 @@ async def _on_startup():
437
  _logger.warning('BOOT: Telegram webhook auto-setup skip β€” %s', _tg_wh_err)
438
 
439
 
440
- # ── CRIT-2: /api/token-status β€” PUBLIC endpoint (no auth) per CF Worker ──────
441
- # CF Worker usa questa risposta per mostrare un banner se il token Γ¨ ephemeral.
442
- # Non rivela il token β€” solo lo stato (ephemeral vs configurato).
443
- @app.get('/api/token-status', include_in_schema=False)
444
- async def _token_status_endpoint():
445
- """CRIT-2: permette al CF Worker di rilevare INTERNAL_TOKEN ephemeral silenzioso."""
446
- from fastapi.responses import JSONResponse
447
- return JSONResponse({
448
- 'token_configured': not _TOKEN_IS_EPHEMERAL,
449
- 'ephemeral': _TOKEN_IS_EPHEMERAL,
450
- 'message': (
451
- 'INTERNAL_TOKEN non configurato β€” ogni restart invalida il token CF Worker.'
452
- if _TOKEN_IS_EPHEMERAL else
453
- 'INTERNAL_TOKEN configurato correttamente.'
454
- ),
455
- })
456
-
457
-
458
  async def _startup_warmup() -> None:
459
  """
460
  S388: Warmup dei provider Groq al boot.
 
44
  _GENERATED_TOKEN = _secrets_mod.token_hex(32)
45
  if "INTERNAL_TOKEN" not in os.environ or os.environ["INTERNAL_TOKEN"] == _GENERATED_TOKEN:
46
  os.environ['INTERNAL_TOKEN'] = _GENERATED_TOKEN
 
47
  _logger.critical('BOOT: INTERNAL_TOKEN not set β€” ephemeral token generato per questa sessione.')
48
  _logger.critical('BOOT: Ogni restart cambia il token β†’ CF Worker riceve 401 finchΓ© il secret non Γ¨ aggiornato!')
49
  _logger.critical('BOOT: session token (copia in CF Workers secret INTERNAL_TOKEN): %r', _GENERATED_TOKEN)
50
  _logger.critical('BOOT: Fix β€” imposta INTERNAL_TOKEN uguale su HF Spaces e CF Workers secrets.')
51
  else:
 
52
  _logger.info('BOOT: INTERNAL_TOKEN configurato OK')
53
 
54
  # ── CORS β€” env-driven, Safari-safe ───────────────────────────────────────────
55
  # ALLOWED_ORIGINS: comma-separated list, e.g. "https://agente-ai.vercel.app,http://localhost:5173"
56
  # Supports wildcard suffix match (*.vercel.app, *.hf.space) for preview URLs.
57
  _ALLOWED_ORIGINS_ENV = os.getenv('ALLOWED_ORIGINS', '')
 
 
58
  _ALWAYS_ALLOWED = [
59
  'http://localhost:5173',
60
  'http://localhost:4173',
61
  'http://localhost:3000',
62
  'http://localhost:8080',
63
  'http://localhost:8000',
64
+ ]
65
  _VERCEL_PATTERNS = ['.vercel.app', '.vercel.sh', '.pages.dev']
66
  _HF_PATTERNS = ['.hf.space', '.huggingface.co']
67
 
 
225
  from api.event_bus import router as _event_bus_router # ARCH-F1.2: Event Bus (ADR Fase 1)
226
  from api.event_store import router as _event_store_router # ARCH-F1.3: Event Store (ADR Fase 1)
227
  from api.session_manager import router as _session_mgr_router # ARCH-F1.4: Session Manager (ADR Fase 1)
 
228
  from api.kernel import router as _kernel_router # ARCH-K2.1: AI Kernel (interfaccia unica Brainβ†’Kernel)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  # Doc2-1b-FIX: memory/sync router non era montato β€” endpoint /api/memory/sync/* non raggiungibili
230
  # NOTA: create_memory_sync_router(memory) Γ¨ una factory β€” richiede l'istanza MemoryManager.
231
  # GAP-5-FIX: memory/sync router montato in _on_startup() (vedi sotto)
 
270
  app.include_router(_event_store_router) # ARCH-F1.3: /api/events/store + /api/events/replay
271
  app.include_router(_session_mgr_router) # ARCH-F1.4: /api/sessions/**
272
  app.include_router(_kernel_router) # ARCH-K2.1: /api/kernel/** (submitTask, chat, memory, publishEvent)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  # (memory/sync router montato in _on_startup)
274
 
275
  # ── Startup: heartbeat + warmup ────────────────────────────────────────────────
 
294
  _start_scheduler()
295
  _start_incident_reg() # GAP-A1: Incident Registry
296
  _start_decision_mem() # GAP-A2: Decision Memory
 
 
 
 
 
 
 
 
 
 
297
  _logger.info('BOOT: scheduler server-side avviato')
298
  # S388: warmup TCP connection pools β€” inizializza i client Groq con 1 token
299
  # cosΓ¬ la prima vera richiesta utente non paga il costo di handshake HTTP/TLS (~80ms per provider).
 
320
  app.include_router(_skills_router) # P17-B2
321
  except Exception as _skills_err:
322
  _logger.warning('BOOT: skills_router registration err β€” %s', _skills_err)
 
 
 
 
323
  import asyncio as _aio
324
  _t_wm = _aio.create_task(_startup_warmup())
325
  _t_wm.add_done_callback(lambda t: _log_task_exc(t, 'startup_warmup'))
 
362
  _logger.info('BOOT: job queue consumer/publisher avviato (SPACE_ROLE=%s)', os.getenv('SPACE_ROLE', 'unknown'))
363
  except Exception as _jq_err:
364
  _logger.warning('BOOT: job queue consumer skip β€” %s', _jq_err)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
365
  # TG-WEBHOOK-AUTO: Registra il webhook all'avvio se USE_WEBHOOK=true
366
  if os.getenv('USE_WEBHOOK', '').lower() == 'true':
367
  try:
 
373
  _logger.warning('BOOT: Telegram webhook auto-setup skip β€” %s', _tg_wh_err)
374
 
375
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
  async def _startup_warmup() -> None:
377
  """
378
  S388: Warmup dei provider Groq al boot.
tests/test_regression_doc2.py CHANGED
@@ -231,7 +231,7 @@ class TestMemorySyncRouterMount(unittest.TestCase):
231
  class TestTerminalRoutingNotFixedOnSpaceA(unittest.TestCase):
232
  """
233
  Bug originale: il terminale (/api/terminal, /ws/terminal) veniva instradato
234
- in modo fisso sullo Space A (baida-a-terminal.hf.space / BRAIN) invece
235
  che su HANDS (Space B), causando comportamento errato in produzione.
236
 
237
  Fix: functions/api/[[catchall]].ts instrada /api/terminal e /ws/* verso
@@ -274,7 +274,7 @@ class TestTerminalRoutingNotFixedOnSpaceA(unittest.TestCase):
274
  src = self._read(self._CATCHALL)
275
  self.assertIn("env.BACKEND_URL_A", src,
276
  "BACKEND_URL_A non piΓΉ letto da env β€” verificare come viene risolto il backend BRAIN")
277
- self.assertNotIn("baida-a-terminal.hf.space", src,
278
  "Hostname reale dello Space A hardcoded in catchall.ts β€” deve restare solo un placeholder/commento")
279
 
280
  def test_space_a_only_appears_as_last_fallback_in_chain(self):
@@ -283,7 +283,7 @@ class TestTerminalRoutingNotFixedOnSpaceA(unittest.TestCase):
283
  idx_chain = src.find("_getBackendChain")
284
  self.assertNotEqual(idx_chain, -1, "_getBackendChain non trovato in agentSSE.ts")
285
  chain_block = src[idx_chain: idx_chain + 1500]
286
- idx_space_a = chain_block.find("baida-a-terminal.hf.space")
287
  idx_space_b = chain_block.find("baida00-ai-backend-collab.hf.space")
288
  self.assertNotEqual(idx_space_a, -1, "Space A non trovato nella catena di fallback")
289
  self.assertNotEqual(idx_space_b, -1, "Space B non trovato nella catena di fallback")
 
231
  class TestTerminalRoutingNotFixedOnSpaceA(unittest.TestCase):
232
  """
233
  Bug originale: il terminale (/api/terminal, /ws/terminal) veniva instradato
234
+ in modo fisso sullo Space A (arjanit98-terminal.hf.space / BRAIN) invece
235
  che su HANDS (Space B), causando comportamento errato in produzione.
236
 
237
  Fix: functions/api/[[catchall]].ts instrada /api/terminal e /ws/* verso
 
274
  src = self._read(self._CATCHALL)
275
  self.assertIn("env.BACKEND_URL_A", src,
276
  "BACKEND_URL_A non piΓΉ letto da env β€” verificare come viene risolto il backend BRAIN")
277
+ self.assertNotIn("arjanit98-terminal.hf.space", src,
278
  "Hostname reale dello Space A hardcoded in catchall.ts β€” deve restare solo un placeholder/commento")
279
 
280
  def test_space_a_only_appears_as_last_fallback_in_chain(self):
 
283
  idx_chain = src.find("_getBackendChain")
284
  self.assertNotEqual(idx_chain, -1, "_getBackendChain non trovato in agentSSE.ts")
285
  chain_block = src[idx_chain: idx_chain + 1500]
286
+ idx_space_a = chain_block.find("arjanit98-terminal.hf.space")
287
  idx_space_b = chain_block.find("baida00-ai-backend-collab.hf.space")
288
  self.assertNotEqual(idx_space_a, -1, "Space A non trovato nella catena di fallback")
289
  self.assertNotEqual(idx_space_b, -1, "Space B non trovato nella catena di fallback")