create-panal-agent 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-panal-agent",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Crea un agente de IA para Panal, funcionando y cobrando on-chain, en cinco minutos",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -247,11 +247,15 @@ export function aHandle(nombre: string): string {
247
247
  }
248
248
 
249
249
  /**
250
- * PanalNames en Monad mainnet. Vacío mientras no esté desplegado, y entonces
251
- * este paso no hace nada — que es lo correcto: no hay contrato al que pedirle.
250
+ * PanalNames en Monad mainnet, desplegado el 2026-08-14 en el bloque 95750662.
251
+ *
252
+ * Es quien reparte los nombres unicos. Hoy reclamar no cuesta nada: la tarifa
253
+ * esta a cero para que un agente recien creado —que tiene MON para gas y cero
254
+ * $PANAL— pueda quedarse con el suyo desde el primer minuto.
255
+ *
252
256
  * Se puede apuntar a otro con PANAL_NAMES_ADDRESS.
253
257
  */
254
- const PANAL_NAMES = '';
258
+ const PANAL_NAMES = '0xc94a8107C87859cAd2E472e71BbE25c15cdD614A';
255
259
 
256
260
  const NOMBRES_ABI = [
257
261
  {
@@ -421,7 +421,7 @@ async function work(taskId: bigint, brief: string, sobre: CallEnvelope | null):
421
421
  // Lo PRIMERO, antes de trabajar: si el proceso muere a mitad, esto es lo
422
422
  // único que permite retomarlo. Guardarlo después sería guardarlo nunca.
423
423
  saveBrief(taskId, brief);
424
- const task = await panal.getTask(taskId);
424
+ const task = await leerTarea(taskId);
425
425
  const salida = await handleTask(
426
426
  brief,
427
427
  contexto({ taskId, client: task.client, amount: task.amount, deadline: task.deadline }, sobre),
@@ -608,12 +608,65 @@ function pasaDelLimite(req: IncomingMessage): boolean {
608
608
  const CACHE_TAREA_MS = 5_000;
609
609
  const tareasCache = new Map<string, { cliente: Address; hasta: number }>();
610
610
 
611
+ /**
612
+ * La tarea existe en la cadena, pero el nodo que consultamos aún no la ve.
613
+ *
614
+ * NO es un error del agente ni del cliente, y por eso tiene su propio tipo:
615
+ * quien llama necesita poder distinguir «todavía no» de «se rompió algo», que
616
+ * son dos cosas con reacciones opuestas —una se reintenta, la otra no.
617
+ */
618
+ class TareaAunNoVisible extends Error {
619
+ constructor(readonly taskId: bigint) {
620
+ super(`la tarea #${taskId} todavía no es visible en este nodo RPC`);
621
+ this.name = 'TareaAunNoVisible';
622
+ }
623
+ }
624
+
625
+ /**
626
+ * ¿Este fallo es «esa tarea no existe (todavía)»?
627
+ *
628
+ * `tasks` es un array público, así que su getter solo puede revertir por
629
+ * índice fuera de rango. Cualquier otro fallo —RPC caído, timeout, red— tiene
630
+ * otra forma y NO se disfraza de esto: tragárselo escondería una avería real.
631
+ */
632
+ function pareceInexistente(err: unknown): boolean {
633
+ const m = err instanceof Error ? `${err.message}` : String(err);
634
+ return /revert|out-of-bounds|out of bounds|0x32/i.test(m);
635
+ }
636
+
637
+ /**
638
+ * Lee la tarea aguantando el desfase entre nodos.
639
+ *
640
+ * POR QUÉ EXISTE. El cliente mina `createTask` contra SU RPC y, en cuanto
641
+ * tiene el recibo, nos manda el encargo. Nosotros validamos leyendo la tarea
642
+ * contra el NUESTRO, que es otro nodo y puede ir un bloque por detrás: para él
643
+ * esa tarea aún no existe, el getter revierte y el envío se caía con un 500.
644
+ * El cliente veía «no se pudo enviar el brief» y tenía que reintentar a mano,
645
+ * con su dinero ya bloqueado. Fallaba a la primera y funcionaba a la segunda,
646
+ * que es la firma de una carrera, no de una avería.
647
+ *
648
+ * Cuatro intentos con espera creciente cubren de sobra un bloque de Monad
649
+ * (~800 ms) sin castigar al RPC compartido.
650
+ */
651
+ async function leerTarea(taskId: bigint): ReturnType<typeof panal.getTask> {
652
+ for (let intento = 1; intento <= 4; intento++) {
653
+ try {
654
+ return await panal.getTask(taskId);
655
+ } catch (err) {
656
+ if (!pareceInexistente(err)) throw err;
657
+ if (intento === 4) break;
658
+ await new Promise((r) => setTimeout(r, 250 * intento));
659
+ }
660
+ }
661
+ throw new TareaAunNoVisible(taskId);
662
+ }
663
+
611
664
  async function clienteDeTarea(taskId: bigint): Promise<Address> {
612
665
  const k = taskId.toString();
613
666
  const ahora = Date.now();
614
667
  const cacheada = tareasCache.get(k);
615
668
  if (cacheada && cacheada.hasta > ahora) return cacheada.cliente;
616
- const task = await panal.getTask(taskId);
669
+ const task = await leerTarea(taskId);
617
670
  if (tareasCache.size > 1_000) for (const [kk, v] of tareasCache) if (v.hasta <= ahora) tareasCache.delete(kk);
618
671
  tareasCache.set(k, { cliente: task.client, hasta: ahora + CACHE_TAREA_MS });
619
672
  return task.client;
@@ -829,7 +882,9 @@ const server = createServer((req, res) => {
829
882
  throw err;
830
883
  }
831
884
 
832
- const task = await panal.getTask(taskId);
885
+ // Con reintentos: el cliente acaba de minar la tarea contra otro nodo y
886
+ // el nuestro puede ir por detrás. Ver `leerTarea`.
887
+ const task = await leerTarea(taskId);
833
888
 
834
889
  // Cuatro comprobaciones, y las cuatro importan: que la tarea sea tuya,
835
890
  // que siga abierta, que quien dice firmar sea el cliente que pagó, y que
@@ -956,6 +1011,16 @@ const server = createServer((req, res) => {
956
1011
 
957
1012
  json(res, 404, { error: 'no existe' });
958
1013
  })().catch((err) => {
1014
+ // «Todavía no la veo» NO es un 500. Con 425 (Too Early) quien llama sabe
1015
+ // que reintentar tiene sentido; con 500 parecía una avería del agente y el
1016
+ // dashboard se rendía dejando al cliente reenviando a mano. Se responde
1017
+ // rápido y sin ruido en el log: no hay nada roto que mirar.
1018
+ if (err instanceof TareaAunNoVisible) {
1019
+ if (!res.headersSent) {
1020
+ json(res, 425, { error: err.message, reintentable: true });
1021
+ } else res.end();
1022
+ return;
1023
+ }
959
1024
  console.error(`[http] ${err instanceof Error ? err.message : err}`);
960
1025
  if (!res.headersSent) json(res, 500, { error: 'error interno' });
961
1026
  else res.end();