@arcadialdev/arcality 2.4.37 → 2.4.49

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.
@@ -422,10 +422,12 @@ var KnowledgeService = class _KnowledgeService {
422
422
  apiBase;
423
423
  apiKey;
424
424
  projectId = null;
425
+ orgId = null;
425
426
  constructor() {
426
427
  this.apiBase = process.env.ARCALITY_API_URL || "https://arcalityqadev.arcadial.lat";
427
428
  this.apiKey = process.env.ARCALITY_API_KEY || "";
428
429
  this.projectId = process.env.ARCALITY_PROJECT_ID || null;
430
+ this.orgId = process.env.ARCALITY_ORG_ID || null;
429
431
  }
430
432
  /**
431
433
  * Fetch wrapper with a 10-second timeout to prevent hanging.
@@ -530,10 +532,13 @@ var KnowledgeService = class _KnowledgeService {
530
532
  return null;
531
533
  try {
532
534
  const pathUrl = new URL(url).pathname;
533
- const res = await this.fetchWithTimeout(
534
- `${this.apiBase}/api/v1/portal/context?project_id=${pid}&path=${encodeURIComponent(pathUrl)}`,
535
- { headers: { "x-api-key": this.apiKey } }
536
- );
535
+ let contextUrl = `${this.apiBase}/api/v1/portal/context?project_id=${pid}&path=${encodeURIComponent(pathUrl)}`;
536
+ if (this.orgId) {
537
+ contextUrl += `&organization_id=${this.orgId}`;
538
+ }
539
+ const res = await this.fetchWithTimeout(contextUrl, {
540
+ headers: { "x-api-key": this.apiKey }
541
+ });
537
542
  if (!res.ok)
538
543
  return null;
539
544
  return await res.json();
@@ -558,6 +563,7 @@ var KnowledgeService = class _KnowledgeService {
558
563
  "x-api-key": this.apiKey
559
564
  },
560
565
  body: JSON.stringify({
566
+ organization_id: this.orgId,
561
567
  project_id: pid,
562
568
  rule_type: "UI_UX",
563
569
  // Default según especificación
@@ -585,6 +591,7 @@ var KnowledgeService = class _KnowledgeService {
585
591
  "x-api-key": this.apiKey
586
592
  },
587
593
  body: JSON.stringify({
594
+ organization_id: this.orgId,
588
595
  project_id: pid,
589
596
  field_identifier: identifier,
590
597
  field_type: type,
@@ -611,6 +618,7 @@ var KnowledgeService = class _KnowledgeService {
611
618
  "x-api-key": this.apiKey
612
619
  },
613
620
  body: JSON.stringify({
621
+ organization_id: this.orgId,
614
622
  project_id: pid,
615
623
  doc_type: docType,
616
624
  title,
@@ -635,12 +643,29 @@ var KnowledgeService = class _KnowledgeService {
635
643
  var getBase = () => process.env.ARCALITY_API_URL ? `${process.env.ARCALITY_API_URL}/api/v1` : null;
636
644
  var getKey = () => process.env.ARCALITY_API_KEY || "";
637
645
  var getPid = () => process.env.ARCALITY_PROJECT_ID || "";
646
+ var getOrgId = () => process.env.ARCALITY_ORG_ID;
647
+ var getMissionId = () => process.env.ARCALITY_MISSION_ID || EMPTY_GUID;
638
648
  var EMPTY_GUID = "00000000-0000-0000-0000-000000000000";
649
+ var configWarningLogged = false;
639
650
  function isConfigured() {
640
651
  const base = getBase();
641
652
  const key = getKey();
642
653
  const pid = getPid();
643
- if (!base || !key || !pid || pid === EMPTY_GUID) {
654
+ const orgId = getOrgId();
655
+ if (!base || !key || !pid || pid === EMPTY_GUID || !orgId) {
656
+ if (!configWarningLogged) {
657
+ const missing = [];
658
+ if (!base)
659
+ missing.push("ARCALITY_API_URL");
660
+ if (!key)
661
+ missing.push("ARCALITY_API_KEY");
662
+ if (!pid || pid === EMPTY_GUID)
663
+ missing.push("ARCALITY_PROJECT_ID");
664
+ if (!orgId)
665
+ missing.push("ARCALITY_ORG_ID");
666
+ console.warn(`[CollectiveMemory] Service disabled. Missing: ${missing.join(", ")}`);
667
+ configWarningLogged = true;
668
+ }
644
669
  return false;
645
670
  }
646
671
  return true;
@@ -656,6 +681,7 @@ async function pushRule(rule) {
656
681
  "x-api-key": getKey()
657
682
  },
658
683
  body: JSON.stringify({
684
+ organization_id: getOrgId(),
659
685
  project_id: getPid(),
660
686
  rule_type: rule.rule_type,
661
687
  title: rule.title.substring(0, 100),
@@ -690,6 +716,7 @@ async function pushField(field) {
690
716
  "x-api-key": getKey()
691
717
  },
692
718
  body: JSON.stringify({
719
+ organization_id: getOrgId(),
693
720
  project_id: getPid(),
694
721
  page_id: field.page_id ?? null,
695
722
  field_identifier: field.field_identifier.substring(0, 100),
@@ -719,6 +746,7 @@ async function pushKnowledge(knowledge) {
719
746
  "x-api-key": getKey()
720
747
  },
721
748
  body: JSON.stringify({
749
+ organization_id: getOrgId(),
722
750
  project_id: getPid(),
723
751
  doc_type: knowledge.doc_type,
724
752
  title: knowledge.title.substring(0, 150),
@@ -738,12 +766,77 @@ async function pushKnowledge(knowledge) {
738
766
  return null;
739
767
  }
740
768
  }
769
+ async function searchPromptPattern(prompt, limit = 5) {
770
+ if (!isConfigured())
771
+ return [];
772
+ try {
773
+ const apiUrl = process.env.ARCALITY_API_URL ? `${process.env.ARCALITY_API_URL}/api/v1/prompt-execution-patterns/search` : `${getBase()}/prompt-execution-patterns/search`;
774
+ const payload = {
775
+ organization_id: getOrgId(),
776
+ project_id: getPid(),
777
+ prompt,
778
+ limit
779
+ };
780
+ console.log(`[PatternSearch] \u{1F50D} Buscando patrones en: ${apiUrl}`);
781
+ console.log(`[PatternSearch] \u{1F4E6} Payload: ${JSON.stringify(payload, null, 2)}`);
782
+ const res = await fetch(apiUrl, {
783
+ method: "POST",
784
+ headers: {
785
+ "Content-Type": "application/json",
786
+ "x-api-key": getKey()
787
+ },
788
+ body: JSON.stringify(payload)
789
+ });
790
+ if (!res.ok) {
791
+ console.warn(`[PatternSearch] HTTP ${res.status} from ${apiUrl}`);
792
+ return [];
793
+ }
794
+ const data = await res.json();
795
+ const results = Array.isArray(data) ? data : data.matches || data.results || [];
796
+ console.log(`[PatternSearch] \u2705 ${results.length} patrones encontrados.`);
797
+ return results;
798
+ } catch (err) {
799
+ console.warn(`[PatternSearch] \u26A0\uFE0F Error buscando patrones: ${err?.message || "unknown"}`);
800
+ return [];
801
+ }
802
+ }
803
+ async function savePromptPattern(patternData) {
804
+ if (!isConfigured())
805
+ return false;
806
+ try {
807
+ const apiUrl = process.env.ARCALITY_API_URL ? `${process.env.ARCALITY_API_URL}/api/v1/prompt-execution-patterns` : `${getBase()}/prompt-execution-patterns`;
808
+ const payload = {
809
+ organization_id: getOrgId(),
810
+ project_id: getPid(),
811
+ mission_id: getMissionId(),
812
+ ...patternData
813
+ };
814
+ console.log(`[PatternSave] \u{1F4E4} POST ${apiUrl} | org=${payload.organization_id} proj=${payload.project_id} mission=${payload.mission_id}`);
815
+ const res = await fetch(apiUrl, {
816
+ method: "POST",
817
+ headers: {
818
+ "Content-Type": "application/json",
819
+ "x-api-key": getKey()
820
+ },
821
+ body: JSON.stringify(payload)
822
+ });
823
+ if (!res.ok) {
824
+ console.warn(`[PatternSave] \u26A0\uFE0F HTTP ${res.status} al guardar patr\xF3n en ${apiUrl}: ${await res.text().catch(() => "")}`);
825
+ } else {
826
+ console.log(`[PatternSave] \u2705 Patr\xF3n guardado exitosamente.`);
827
+ }
828
+ return res.ok;
829
+ } catch (err) {
830
+ console.warn(`[PatternSave] \u26A0\uFE0F Error guardando patr\xF3n: ${err?.message || "unknown"}`);
831
+ return false;
832
+ }
833
+ }
741
834
  async function getKnownFieldIdentifiers(pathUrl) {
742
835
  if (!isConfigured())
743
836
  return [];
744
837
  try {
745
838
  const res = await fetch(
746
- `${getBase()}/portal/context?project_id=${getPid()}&path=${encodeURIComponent(pathUrl)}`,
839
+ `${getBase()}/portal/context?organization_id=${getOrgId()}&project_id=${getPid()}&path=${encodeURIComponent(pathUrl)}`,
747
840
  { headers: { "x-api-key": getKey() } }
748
841
  );
749
842
  if (!res.ok)
@@ -801,6 +894,8 @@ var AIAgentHelper = class {
801
894
  logs = [];
802
895
  checkpoints = /* @__PURE__ */ new Map();
803
896
  sessionStorage = /* @__PURE__ */ new Map();
897
+ criticalNetworkError = null;
898
+ criticalJsError = null;
804
899
  constructor(page, contextDir = "out", testInfo, knowledgeService) {
805
900
  this.page = page;
806
901
  this.contextDir = contextDir;
@@ -815,10 +910,44 @@ var AIAgentHelper = class {
815
910
  });
816
911
  this.page.on("requestfailed", (request) => {
817
912
  const failure = request.failure();
913
+ const resourceType = request.resourceType();
914
+ const url = request.url();
915
+ if (["fetch", "xhr", "document"].includes(resourceType)) {
916
+ if (!url.includes("google") && !url.includes("analytics") && !url.includes("telemetry")) {
917
+ if (resourceType === "document" || request.method() !== "GET") {
918
+ this.criticalNetworkError = `Network Request Failed (${request.method()}): ${url} - ${failure?.errorText || "Aborted"}`;
919
+ }
920
+ }
921
+ }
818
922
  this.logs.push(`[NETWORK_ERROR] ${request.method()} ${request.url()} - ${failure?.errorText || "Unknown error"}`);
819
923
  if (this.logs.length > 30)
820
924
  this.logs.shift();
821
925
  });
926
+ this.page.on("response", (response) => {
927
+ const status = response.status();
928
+ if (status >= 500) {
929
+ const request = response.request();
930
+ const resourceType = request.resourceType();
931
+ const url = response.url();
932
+ if (["fetch", "xhr", "document"].includes(resourceType)) {
933
+ if (!url.includes("google") && !url.includes("analytics") && !url.includes("telemetry")) {
934
+ if (resourceType === "document" || request.method() !== "GET") {
935
+ this.criticalNetworkError = `HTTP Server Error ${status}: ${request.method()} ${url}`;
936
+ }
937
+ }
938
+ }
939
+ }
940
+ });
941
+ this.page.on("pageerror", (exception) => {
942
+ this.criticalJsError = `Excepci\xF3n JS no manejada: ${exception.message}`;
943
+ this.logs.push(`[JS_CRASH] ${exception.message}`);
944
+ });
945
+ this.page.on("crash", () => {
946
+ this.criticalJsError = `Browser Crash: La pesta\xF1a del navegador colaps\xF3 inesperadamente.`;
947
+ });
948
+ }
949
+ clearNetworkError() {
950
+ this.criticalNetworkError = null;
822
951
  }
823
952
  loadSkills() {
824
953
  try {
@@ -920,16 +1049,21 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
920
1049
  const isVisible = (el) => {
921
1050
  const style = window.getComputedStyle(el);
922
1051
  const rect = el.getBoundingClientRect();
923
- const hasSize = rect.width > 1 && rect.height > 1;
924
- const isNotTranslucent = parseFloat(style.opacity || "1") > 0.05;
1052
+ const tag = el.tagName.toLowerCase();
1053
+ const isInput = tag === "input" || tag === "textarea" || tag === "select";
1054
+ const isHiddenInput = tag === "input" && el.getAttribute("type") === "hidden";
1055
+ const hasSize = isInput && !isHiddenInput ? true : rect.width > 1 && rect.height > 1;
1056
+ const isNotTranslucent = isInput && !isHiddenInput ? true : parseFloat(style.opacity || "1") > 0.05;
925
1057
  const isCssVisible = style.display !== "none" && style.visibility !== "hidden";
926
1058
  return isCssVisible && isNotTranslucent && hasSize;
927
1059
  };
928
- const nodes = Array.from(document.querySelectorAll('button, input, select, textarea, [role], a, h1, h2, label, span, p, i, svg, img, [class*="alert"],[class*="toast"],[class*="message"],[class*="error"],[class*="success"],[id*="alert"],[id*="toast"],[role="alert"],[role="status"],.error-message,.success-message'));
1060
+ const nodes = Array.from(document.querySelectorAll('button, input, select, textarea, [role], a, h1, h2, label, span, p, i, svg, img, igx-input-group, igx-select, igx-combo, [role="gridcell"], [role="row"], [role="listitem"], [class*="radio"], [class*="checkbox"], [data-state], [aria-selected], [class*="alert"],[class*="toast"],[class*="message"],[class*="error"],[class*="success"],[id*="alert"],[id*="toast"],[role="alert"],[role="status"],.error-message,.success-message'));
929
1061
  let localCount = 0;
1062
+ const nameCounts = {};
930
1063
  return nodes.filter((el) => {
931
1064
  const htmlEl = el;
932
- if (htmlEl.tagName.toLowerCase() === "input" && htmlEl.type === "file") {
1065
+ const tag = htmlEl.tagName.toLowerCase();
1066
+ if (tag === "input" || tag === "textarea" || tag === "select") {
933
1067
  return window.getComputedStyle(htmlEl).display !== "none";
934
1068
  }
935
1069
  return isVisible(htmlEl);
@@ -946,16 +1080,26 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
946
1080
  textLower.includes("campo requerido") || textLower.includes("campo obligatorio") || textLower === "requerido" || textLower === "obligatorio" || textLower.includes("este campo es requerido") || textLower.includes("este campo es obligatorio") || textLower.includes("no puede estar vac\xEDo") || textLower.includes("no puede ser") || textLower.includes("no es v\xE1lido") || textLower.includes("no es correcto") || textLower.includes("formato incorrecto") || textLower.includes("formato inv\xE1lido") || textLower.includes("valor no permitido")) && text.length < 200 && !textLower.includes("recuerda") && !textLower.includes("antes de guardar");
947
1081
  const isCriticalSuccess = (textLower.includes("exitosamente") || textLower.includes("guardado") || textLower.includes("creado") || textLower.includes("success") || textLower.includes("correctamente") && text.length < 100) && text.length < 200;
948
1082
  const isCriticalFeedback = isCriticalFailure || isCriticalSuccess;
949
- if (!isCriticalFeedback && (tag === "span" || tag === "p" || tag === "label" || tag === "i" || tag === "svg" || tag === "img" || tag === "div")) {
950
- const hasIdentification = htmlEl.hasAttribute("aria-label") || htmlEl.hasAttribute("title") || role || htmlEl.id;
1083
+ const isFormSelection = tag === "input" && (htmlEl.getAttribute("type") === "radio" || htmlEl.getAttribute("type") === "checkbox") || (role === "radio" || role === "checkbox" || role === "switch");
1084
+ if (!isCriticalFeedback && (tag === "span" || tag === "p" || tag === "label" || tag === "i" || tag === "svg" || tag === "img" || tag === "div" || tag === "input" || tag === "textarea" || tag === "select")) {
1085
+ const hasIdentification = htmlEl.hasAttribute("aria-label") || htmlEl.hasAttribute("title") || htmlEl.hasAttribute("name") || htmlEl.hasAttribute("placeholder") || role || htmlEl.id;
951
1086
  if (text.length > 300 && !isCriticalFeedback && !hasIdentification)
952
1087
  return null;
953
- if (text.length < 2 && !hasIdentification && tag !== "svg" && tag !== "img" && tag !== "i")
1088
+ if (text.length < 2 && !hasIdentification && tag !== "svg" && tag !== "img" && tag !== "i" && !isFormSelection && tag !== "input" && tag !== "textarea" && tag !== "select")
954
1089
  return null;
955
1090
  }
1091
+ if (tag !== "form" && tag !== "table" && tag !== "tr" && tag !== "body") {
1092
+ const interactiveChildren = htmlEl.querySelectorAll('button, input, select, textarea, a, [role="button"], [role="checkbox"], [role="radio"]');
1093
+ if (interactiveChildren.length >= 2) {
1094
+ return null;
1095
+ }
1096
+ }
956
1097
  const idx = startIdx + localCount++;
957
1098
  htmlEl.setAttribute("agent-idx", idx.toString());
958
1099
  let name = text || htmlEl.getAttribute("aria-label") || htmlEl.getAttribute("title") || htmlEl.getAttribute("placeholder");
1100
+ if (name && name.length > 150) {
1101
+ name = name.substring(0, 147) + "...";
1102
+ }
959
1103
  if (!name && (tag === "input" || tag === "select" || htmlEl.getAttribute("role") === "combobox")) {
960
1104
  const labelledBy = htmlEl.getAttribute("aria-labelledby");
961
1105
  if (labelledBy) {
@@ -986,6 +1130,30 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
986
1130
  tries++;
987
1131
  }
988
1132
  }
1133
+ if (!name) {
1134
+ let next = htmlEl.nextSibling;
1135
+ let tries = 0;
1136
+ while (next && tries < 3) {
1137
+ if (next.nodeType === 3) {
1138
+ const textContent = (next.textContent || "").trim();
1139
+ if (textContent.length > 1) {
1140
+ name = textContent;
1141
+ break;
1142
+ }
1143
+ } else if (next.nodeType === 1) {
1144
+ const nextEl = next;
1145
+ if (nextEl.tagName !== "BUTTON" && nextEl.tagName !== "INPUT") {
1146
+ const nextText = (nextEl.innerText || "").trim();
1147
+ if (nextText.length > 1 && nextText.length < 150) {
1148
+ name = nextText;
1149
+ break;
1150
+ }
1151
+ }
1152
+ }
1153
+ next = next.nextSibling;
1154
+ tries++;
1155
+ }
1156
+ }
989
1157
  if (!name) {
990
1158
  const container = htmlEl.closest("div, fieldset, section");
991
1159
  if (container) {
@@ -997,6 +1165,15 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
997
1165
  }
998
1166
  }
999
1167
  }
1168
+ if (!name && (isFormSelection || tag === "input")) {
1169
+ const parentContainer = htmlEl.closest('li, tr, .row, [class*="item"], [class*="card"], div:has(> span, > p)');
1170
+ if (parentContainer) {
1171
+ const containerText = (parentContainer.innerText || "").trim();
1172
+ if (containerText.length > 1 && containerText.length < 300) {
1173
+ name = containerText.split("\n").filter(Boolean).join(" ");
1174
+ }
1175
+ }
1176
+ }
1000
1177
  }
1001
1178
  if (!name) {
1002
1179
  const iconMatch = htmlEl.outerHTML.match(/fa-([a-z0-9-]+)|md-([a-z0-9-]+)|bi-([a-z0-9-]+)/i);
@@ -1017,7 +1194,7 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1017
1194
  const isError = isCriticalFailure || isErrorClass && text.split(" ").length > 2;
1018
1195
  const isSuccess = classLower.includes("success") || isCriticalSuccess || textLower.includes("\xE9xito");
1019
1196
  const isTab = role === "tab" || classLower.includes("tab") || classLower.includes("pesta\xF1a");
1020
- if (tag === "input" || tag === "textarea" || tag === "select")
1197
+ if (tag === "input" || tag === "textarea" || tag === "select" || isFormSelection)
1021
1198
  type = "FIELD";
1022
1199
  else if (tag === "button" || role === "button" || tag === "a" || role === "link" || isTab)
1023
1200
  type = "ACTION";
@@ -1045,6 +1222,11 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1045
1222
  }
1046
1223
  if (!name)
1047
1224
  name = tag;
1225
+ const baseName = name;
1226
+ nameCounts[baseName] = (nameCounts[baseName] || 0) + 1;
1227
+ if (nameCounts[baseName] > 1) {
1228
+ name = `${baseName} - ocur ${nameCounts[baseName]}`;
1229
+ }
1048
1230
  let score = 10;
1049
1231
  if (type === "MESSAGE")
1050
1232
  score = 200;
@@ -1077,6 +1259,25 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1077
1259
  }
1078
1260
  }
1079
1261
  console.log(`>>ARCALITY_STATUS>> \u{1F50D} Percepci\xF3n profunda encontr\xF3 ${allComponents.length} componentes`);
1262
+ const urlForBlankCheck = this.page.url();
1263
+ const isAuthPage = urlForBlankCheck.includes("/login") || urlForBlankCheck.includes("/auth") || urlForBlankCheck.includes("oauth") || urlForBlankCheck.includes("/#/");
1264
+ if (allComponents.length < 10 && !isAuthPage) {
1265
+ const bodyTextLength = await this.page.evaluate(() => document.body.innerText.trim().length).catch(() => 999);
1266
+ if (bodyTextLength < 80) {
1267
+ console.warn(`
1268
+ \u26A0\uFE0F [BLANK_STATE] Primera medici\xF3n: ${allComponents.length} componentes, ${bodyTextLength} chars. Esperando re-render...`);
1269
+ await this.page.waitForTimeout(2500);
1270
+ const bodyTextLength2 = await this.page.evaluate(() => document.body.innerText.trim().length).catch(() => 999);
1271
+ const components2 = await this.page.evaluate(() => document.querySelectorAll("a, button, input, select, textarea").length).catch(() => 999);
1272
+ if (bodyTextLength2 < 80 && components2 < 10) {
1273
+ console.error(`
1274
+ \u{1F6D1} [BLANK_STATE_CRASH] Vista en blanco confirmada tras espera (Comps: ${components2}, Texto: ${bodyTextLength2} chars). URL: ${urlForBlankCheck}`);
1275
+ throw new Error(`[BLANK_STATE_CRASH] El portal carg\xF3 en blanco. Esto es un fallo de renderizado del portal, no del agente.`);
1276
+ } else {
1277
+ console.log(`>>ARCALITY_STATUS>> \u2705 [BLANK_STATE] Falsa alarma. P\xE1gina recuper\xF3 contenido tras espera (${components2} comps, ${bodyTextLength2} chars).`);
1278
+ }
1279
+ }
1280
+ }
1080
1281
  const sortedComponents = allComponents.sort((a, b) => b.score - a.score).slice(0, 500);
1081
1282
  const title = await this.page.title();
1082
1283
  const url = this.page.url();
@@ -1214,7 +1415,7 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1214
1415
  return false;
1215
1416
  const matches = words.filter((w) => stripDynamic(memPrompt).includes(w)).length;
1216
1417
  const matchRatio = matches / words.length;
1217
- if (matchRatio < 0.6)
1418
+ if (matchRatio < 0.85)
1218
1419
  return false;
1219
1420
  const memVerbs = ["elimina", "borra", "quita", "delete", "remove", "crea", "nueva", "a\xF1ade", "add", "create", "new", "edita", "modifica", "cambia", "edit", "update"].filter((v) => memPrompt.includes(v));
1220
1421
  const hasSharedVerb = currentVerbs.some((cv) => memVerbs.includes(cv));
@@ -1241,11 +1442,35 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1241
1442
  });
1242
1443
  if (matchingRun) {
1243
1444
  const memStep = matchingRun.steps_data[historyLength];
1244
- const foundComponent = state.components.find((c) => {
1445
+ let bestMatch = null;
1446
+ let bestMatchScore = -1;
1447
+ for (const c of state.components) {
1245
1448
  const cleanMemName = (memStep.componentName || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
1246
1449
  const cleanCurrName = (c.name || "").replace(/\[.*?\]/g, "").replace(/\(.*?\)/g, "").trim().toLowerCase();
1247
- return c.selector === memStep.action_data?.selector || cleanMemName !== "" && (cleanMemName === cleanCurrName || cleanCurrName.includes(cleanMemName));
1248
- });
1450
+ let score = -1;
1451
+ if (c.selector === memStep.action_data?.selector) {
1452
+ score = 100;
1453
+ } else if (cleanMemName !== "") {
1454
+ if (cleanMemName === cleanCurrName) {
1455
+ score = 90;
1456
+ } else if (cleanCurrName.includes(cleanMemName)) {
1457
+ const lengthDiff = cleanCurrName.length - cleanMemName.length;
1458
+ if (lengthDiff < 10)
1459
+ score = 80;
1460
+ else if (lengthDiff < 30)
1461
+ score = 60;
1462
+ else if (lengthDiff < 80)
1463
+ score = 40;
1464
+ else
1465
+ score = 5;
1466
+ }
1467
+ }
1468
+ if (score > bestMatchScore) {
1469
+ bestMatchScore = score;
1470
+ bestMatch = c;
1471
+ }
1472
+ }
1473
+ const foundComponent = bestMatchScore >= 40 ? bestMatch : null;
1249
1474
  if (!foundComponent && memStep.action_data?.action !== "wait") {
1250
1475
  console.log(`>>ARCALITY_STATUS>> \u{1F50D} Descartado (Componente visual no encontrado: ${memStep.componentName})`);
1251
1476
  return null;
@@ -1268,10 +1493,23 @@ ${relevantFailures.map((m) => `- ${m.prompt}`).join("\n")}
1268
1493
  return null;
1269
1494
  }
1270
1495
  if (foundComponent) {
1496
+ let actionData = { ...memStep.action_data };
1497
+ if (actionData.value === "{{secret}}") {
1498
+ const cName = (foundComponent.name || "").toLowerCase();
1499
+ if (cName.includes("password") || cName.includes("contrase\xF1a")) {
1500
+ actionData.value = "Qa_Test123!";
1501
+ } else if (cName.includes("email") || cName.includes("correo")) {
1502
+ actionData.value = `qa_${Date.now()}@test.com`;
1503
+ } else if (cName.includes("descrip")) {
1504
+ actionData.value = `Descripci\xF3n QA Automatizada ${Date.now()}`;
1505
+ } else {
1506
+ actionData.value = `QA_Test_${Date.now()}`;
1507
+ }
1508
+ }
1271
1509
  return {
1272
- thought: `[GU\xCDA DE \xC9XITO] Reutilizando paso maestro aprendido: ${memStep.action_data.action} en ${foundComponent.name}`,
1510
+ thought: `[GU\xCDA DE \xC9XITO] Reutilizando paso maestro aprendido: ${actionData.action} en ${foundComponent.name}`,
1273
1511
  actions: [{
1274
- ...memStep.action_data,
1512
+ ...actionData,
1275
1513
  idx: foundComponent.idx,
1276
1514
  description: foundComponent.name,
1277
1515
  selector: foundComponent.selector,
@@ -1476,12 +1714,14 @@ Every turn, you MUST follow these 4 phases:
1476
1714
  ## Phase 3: ACT
1477
1715
  - Execute your planned actions using \`perform_ui_actions\`.
1478
1716
  - Group multiple related actions in one call (e.g., click+fill for 3 fields = 6 actions in one call).
1479
- - For dropdowns/selects (MANDATORY PROTOCOL \u2014 complete in 2 turns MAX):
1480
- 1. **Turn A**: Click the dropdown trigger to open it, then WAIT to see options.
1481
- 2. **Turn B**: Click the desired option AND immediately click the next logical button (e.g., \`Siguiente\`, \`Guardar\`) in the SAME action call. DO NOT wait another turn just to verify.
1482
- - If the field still shows a validation error after you clicked Siguiente: use \`inspect_element_details\` on the field ONCE. If selector works, try clicking the field and pressing Tab. Then click Siguiente again.
1483
- - **STRICTLY FORBIDDEN**: Sending Escape repeatedly without clicking Siguiente. If you sent Escape once and the dropdown is still visible, click OUTSIDE the dropdown or click Siguiente directly \u2014 the form validation will confirm the selection.
1717
+ - For comboboxes and dropdowns (CRITICAL PROTOCOL):
1718
+ - **STEP 1 (MANDATORY)**: Click the dropdown/combobox trigger to OPEN it. Wait for the list to appear.
1719
+ - **STEP 2 (MANDATORY, SAME or NEXT turn)**: Click the desired option directly (\`[li]\` or \`[option]\`). Do NOT use \`fill\` to type text.
1720
+ - **STEP 3**: If more fields remain in the same form section, do NOT group them in the same turn. Wait for re-render first.
1721
+ - **NEVER** insert a \`wait\` action between clicking the trigger and clicking the option \u2014 complete both in one turn.
1722
+ - **STRICTLY FORBIDDEN**: Sending Escape repeatedly. After selecting an option, click the NEXT logical button directly.
1484
1723
  - For menus: Click menu icon \u2192 WAIT for next turn \u2192 Click option.
1724
+ - For \`<input type="date">\` native fields: NEVER use \`fill\`. ALWAYS use \`interact_native_control\` with value in YYYY-MM-DD format. Do this ONCE per field. If it fails, do NOT retry \u2014 move on and click Siguiente to see the validation message.
1485
1725
 
1486
1726
  ## Phase 4: VERIFY
1487
1727
  - After critical actions (SAVE/CREATE/DELETE), verify the result:
@@ -1513,10 +1753,11 @@ These are NOT optional. You MUST use these tools in these situations:
1513
1753
  # CRITICAL RULES
1514
1754
  1. **NEVER BLIND-RETRY**: If an action fails, you MUST investigate with \`inspect_element_details\` before retrying. Repeating the exact same failed action is FORBIDDEN.
1515
1755
  2. **NO REPEAT ACTIONS**: Check HISTORY. If you already filled a field or clicked a button, do NOT do it again unless the UI clearly reset OR the button belongs to a different step/modal/section mentioned in the mission (e.g., Save in modal vs Save in main form).
1516
- 3. **GROUP ACTIONS**: You MUST group non-conflicting actions in a single turn to save time and tokens. You can \`fill\` multiple text inputs in the SAME turn.
1517
- - NEVER use \`click\` on a text field before using \`fill\`. The \`fill\` action automatically focuses the field.
1518
- - Example: \`fill\` Nombre, \`fill\` Fecha, \`fill\` Descripci\xF3n all in one single turn.
1519
- - BUT keep dropdown interactions isolated: Turn A = \`click\` dropdown. Turn B = \`click\` option.
1756
+ 3. **GROUP ACTIONS CAREFULLY**: You MUST group non-conflicting actions in a single turn to save time and tokens.
1757
+ - You can \`fill\` multiple plain text inputs in the SAME turn (Nombre, Descripci\xF3n, etc.).
1758
+ - You CANNOT mix dropdown selection with filling other fields in the same turn (React re-renders invalidate idx).
1759
+ - For \`<input type="date">\` fields: use \`interact_native_control\` ONCE per field \u2014 do NOT retry with \`fill\` or \`click\`.
1760
+ - NEVER insert a \`wait\` action between related dropdown steps (click trigger \u2192 click option). It breaks the sequence.
1520
1761
 
1521
1762
  3. **CURRENT STATE > MEMORY**: What you see in CURRENT COMPONENTS is the truth. SUCCESS MEMORY is a guide, not gospel.
1522
1763
  4. **PAGE TRANSITIONS**: If URL contains '/edit' or you see a 'Save' button, you ARE in the edit view. Don't go back.
@@ -1536,6 +1777,7 @@ These are NOT optional. You MUST use these tools in these situations:
1536
1777
  5. If you cannot determine which field is the duplicate, use \`capture_console_errors\` to get hints from the application.
1537
1778
  7. **DATA UNIQUENESS**: When generating test data, ALWAYS include a unique suffix (timestamp, counter). Never use "Test123" or "Prueba" alone.
1538
1779
  8. **FINISH CORRECTLY**: Use \`finish: true\` ONLY when the mission objective is FULLY achieved.
1780
+ - **MANDATORY FORMS RULE**: If your mission involves creating, editing, or configuring something, YOU MUST CLICK THE FINAL SUBMIT/SAVE BUTTON ("Guardar", "Siguiente", "Finalizar") AND VERIFY THE SUCCESS MESSAGE BEFORE emitting \`finish: true\`. Typing the last field is NOT finishing the mission! Do NOT finish prematurely.
1539
1781
  - **BEWARE**: If you see a record in a table, confirm it is the one you JUST created (e.g., check timestamp or log message). Do NOT assume an existing record is your success.
1540
1782
  - **MESSAGES**: Be suspicious of success messages that appear right after login or page load. Real success messages usually appear IMMEDIATELY after you click SAVE.
1541
1783
  9. **LARGE COMPONENTS**: If a component name is >50 characters, it's probably a CONTAINER, not a field. Do not interact with it.
@@ -1569,7 +1811,21 @@ When a form is divided into numbered steps (e.g., Step 1 \u2192 Step 2 \u2192 St
1569
1811
 
1570
1812
  4. **STEP VERIFICATION**: Before clicking Next/Save on any step, VERIFY that date fields have future-or-today dates. Use \`validate_element_state\` with 'has_value' to confirm.
1571
1813
 
1572
- 5. **FINAL PREVIEW STEP**: If you reach a final step like "Previsualizaci\xF3n", "Resumen", or see a "Terminar" / "Finalizar" button, your objective is ALMOST COMPLETE. Do NOT go back to previous steps just because you see read-only data. Click the final submit button ("Terminar", "Guardar", "Finalizar") to complete the mission.`
1814
+ 5. **FINAL PREVIEW STEP**: If you reach a final step like "Previsualizaci\xF3n", "Resumen", or see a "Terminar" / "Finalizar" button, your objective is ALMOST COMPLETE. Do NOT go back to previous steps just because you see read-only data. Click the final submit button ("Terminar", "Guardar", "Finalizar") to complete the mission.
1815
+
1816
+ 6. **DATE FIELD \u2014 ONE SHOT RULE**: For any \`<input type="date">\`, you get EXACTLY ONE attempt with \`interact_native_control\`. If it does not stick (field shows empty on next perception), do NOT retry the date. Instead: a) use \`send_keyboard_event\` to type the date digit by digit (e.g., "0", "5", "0", "5", "2", "0", "2", "6"), OR b) click \`Siguiente\` and see if ZOD validates it. If ZOD rejects it, THEN and ONLY THEN come back to fix. Looping 3+ times on the same date field is FORBIDDEN.
1817
+
1818
+ 7. **MODAL REWARD COMBOBOX**: In the rewards modal ("Configurar Recompensas del Juego"), the "Tipo de recompensa" dropdown is a \`[div]\` trigger, NOT an \`[input]\`. Always use 2 turns: Turn A = Click the trigger div to open the option list. Turn B = Click the specific option (e.g., \`[li] Puntos\`, \`[li] Cup\xF3n\`). Do NOT use \`fill\` on this field.
1819
+
1820
+ 8. **MODAL SELECTION PROTOCOL (CRITICAL)**: In selection modals (e.g., "Selecciona cup\xF3n"), you MUST select an item BEFORE clicking "Seleccionar" or "Confirmar". To select an item, click on ANY visible element within the item's row: the radio button, the item's name, OR its status label (e.g., the text "Activo" next to a coupon name). Clicking the confirm button without a prior selection WILL FAIL. If you do NOT see a radio button IDX in the component list, click the item's STATUS text (like "Activo") or its NAME text instead \u2014 this is the EXPECTED behavior for custom UI components.
1821
+
1822
+ 9. **RADIO BUTTON FALLBACK**: If you cannot find a dedicated radio button element for a list item, do NOT loop. Instead: a) Click the item's visible text (name, status, or any text in its row). b) If that doesn't work, try clicking the row container element. c) NEVER use the search box to "select" an item \u2014 the search box only filters the list, it does NOT select.
1823
+
1824
+ 10. **DROPDOWN TOGGLE WARNING (CRITICAL)**: Clicking a dropdown/combobox trigger TOGGLES it: 1st click = open, 2nd click = CLOSE, 3rd click = open again. If you clicked a dropdown trigger and do NOT see options in the next turn, do NOT click it again \u2014 you will just close it! Instead, use \`send_keyboard_event\` with "ArrowDown" to open/navigate the dropdown, then "Enter" to select.
1825
+
1826
+ 11. **COMBOBOX KEYBOARD FALLBACK**: If you click a combobox/select field and the dropdown options (\`[li]\`, \`[OPTION]\`) are NOT visible after 1 turn, use this keyboard sequence: a) Click the combobox field ONCE. b) Use \`send_keyboard_event\` with key "ArrowDown" to open the options and highlight the first one. c) Use \`send_keyboard_event\` with key "Enter" to select it. This avoids the toggle problem and works with all dropdown implementations.
1827
+
1828
+ 12. **ERROR STOP PROTOCOL (CRITICAL)**: If you see a message starting with \`\u{1F6D1} [ERROR_CR\xCDTICO]\` or \`\u{1F6D1} ERROR POST-GUARDADO\`, do NOT assume it is a transient error. You MUST analyze the text. If it says "ya existe" or "duplicado", you MUST change the offending field value (e.g., add a random number) BEFORE retrying. If you retry without changing any data and the error persists, you MUST use \`report_inability_to_proceed\` immediately. Looping on the same error toast is FORBIDDEN.`
1573
1829
  }
1574
1830
  ];
1575
1831
  if (this.testInfo) {
@@ -1643,7 +1899,7 @@ ${history.slice(-25).join("\n") || "None"}`
1643
1899
  headers["anthropic-version"] = "2023-06-01";
1644
1900
  }
1645
1901
  const controller = new AbortController();
1646
- const timeoutId = setTimeout(() => controller.abort(), 9e4);
1902
+ const timeoutId = setTimeout(() => controller.abort(), 18e4);
1647
1903
  try {
1648
1904
  response = await fetch(endpointUrl, {
1649
1905
  method: "POST",
@@ -1665,7 +1921,7 @@ ${history.slice(-25).join("\n") || "None"}`
1665
1921
  } catch (fetchErr) {
1666
1922
  clearTimeout(timeoutId);
1667
1923
  if (fetchErr.name === "AbortError") {
1668
- throw new Error(`Arcality Brain Timeout: No response from ${endpointUrl} after 90s`);
1924
+ throw new Error(`Arcality Brain Timeout: No response from ${endpointUrl} after 180s`);
1669
1925
  }
1670
1926
  throw new Error(`Arcality Brain Network Error: ${fetchErr.message}`);
1671
1927
  }
@@ -2264,7 +2520,7 @@ ${report}` });
2264
2520
  this.lastScreenshot = base64Img;
2265
2521
  return {
2266
2522
  thought: input.thought,
2267
- actions: input.actions.map((a) => {
2523
+ actions: (input.actions || []).map((a) => {
2268
2524
  const c = state.components.find((comp) => comp.idx === a.idx);
2269
2525
  let finalValue = a.value;
2270
2526
  if (a.action === "fill" && finalValue && /^\d{2}\/\d{2}\/\d{4}$/.test(finalValue)) {
@@ -2323,13 +2579,8 @@ ${history.join("\n")}` }
2323
2579
  }
2324
2580
  }
2325
2581
  /**
2326
- * MODO BRIDGE: Envía la percepción de la CLI hacia el Portal Web (SaaS)
2327
- * para que el servidor decida la acción usando su propia infraestructura e IP.
2328
- // askBridge eliminado — proxy mode ahora está integrado directamente en askIA.
2329
-
2330
- /**
2331
- * QA Skill: Summarizes the mission execution into a clean, human-friendly English YAML format.
2332
- */
2582
+ * QA Skill: Summarizes the mission execution into a clean, human-friendly English YAML format.
2583
+ */
2333
2584
  async summarizeMissionToYaml(prompt, history, startUrl) {
2334
2585
  if (!process.env.ANTHROPIC_API_KEY)
2335
2586
  return null;
@@ -2494,6 +2745,7 @@ var SecurityScanner = class {
2494
2745
  var import_config = require("dotenv/config");
2495
2746
  var fs2 = __toESM(require("fs"));
2496
2747
  var path2 = __toESM(require("path"));
2748
+ var crypto2 = __toESM(require("crypto"));
2497
2749
  var _sessionRuleCache = /* @__PURE__ */ new Set();
2498
2750
  function captureValidationRule(errorMessage, context) {
2499
2751
  const key = errorMessage.substring(0, 80).toLowerCase().trim();
@@ -2520,22 +2772,33 @@ function captureValidationRule(errorMessage, context) {
2520
2772
  process.env.LOGIN_PASSWORD = process.env[`${activeConfig}_PASS`];
2521
2773
  await page.context().grantPermissions(["geolocation"]);
2522
2774
  await page.context().setGeolocation({ latitude: 19.4326, longitude: -99.1332 });
2523
- const agent = new AIAgentHelper(page, contextDir, testInfo);
2524
- const prompt = process.env.SMART_PROMPT || "Navega al inicio y verifica que el sitio cargue";
2525
2775
  const history = [];
2526
2776
  const urlHistory = [];
2527
2777
  const stepsData = [];
2778
+ const memoryFile = path2.join(contextDir, "memoria-agente.json");
2779
+ if (fs2.existsSync(memoryFile)) {
2780
+ try {
2781
+ fs2.unlinkSync(memoryFile);
2782
+ } catch {
2783
+ }
2784
+ }
2785
+ const agent = new AIAgentHelper(page, contextDir, testInfo);
2786
+ const prompt = process.env.SMART_PROMPT || "Navega al inicio y verifica que el sitio cargue";
2528
2787
  const successKeywords = /exitosamente|creado correctamente|guardado correctamente|misión cumplida|registered successfully|created successfully|saved successfully|operación exitosa|registro guardado|successfully|correctly/i;
2529
2788
  const failureKeywords = /\berror\b|falló|failed|no se puede|cannot|unable|ya existe|already exists|existente|exist|inválido|invalid|incorrecto|incorrect|ligado|linked|depende|depends|duplicado|duplicate|repetido|repeated|conflict|reintentar|retry|missing|required|obligatorio|bad request|400|500/i;
2789
+ const systemErrorKeywords = /error interno|internal error|intentarlo más tarde|try again later|inténtelo más tarde|servicio no disponible|service unavailable|something went wrong|algo salió mal|ha ocurrido un error inesperado|unexpected error|server error|no se pudo procesar|could not be processed|comuníquese con soporte|contact support/i;
2530
2790
  let aiMarkedSuccess = false;
2531
2791
  let hasCriticalError = false;
2792
+ let failReason = "";
2532
2793
  let resultsSaved = false;
2533
2794
  let isFinished = false;
2534
2795
  let stepCount = 0;
2535
2796
  let guideStepCount = 0;
2797
+ let guideIdx = 0;
2536
2798
  let stepsDataBackup = [];
2537
- const maxSteps = 30;
2799
+ const maxSteps = 50;
2538
2800
  let lastSeenSuccessToast = "";
2801
+ let lastSeenErrorToast = "";
2539
2802
  let accumulatedCost = 0;
2540
2803
  let totalMissionUsage = { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 };
2541
2804
  const saveMissionResults = () => {
@@ -2546,10 +2809,10 @@ function captureValidationRule(errorMessage, context) {
2546
2809
  fs2.mkdirSync(contextDir, { recursive: true });
2547
2810
  const finalSuccess = aiMarkedSuccess && !hasCriticalError;
2548
2811
  try {
2549
- const memoryFile = path2.join(contextDir, "memoria-agente.json");
2812
+ const memoryFile2 = path2.join(contextDir, "memoria-agente.json");
2550
2813
  let memories = [];
2551
- if (fs2.existsSync(memoryFile)) {
2552
- const content = fs2.readFileSync(memoryFile, "utf8").trim();
2814
+ if (fs2.existsSync(memoryFile2)) {
2815
+ const content = fs2.readFileSync(memoryFile2, "utf8").trim();
2553
2816
  if (content) {
2554
2817
  try {
2555
2818
  memories = JSON.parse(content);
@@ -2559,24 +2822,77 @@ function captureValidationRule(errorMessage, context) {
2559
2822
  }
2560
2823
  }
2561
2824
  }
2562
- const effectiveLocalSteps = stepsData.length > 0 ? stepsData : stepsDataBackup;
2825
+ const rawSteps = stepsData.length > 0 ? stepsData : stepsDataBackup;
2826
+ const cleanSteps = rawSteps.filter((step, idx) => {
2827
+ const name = (step.componentName || "").toLowerCase();
2828
+ const stepUrl = (step.url || "").toLowerCase();
2829
+ if (stepUrl.includes("/login") || name.includes("password") || name.includes("contrase\xF1a") || name.includes("iniciar sesi\xF3n")) {
2830
+ return false;
2831
+ }
2832
+ if (step.action_data?.action === "wait")
2833
+ return false;
2834
+ if (name.includes("cancelar") && idx > 0) {
2835
+ const prev = (rawSteps[idx - 1]?.componentName || "").toLowerCase();
2836
+ if (prev.includes("siguiente") || prev.includes("next") || prev.includes("guardar"))
2837
+ return false;
2838
+ }
2839
+ if (name.includes("agregar recompensas") && idx > 0) {
2840
+ const prev = (rawSteps[idx - 1]?.componentName || "").toLowerCase();
2841
+ if (prev.includes("siguiente") || prev.includes("next"))
2842
+ return false;
2843
+ }
2844
+ if (idx > 0) {
2845
+ const prev = rawSteps[idx - 1];
2846
+ const prevName = (prev?.componentName || "").toLowerCase();
2847
+ if (prevName === name && name !== "") {
2848
+ const currAction = step.action_data?.action;
2849
+ const prevAction = prev.action_data?.action;
2850
+ if (prevAction === "interact_native_control")
2851
+ return false;
2852
+ if (prevAction === "fill" && currAction === "click")
2853
+ return false;
2854
+ if (prevAction === "click" && currAction === "click")
2855
+ return false;
2856
+ }
2857
+ }
2858
+ if (step.action_data?.action === "click" && idx < rawSteps.length - 1) {
2859
+ const next = rawSteps[idx + 1];
2860
+ if (next.action_data?.action === "fill" && next.componentName === step.componentName) {
2861
+ return false;
2862
+ }
2863
+ }
2864
+ if ((step.action_data?.action === "fill" || step.action_data?.action === "interact_native_control" || step.action_data?.action === "click") && (name.includes("fecha") || name.includes("date"))) {
2865
+ for (let j = idx + 1; j < rawSteps.length; j++) {
2866
+ const futureStep = rawSteps[j];
2867
+ const futureName = (futureStep.componentName || "").toLowerCase();
2868
+ if (futureName.includes("siguiente") || futureName.includes("next") || futureName.includes("guardar") || futureName.includes("save")) {
2869
+ break;
2870
+ }
2871
+ if (futureName === name && (futureStep.action_data?.action === "fill" || futureStep.action_data?.action === "interact_native_control")) {
2872
+ return false;
2873
+ }
2874
+ }
2875
+ }
2876
+ return true;
2877
+ });
2563
2878
  const missionData = {
2564
2879
  prompt,
2565
2880
  steps: history,
2566
- steps_data: effectiveLocalSteps,
2881
+ steps_data: cleanSteps,
2567
2882
  success: finalSuccess,
2568
2883
  error: hasCriticalError,
2884
+ fail_reason: failReason,
2569
2885
  timestamp: Date.now()
2570
2886
  };
2571
2887
  memories.push(missionData);
2572
- fs2.writeFileSync(memoryFile, JSON.stringify(memories, null, 2));
2573
- console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria guardada: ${effectiveLocalSteps.length} pasos nuevos`);
2888
+ fs2.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
2889
+ console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria guardada: ${cleanSteps.length} pasos limpios (${rawSteps.length - cleanSteps.length} corruptos filtrados)`);
2574
2890
  } catch (err) {
2575
2891
  console.error("\u274C Fall\xF3 al guardar memoria:", err);
2576
2892
  }
2577
2893
  try {
2578
2894
  const logPath = path2.join(contextDir, `agent-log-${Date.now()}.json`);
2579
- fs2.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError, usage: totalMissionUsage, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
2895
+ fs2.writeFileSync(logPath, JSON.stringify({ prompt, history, steps: stepCount, success: finalSuccess, error: hasCriticalError, fail_reason: failReason, usage: totalMissionUsage, timestamp: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
2580
2896
  try {
2581
2897
  fs2.appendFileSync(path2.join(contextDir, "arcality-history.log"), JSON.stringify({ ts: (/* @__PURE__ */ new Date()).toISOString(), mission: prompt.substring(0, 150), success: finalSuccess, steps: stepCount, usage: totalMissionUsage }) + "\n");
2582
2898
  } catch (e) {
@@ -2600,7 +2916,15 @@ function captureValidationRule(errorMessage, context) {
2600
2916
  if (!isFailed && stepsData.length === 0 && stepsDataBackup.length === 0) {
2601
2917
  console.warn(`[CollectiveMemory] \u26A0\uFE0F stepsData vac\xEDo en path de \xE9xito \u2014 intentando usar backup...`);
2602
2918
  }
2603
- const effectiveSteps = stepsData.length > 0 ? stepsData : stepsDataBackup;
2919
+ const baseSteps = stepsData.length > 0 ? stepsData : stepsDataBackup;
2920
+ const effectiveSteps = baseSteps.filter((step) => {
2921
+ const name = (step.componentName || "").toLowerCase();
2922
+ const stepUrl = (step.url || "").toLowerCase();
2923
+ if (stepUrl.includes("/login") || name.includes("password") || name.includes("contrase\xF1a") || name.includes("iniciar sesi\xF3n")) {
2924
+ return false;
2925
+ }
2926
+ return true;
2927
+ });
2604
2928
  console.log(`
2605
2929
  ======================================================`);
2606
2930
  console.log(`\u{1F9E0} [MEMORIA COLECTIVA - INGESTA DE APRENDIZAJE]`);
@@ -2671,10 +2995,10 @@ function captureValidationRule(errorMessage, context) {
2671
2995
  throw new Error(`URL Inv\xE1lida: "${loginUrl}". Aseg\xFArate de configurar la Base URL con http://`);
2672
2996
  await page.goto(loginUrl);
2673
2997
  try {
2674
- const userInp = page.locator('input[type="email"], input[name="email"], input[name="username"], [placeholder*="usuario" i], [placeholder*="correo" i], [placeholder*="email" i]').first();
2998
+ const userInp = page.locator('input[id="username"], input[name="username"], input[type="email"], input[name="email"], [placeholder*="usuario" i], [placeholder*="correo" i], [placeholder*="email" i]').first();
2675
2999
  await userInp.waitFor({ state: "visible", timeout: 1e4 });
2676
- const passInp = page.locator('input[type="password"], input[name="password"], [placeholder*="contrase\xF1a" i]').first();
2677
- const subBtn = page.locator('button[type="submit"], button:has-text("Login"), button:has-text("Entrar"), [role="button"]:has-text("Entrar")').first();
3000
+ const passInp = page.locator('input[id="password"], input[name="password"], input[type="password"], [placeholder*="contrase\xF1a" i]').first();
3001
+ const subBtn = page.locator('button[type="submit"], button:has-text("Login"), button:has-text("Entrar"), [role="button"]:has-text("Entrar"), button:has-text("ENTRAR")').first();
2678
3002
  await userInp.click();
2679
3003
  await userInp.fill(process.env.LOGIN_USER || "");
2680
3004
  await passInp.click();
@@ -2707,6 +3031,90 @@ function captureValidationRule(errorMessage, context) {
2707
3031
  isFinished = true;
2708
3032
  saveMissionResults();
2709
3033
  });
3034
+ let reusedPatternId = null;
3035
+ let usedDirectReuse = false;
3036
+ let patternContext = "";
3037
+ try {
3038
+ const patterns = await searchPromptPattern(prompt, 5);
3039
+ if (patterns && patterns.length > 0) {
3040
+ const bestMatch = patterns[0];
3041
+ const normalize = (txt) => {
3042
+ return txt.toLowerCase().replace(/ar\b|er\b|ir\b/g, "").split(/\s+/).filter((w) => w.length > 2 && !["una", "uno", "las", "los", "del", "con", "para", "nueva", "nuevo"].includes(w));
3043
+ };
3044
+ const promptKeywords = normalize(prompt);
3045
+ const patternKeywords = normalize(bestMatch.prompt || bestMatch.original_prompt || bestMatch.originalPrompt || bestMatch.name || "");
3046
+ const matches = promptKeywords.filter((kw) => patternKeywords.some((pkw) => pkw.includes(kw) || kw.includes(pkw))).length;
3047
+ const matchRatio = matches / Math.max(promptKeywords.length, 1);
3048
+ if (matchRatio >= 0.85) {
3049
+ console.log(`>>ARCALITY_STATUS>> \u{1F9E0} Memoria Colectiva detectada (id: ${bestMatch.id}). Descargando a memoria local...`);
3050
+ reusedPatternId = bestMatch.id || "unknown";
3051
+ let patternJsonObj = {};
3052
+ try {
3053
+ const rawPj = bestMatch.pattern_json || bestMatch.patternJson;
3054
+ patternJsonObj = typeof rawPj === "string" ? JSON.parse(rawPj) : rawPj || {};
3055
+ } catch {
3056
+ patternJsonObj = {};
3057
+ }
3058
+ const rawStepsTechnical = patternJsonObj.steps_technical || patternJsonObj.stepsTechnical;
3059
+ const resolvedStepsData = Array.isArray(rawStepsTechnical) ? rawStepsTechnical : [];
3060
+ if (resolvedStepsData.length === 0) {
3061
+ console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Patr\xF3n descargado sin steps_technical v\xE1lidos. Modo Gu\xEDa no ser\xE1 activado.`);
3062
+ }
3063
+ const localMemoryData = {
3064
+ prompt,
3065
+ steps: patternJsonObj.history || [],
3066
+ steps_data: resolvedStepsData,
3067
+ success: true,
3068
+ error: false,
3069
+ timestamp: Date.now()
3070
+ };
3071
+ try {
3072
+ const memoryFile2 = path2.join(contextDir, "memoria-agente.json");
3073
+ let memories = [];
3074
+ if (fs2.existsSync(memoryFile2)) {
3075
+ try {
3076
+ memories = JSON.parse(fs2.readFileSync(memoryFile2, "utf8"));
3077
+ } catch {
3078
+ memories = [];
3079
+ }
3080
+ }
3081
+ const normalizeForDedup = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
3082
+ const normalizedCurrentPrompt = normalizeForDedup(prompt);
3083
+ const alreadyExists = memories.some((m) => normalizeForDedup(m.prompt || "") === normalizedCurrentPrompt);
3084
+ if (!alreadyExists) {
3085
+ if (resolvedStepsData.length > 0) {
3086
+ memories.push(localMemoryData);
3087
+ fs2.writeFileSync(memoryFile2, JSON.stringify(memories, null, 2));
3088
+ console.log(`>>ARCALITY_STATUS>> \u2728 Memoria sincronizada localmente (${resolvedStepsData.length} pasos). El Agente entrar\xE1 en modo GU\xCDA.`);
3089
+ } else {
3090
+ console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Memoria NO sincronizada: el patr\xF3n del backend tiene 0 pasos t\xE9cnicos v\xE1lidos.`);
3091
+ }
3092
+ } else {
3093
+ console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F Patr\xF3n ya existe en memoria local. Modo GU\xCDA activado sin re-escritura.`);
3094
+ }
3095
+ } catch (err) {
3096
+ console.warn(`Error al sincronizar memoria local: ${err}`);
3097
+ }
3098
+ } else if (matchRatio >= 0.5) {
3099
+ let patternJsonObj = {};
3100
+ try {
3101
+ patternJsonObj = typeof bestMatch.pattern_json === "string" ? JSON.parse(bestMatch.pattern_json) : bestMatch.pattern_json || bestMatch.patternJson || {};
3102
+ } catch {
3103
+ patternJsonObj = {};
3104
+ }
3105
+ console.log(`>>ARCALITY_STATUS>> \u{1F916} Similitud parcial detectada (text_match_found: true). LLM ser\xE1 llamado con contexto del patr\xF3n (llm_called: true)`);
3106
+ const readableHistory = patternJsonObj.history?.slice(0, 10).map((h) => `- ${h}`).join("\n") || "";
3107
+ patternContext = `Existe un patr\xF3n similar previamente aprobado que logr\xF3 el \xE9xito. \xDAsalo como referencia para tu estrategia:
3108
+ ${readableHistory}`;
3109
+ } else {
3110
+ console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F Patr\xF3n de BD descartado por baja similitud (${matchRatio.toFixed(2)}).`);
3111
+ }
3112
+ } else {
3113
+ console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F No se encontraron patrones previos (no_match: true).`);
3114
+ }
3115
+ } catch (e) {
3116
+ console.warn(`Error buscando patrones: ${e.message}`);
3117
+ }
2710
3118
  const maxTotalIterations = maxSteps + 50;
2711
3119
  let totalIterations = 0;
2712
3120
  while (!isFinished && stepCount < maxSteps) {
@@ -2714,10 +3122,22 @@ function captureValidationRule(errorMessage, context) {
2714
3122
  if (totalIterations > maxTotalIterations) {
2715
3123
  console.error(`>>ARCALITY_STATUS>> \u{1F6D1} Safety limit: ${totalIterations} iteraciones totales alcanzadas. Abortando.`);
2716
3124
  hasCriticalError = true;
3125
+ failReason = "safety_limit_iterations";
2717
3126
  break;
2718
3127
  }
2719
3128
  let containsError = false;
2720
- await page.waitForTimeout(1e3);
3129
+ if (page.isClosed()) {
3130
+ console.error(`
3131
+ \u{1F6D1} [PAGE CRASH] La pesta\xF1a del navegador fue cerrada inesperadamente (inicio de ciclo).`);
3132
+ console.error(` >> Causa probable: El portal caus\xF3 una navegaci\xF3n forzada, crash de tab, o redirigi\xF3 fuera del contexto.`);
3133
+ hasCriticalError = true;
3134
+ failReason = "portal_page_crash";
3135
+ isFinished = true;
3136
+ saveMissionResults();
3137
+ break;
3138
+ }
3139
+ await page.waitForTimeout(1e3).catch(() => {
3140
+ });
2721
3141
  const currentUrl = page.url();
2722
3142
  const historyStr = history.join(" ").toLowerCase();
2723
3143
  const hadSubmitAction = historyStr.includes("guardar") || historyStr.includes("save") || historyStr.includes("registrar");
@@ -2748,6 +3168,49 @@ function captureValidationRule(errorMessage, context) {
2748
3168
  }
2749
3169
  }
2750
3170
  }
3171
+ const isReviewStepVisible = await page.locator('text="Previsualizaci\xF3n"').isVisible().catch(() => false) || await page.locator('text="Resumen"').isVisible().catch(() => false) || await page.locator('text="Confirmaci\xF3n"').isVisible().catch(() => false);
3172
+ const looksLikeFinalStep = isReviewStepVisible && stepCount >= 10;
3173
+ if (looksLikeFinalStep && !isFinished) {
3174
+ const finalSaveBtn = page.locator('button:has-text("Guardar"), button:has-text("Finalizar"), button:has-text("Terminar"), button:has-text("Crear")');
3175
+ const btnCount = await finalSaveBtn.count();
3176
+ if (btnCount > 0) {
3177
+ const firstBtn = finalSaveBtn.first();
3178
+ if (await firstBtn.isVisible().catch(() => false) && await firstBtn.isEnabled().catch(() => false)) {
3179
+ const btnText = await firstBtn.innerText().catch(() => "");
3180
+ const isModalOpen = await page.locator('[role="dialog"], .modal, .mat-dialog-container').first().isVisible().catch(() => false);
3181
+ if (!isModalOpen) {
3182
+ console.log(`
3183
+ \u2728 [PROACTIVE FINISH] Bot\xF3n final detectado: "${btnText.trim()}". Ejecutando sin turno IA para evitar timeout.`);
3184
+ history.push(`Turno ${stepCount}: [AUTO] click en "[button] ${btnText.trim()}" (Detecci\xF3n proactiva del paso final)`);
3185
+ await firstBtn.click({ timeout: 5e3 }).catch(() => {
3186
+ });
3187
+ await page.waitForTimeout(3e3);
3188
+ const postSaveFb = await page.locator('.toast, .alert, [role="status"], [role="alert"], .snackbar, .notification, .mat-snack-bar-container').all();
3189
+ let proactiveSuccess = false;
3190
+ for (const fb of postSaveFb) {
3191
+ if (await fb.isVisible().catch(() => false)) {
3192
+ const fbTxt = await fb.innerText().catch(() => "");
3193
+ if (successKeywords.test(fbTxt)) {
3194
+ console.log(`>>ARCALITY_STATUS>> \u2728 [PROACTIVE FINISH] \xC9xito confirmado por toast: "${fbTxt.substring(0, 60)}"`);
3195
+ proactiveSuccess = true;
3196
+ break;
3197
+ }
3198
+ }
3199
+ }
3200
+ const postSaveUrl = page.url();
3201
+ const urlChanged = postSaveUrl !== currentUrl && !postSaveUrl.includes("/new") && !postSaveUrl.includes("/create");
3202
+ if (proactiveSuccess || urlChanged) {
3203
+ aiMarkedSuccess = true;
3204
+ isFinished = true;
3205
+ hasCriticalError = false;
3206
+ await persistCollectiveMemory();
3207
+ saveMissionResults();
3208
+ break;
3209
+ }
3210
+ }
3211
+ }
3212
+ }
3213
+ }
2751
3214
  if (stepCount === 1) {
2752
3215
  const initialToasts = await page.locator('.toast, .alert, [role="status"], [role="alert"]').all();
2753
3216
  for (const t of initialToasts) {
@@ -2758,6 +3221,49 @@ function captureValidationRule(errorMessage, context) {
2758
3221
  }
2759
3222
  }
2760
3223
  }
3224
+ let networkError = agent.criticalNetworkError;
3225
+ if (networkError) {
3226
+ console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Error interno del Portal/Red detectado: "${networkError}"`);
3227
+ hasCriticalError = true;
3228
+ failReason = "portal_network_error";
3229
+ isFinished = true;
3230
+ history.push(`Turno ${stepCount}: \u{1F6D1} ERROR CR\xCDTICO DE SISTEMA/RED: ${networkError}. El portal fall\xF3 internamente al ejecutar una petici\xF3n XHR/Fetch.`);
3231
+ saveMissionResults();
3232
+ break;
3233
+ }
3234
+ let jsError = agent.criticalJsError;
3235
+ if (jsError) {
3236
+ console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Error Fatal de JavaScript detectado: "${jsError}"`);
3237
+ hasCriticalError = true;
3238
+ failReason = "portal_js_crash";
3239
+ isFinished = true;
3240
+ history.push(`Turno ${stepCount}: \u{1F6D1} ERROR CR\xCDTICO DE APLICACI\xD3N: ${jsError}. El portal sufri\xF3 una excepci\xF3n no manejada y dej\xF3 de responder.`);
3241
+ saveMissionResults();
3242
+ break;
3243
+ }
3244
+ try {
3245
+ const systemToastEls = await page.locator('.toast, .alert, [role="alert"], [role="status"], .snackbar, .notification, .error-message, .mat-snack-bar-container').all();
3246
+ for (const el of systemToastEls) {
3247
+ if (await el.isVisible().catch(() => false)) {
3248
+ const sysErrTxt = await el.innerText().catch(() => "");
3249
+ if (sysErrTxt && systemErrorKeywords.test(sysErrTxt)) {
3250
+ console.error(`
3251
+ \u{1F6D1} [SYSTEM ERROR DETECTADO] El portal report\xF3 un error de sistema no recuperable en UI:`);
3252
+ console.error(` Mensaje: "${sysErrTxt.trim().substring(0, 150)}"`);
3253
+ console.error(` >> Tipo: portal_internal_error | El agente NO puede corregir errores del servidor.`);
3254
+ hasCriticalError = true;
3255
+ failReason = "portal_internal_error";
3256
+ isFinished = true;
3257
+ history.push(`Turno ${stepCount}: \u{1F6D1} ERROR SIST\xC9MICO DEL PORTAL: "${sysErrTxt.trim().substring(0, 200)}". Este es un fallo del servidor, no un error de datos del agente. El portal devolvi\xF3 HTTP 200 pero report\xF3 un error interno en la UI.`);
3258
+ saveMissionResults();
3259
+ break;
3260
+ }
3261
+ }
3262
+ }
3263
+ } catch {
3264
+ }
3265
+ if (isFinished)
3266
+ break;
2761
3267
  let hasVisibleError = false;
2762
3268
  try {
2763
3269
  const errorEls = await page.locator('.toast, .alert, [role="alert"], [role="status"], .snackbar, .notification, .error-message').all();
@@ -2774,9 +3280,17 @@ function captureValidationRule(errorMessage, context) {
2774
3280
  }
2775
3281
  let response = null;
2776
3282
  if (!hasVisibleError) {
2777
- response = await agent.getActionFromGuia(prompt, stepsData.length);
3283
+ response = await agent.getActionFromGuia(prompt, guideIdx);
3284
+ if (!response) {
3285
+ await page.waitForTimeout(1e3);
3286
+ response = await agent.getActionFromGuia(prompt, guideIdx);
3287
+ if (response) {
3288
+ console.log(`>>ARCALITY_STATUS>> \u{1F504} Gu\xEDa recuper\xF3 el rastro tras esperar 1000ms.`);
3289
+ }
3290
+ }
2778
3291
  } else {
2779
3292
  console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Error real detectado en pantalla. Saltando Gu\xEDa.`);
3293
+ guideIdx = 999999;
2780
3294
  if (stepsData.length > 0) {
2781
3295
  stepsDataBackup = [...stepsData];
2782
3296
  stepsData.length = 0;
@@ -2804,13 +3318,15 @@ function captureValidationRule(errorMessage, context) {
2804
3318
  const isGuideRepeating = guideActionDesc && recentHistory.some((h) => h.includes(guideActionDesc.substring(0, 30)));
2805
3319
  if (isGuideRepeating) {
2806
3320
  console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Gu\xEDa invalidada: la acci\xF3n "${guideActionDesc.substring(0, 50)}" ya fue intentada. Delegando a IA.`);
3321
+ guideIdx++;
2807
3322
  response = null;
2808
3323
  } else {
2809
3324
  guideStepCount++;
3325
+ guideIdx++;
2810
3326
  console.log(`
2811
3327
  ======================================================`);
2812
3328
  console.log(`\u{1F680} [USANDO GU\xCDA DE \xC9XITO - MEMORIA COLECTIVA AHORRANDO TOKENS]`);
2813
- console.log(`>>arcality>> Paso validado (Gu\xEDa #${guideStepCount}, NO consume turno IA)`);
3329
+ console.log(`>>arcality>> Paso validado (Gu\xEDa #${guideStepCount}, guideIdx=${guideIdx}, NO consume turno IA)`);
2814
3330
  console.log(`======================================================
2815
3331
  `);
2816
3332
  }
@@ -2818,7 +3334,10 @@ function captureValidationRule(errorMessage, context) {
2818
3334
  if (!response) {
2819
3335
  stepCount++;
2820
3336
  console.log(`>>ARCALITY_STATUS>> \u23F3 Turno IA ${stepCount} de ${maxSteps} (Gu\xEDa us\xF3 ${guideStepCount} turnos gratis)...`);
2821
- response = await agent.askIA(prompt, history, stepCount);
3337
+ const effectivePrompt = patternContext ? `${prompt}
3338
+
3339
+ ${patternContext}` : prompt;
3340
+ response = await agent.askIA(effectivePrompt, history, stepCount);
2822
3341
  if (response.usage) {
2823
3342
  const inputs = response.usage.input_tokens || 0;
2824
3343
  const cacheCreates = response.usage.cache_creation_input_tokens || 0;
@@ -2839,6 +3358,7 @@ function captureValidationRule(errorMessage, context) {
2839
3358
  response.actions = [];
2840
3359
  aiMarkedSuccess = false;
2841
3360
  hasCriticalError = true;
3361
+ failReason = "cost_limit_reached";
2842
3362
  }
2843
3363
  }
2844
3364
  }
@@ -2849,27 +3369,58 @@ function captureValidationRule(errorMessage, context) {
2849
3369
  console.log(`======================================================
2850
3370
  `);
2851
3371
  if (!response.finish && history.length >= 6) {
2852
- const lastSixActions = history.slice(-6);
2853
- const clickActions = lastSixActions.filter((h) => h.includes("click en"));
2854
- const fillActions = lastSixActions.filter((h) => h.includes("fill en"));
3372
+ const lastActions = history.slice(-8);
3373
+ const clickActions = lastActions.filter((h) => h.includes("click en")).map((h) => h.replace(/^Turno \d+:\s*/, ""));
3374
+ const fillActions = lastActions.filter((h) => h.includes("fill en")).map((h) => h.replace(/^Turno \d+:\s*/, ""));
2855
3375
  const uniqueClicks = new Set(clickActions);
2856
3376
  const uniqueFills = new Set(fillActions);
2857
3377
  const isClickLoop = clickActions.length >= 4 && uniqueClicks.size <= 2;
2858
3378
  const isFillLoop = fillActions.length >= 4 && uniqueFills.size <= 2;
2859
- if (isClickLoop || isFillLoop) {
3379
+ const currentTurnActions = history.filter((h) => h.startsWith(`Turno ${stepCount}:`));
3380
+ const currentTurnHasFill = currentTurnActions.some((h) => h.includes("fill en"));
3381
+ const currentTurnHasNewTarget = currentTurnActions.some((h) => {
3382
+ const match = h.match(/(?:click|fill) en "(.+?)"/);
3383
+ return match && !uniqueClicks.has(`click en ${match[1]}`) && !uniqueClicks.has(`fill en ${match[1]}`);
3384
+ });
3385
+ const agentRecovered = currentTurnHasFill || currentTurnHasNewTarget;
3386
+ if ((isClickLoop || isFillLoop) && !agentRecovered) {
2860
3387
  const loopType = isClickLoop ? "click" : "fill";
2861
3388
  const uniqueTargets = isClickLoop ? uniqueClicks : uniqueFills;
2862
3389
  console.warn(`
2863
3390
  \u26A0\uFE0F [LOOP DETECTOR] Patr\xF3n repetitivo de ${loopType} detectado:`);
2864
- console.warn(` \xDAltimas acciones: ${lastSixActions.join(" \u2192 ")}`);
2865
- console.warn(` El agente est\xE1 atrapado. Forzando terminaci\xF3n con sugerencia.`);
2866
- response.thought = `\u{1F6D1} ERROR: Me he quedado atrapado en un bucle repetitivo intentando interactuar con: ${Array.from(uniqueTargets).join(", ")}. No puedo continuar la misi\xF3n de forma aut\xF3noma.
2867
-
2868
- \u{1F4A1} REGLA DE QA: He detectado que mi acci\xF3n de ${loopType} no est\xE1 cambiando el estado de la p\xE1gina como esperaba. Esto podr\xEDa ser un bug de la aplicaci\xF3n o una limitaci\xF3n de mi percepci\xF3n actual.`;
3391
+ console.warn(` \xDAltimas acciones: ${lastActions.join(" \u2192 ")}`);
3392
+ response.thought = `\u{1F6D1} ERROR: Bucle repetitivo en ${loopType} sobre: ${Array.from(uniqueTargets).join(", ")}.`;
2869
3393
  response.finish = true;
2870
3394
  response.actions = [];
2871
3395
  aiMarkedSuccess = false;
2872
3396
  hasCriticalError = true;
3397
+ failReason = "loop_detected";
3398
+ } else if ((isClickLoop || isFillLoop) && agentRecovered) {
3399
+ console.log(`>>ARCALITY_STATUS>> \u{1F504} Loop candidato detectado pero el agente se recuper\xF3 en este turno. Continuando misi\xF3n.`);
3400
+ }
3401
+ }
3402
+ if (!response.finish && history.length >= 3) {
3403
+ const lastSixHistory = history.slice(-6);
3404
+ const consecutiveWaits = lastSixHistory.filter((h) => h.includes('wait en "')).length;
3405
+ if (consecutiveWaits >= 3) {
3406
+ const blankEvidencePath = path2.join(contextDir, `portal-blank-state-${Date.now()}.png`);
3407
+ await page.screenshot({ path: blankEvidencePath, fullPage: true }).catch(() => {
3408
+ });
3409
+ const blankUrl = page.url();
3410
+ console.error(`
3411
+ \u{1F6D1} [PORTAL RENDERING FAILURE] El agente ejecut\xF3 ${consecutiveWaits} esperas consecutivas sin progreso.`);
3412
+ console.error(` >> URL afectada: ${blankUrl}`);
3413
+ console.error(` >> El portal carg\xF3 el shell (navegaci\xF3n, header) pero los WIDGETS/COMPONENTES quedaron en blanco.`);
3414
+ console.error(` >> Evidencia guardada: ${blankEvidencePath}`);
3415
+ console.error(` >> Tipo: portal_rendering_failure | Esto es un bug del portal, NO del agente.`);
3416
+ response.thought = `\u{1F6D1} [BUG DEL PORTAL DETECTADO] La p\xE1gina ${blankUrl} carg\xF3 su estructura pero los componentes principales (widgets, tablas, listas) NO se renderizaron. El agente estuvo esperando ${consecutiveWaits} turnos sin mejora. Esto es un fallo de renderizado del portal.`;
3417
+ response.finish = true;
3418
+ response.actions = [];
3419
+ aiMarkedSuccess = false;
3420
+ hasCriticalError = true;
3421
+ failReason = "portal_rendering_failure";
3422
+ history.push(`Turno ${stepCount}: \u{1F6D1} [PORTAL_RENDER_FAILURE] La p\xE1gina ${blankUrl} carg\xF3 en blanco (${consecutiveWaits} esperas sin progreso). Los widgets no se renderizaron. Bug del portal detectado por Arcality QA. Evidencia: ${blankEvidencePath}`);
3423
+ saveMissionResults();
2873
3424
  }
2874
3425
  }
2875
3426
  if (response.actions && response.actions.length > 0) {
@@ -2882,8 +3433,15 @@ function captureValidationRule(errorMessage, context) {
2882
3433
  const urlBeforeAction = page.url();
2883
3434
  if ((step.action === "click" || step.action === "double_click") && loc) {
2884
3435
  const desc = (step.description || "").toLowerCase();
2885
- const isMenuTrigger = desc.includes("more_vert") || desc.includes("menu") || desc.includes("icono :") || desc.includes("dots") || desc.includes("opciones");
2886
- const isSubmitAction = desc.includes("guardar") || desc.includes("save") || desc.includes("crear") || desc.includes("registrar") || desc.includes("enviar") || desc.includes("aceptar") || desc.includes("confirmar") || desc.includes("update");
3436
+ const elementType = await loc.getAttribute("type").catch(() => "") || "";
3437
+ const elementRole = await loc.getAttribute("role").catch(() => "") || "";
3438
+ const ariaExpanded = await loc.getAttribute("aria-expanded").catch(() => "") || "";
3439
+ const isInsideForm = await loc.evaluate((el) => !!el.closest("form")).catch(() => false);
3440
+ const isLoginPage = page.url().includes("/login") || page.url().includes("/auth") || page.url().includes("oauth");
3441
+ const isAnchorLink = desc.startsWith("[a]") || step.type === "a";
3442
+ const isMenuItem = isAnchorLink || elementRole === "menuitem" || elementRole === "option" || desc.startsWith("[li]") || step.type === "li";
3443
+ const isMenuTrigger = desc.includes("more_vert") || desc.includes("more_horiz") || elementRole === "menu" || ariaExpanded !== "";
3444
+ const isSubmitAction = !isMenuItem && (elementType === "submit" || isInsideForm && step.type === "button" && !isLoginPage);
2887
3445
  if (isSubmitAction) {
2888
3446
  const rawBodyText = await page.innerText("body").catch(() => "");
2889
3447
  const textLower = rawBodyText.toLowerCase();
@@ -2897,6 +3455,7 @@ function captureValidationRule(errorMessage, context) {
2897
3455
  console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Feedback de ERROR en elemento peque\xF1o: "${elementTextLower.substring(0, 50)}..."`);
2898
3456
  aiMarkedSuccess = false;
2899
3457
  hasCriticalError = true;
3458
+ failReason = "ui_validation_error";
2900
3459
  isFinished = true;
2901
3460
  saveMissionResults();
2902
3461
  break;
@@ -2912,12 +3471,10 @@ function captureValidationRule(errorMessage, context) {
2912
3471
  }
2913
3472
  }
2914
3473
  }
2915
- const previousSubmit = history.find(
2916
- (h) => h.toLowerCase().includes("click") && (h.toLowerCase().includes("guardar") || h.toLowerCase().includes("save") || h.toLowerCase().includes("crear") || h.toLowerCase().includes("registrar"))
2917
- );
3474
+ const previousSubmit = history.find((h) => h.includes("[SUBMIT]"));
2918
3475
  if (previousSubmit && !allowRepeatedNames) {
2919
3476
  console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Advertencia: Click persistente en bot\xF3n de acci\xF3n.`);
2920
- history.push(`Turno ${stepCount}: Intentaste hacer click en "${desc}" nuevamente, pero la p\xE1gina sigue mostrando el mismo estado. \xBFHay alg\xFAn error visible que debas corregir antes?`);
3477
+ history.push(`Turno ${stepCount}: [SUBMIT] Intentaste enviar el formulario con "${desc}" nuevamente, pero la p\xE1gina sigue en el mismo estado. \xBFHay alg\xFAn error visible que debas corregir antes?`);
2921
3478
  await page.waitForTimeout(1e3);
2922
3479
  }
2923
3480
  }
@@ -2944,7 +3501,7 @@ function captureValidationRule(errorMessage, context) {
2944
3501
  }
2945
3502
  console.log(`
2946
3503
  \u{1F6D1} [AUTO-STOP] Bloqueando click repetido en "${desc}" (${sameActionCount} veces). Forzando re-evaluaci\xF3n.`);
2947
- history.push(`Turno ${stepCount}: \u{1F6D1} [AUTO-STOP] El click en "${desc}" fue bloqueado despu\xE9s de ${sameActionCount} repeticiones. Usa un IDX diferente o intenta hacer scroll para revelar el elemento. Si es un dropdown que no abre, usa interact_native_control.`);
3504
+ history.push(`Turno ${stepCount}: \u{1F6D1} [AUTO-STOP] El click en "${desc}" fue bloqueado despu\xE9s de ${sameActionCount} repeticiones. Si es un dropdown/combobox que no abre sus opciones, usa send_keyboard_event con key "ArrowDown" para navegar opciones y "Enter" para seleccionar. NO vuelvas a hacer click en el mismo elemento.`);
2948
3505
  await page.waitForTimeout(800);
2949
3506
  break;
2950
3507
  }
@@ -2956,19 +3513,23 @@ function captureValidationRule(errorMessage, context) {
2956
3513
  await loc.scrollIntoViewIfNeeded({ timeout: 5e3 }).catch(() => {
2957
3514
  });
2958
3515
  if (step.action === "double_click") {
2959
- await loc.dblclick({ timeout: 1e4, force: true });
3516
+ await loc.dblclick({ timeout: 4e3, force: true });
2960
3517
  } else {
2961
3518
  if (desc.includes("toast") || desc.includes("close")) {
2962
3519
  await loc.click({ timeout: 2e3, force: true }).catch(() => console.log(" \u26A0\uFE0F No se pudo cerrar toast, ignorando..."));
2963
3520
  } else {
2964
- await loc.click({ timeout: 1e4, force: true });
3521
+ await loc.click({ timeout: 4e3, force: true });
2965
3522
  }
2966
3523
  }
2967
3524
  if (isMenuTrigger) {
2968
3525
  console.log(" \u23F3 Abriendo men\xFA, esperando 1.5s para renderizado...");
2969
3526
  await page.waitForTimeout(1500);
2970
3527
  }
2971
- const actsLikeSubmit = desc.includes("guardar") || desc.includes("save") || desc.includes("crear") || desc.includes("registrar");
3528
+ if (!isMenuTrigger && (elementRole === "combobox" || elementRole === "listbox")) {
3529
+ console.log(" \u23F3 Dropdown/combobox detectado por ARIA role, esperando 800ms para opciones...");
3530
+ await page.waitForTimeout(800);
3531
+ }
3532
+ const actsLikeSubmit = isSubmitAction;
2972
3533
  if (actsLikeSubmit) {
2973
3534
  console.log(" \u23F3 Esperando transici\xF3n post-guardado...");
2974
3535
  try {
@@ -2989,8 +3550,29 @@ function captureValidationRule(errorMessage, context) {
2989
3550
  saveMissionResults();
2990
3551
  foundSuccessPost = true;
2991
3552
  break;
3553
+ } else if (systemErrorKeywords.test(fbText)) {
3554
+ console.error(`
3555
+ \u{1F6D1} [SYSTEM ERROR POST-GUARDADO] Portal report\xF3 error de sistema: "${fbText.substring(0, 100)}"`);
3556
+ console.error(` >> Tipo: portal_internal_error | Causa probable: fallo en la capa de servicio del backend.`);
3557
+ hasCriticalError = true;
3558
+ failReason = "portal_internal_error";
3559
+ isFinished = true;
3560
+ history.push(`Turno ${stepCount}: \u{1F6D1} ERROR SIST\xC9MICO POST-GUARDADO: "${fbText.trim().substring(0, 200)}". El portal respondi\xF3 con un error de servidor. Imposible continuar sin intervenci\xF3n manual.`);
3561
+ saveMissionResults();
3562
+ foundSuccessPost = true;
3563
+ break;
2992
3564
  } else if (failureKeywords.test(fbText.toLowerCase())) {
2993
3565
  console.log(`>>ARCALITY_STATUS>> \u{1F4DA} [BUSINESS RULE] Error de validaci\xF3n capturado: "${fbText.substring(0, 80)}"`);
3566
+ if (lastSeenErrorToast === fbText.trim()) {
3567
+ console.log(`>>ARCALITY_STATUS>> \u{1F6D1} ERROR PERSISTENTE DETECTADO. La aplicaci\xF3n no acepta los cambios tras reintento.`);
3568
+ hasCriticalError = true;
3569
+ failReason = "unresolved_ui_error";
3570
+ isFinished = true;
3571
+ history.push(`Turno ${stepCount}: \u{1F6D1} ERROR PERSISTENTE: "${fbText.trim()}". La misi\xF3n no puede continuar porque el portal sigue rechazando la acci\xF3n.`);
3572
+ saveMissionResults();
3573
+ break;
3574
+ }
3575
+ lastSeenErrorToast = fbText.trim();
2994
3576
  captureValidationRule(fbText.trim(), `Acci\xF3n: click en "${desc}" en ${page.url()}`);
2995
3577
  const isDuplicateError = /repetido|duplicado|ya existe|already exists/i.test(fbText);
2996
3578
  const correctionHint = isDuplicateError ? `CORRECCI\xD3N OBLIGATORIA: El valor que ingresaste ya existe. Ve al campo que lo caus\xF3 y c\xE1mbialo por uno \xFAnico (a\xF1ade _${Date.now().toString().slice(-4)} como sufijo).` : `CORRECCI\xD3N OBLIGATORIA: Corrige el campo se\xF1alado antes de intentar guardar de nuevo.`;
@@ -3036,11 +3618,36 @@ function captureValidationRule(errorMessage, context) {
3036
3618
  await page.waitForTimeout(500);
3037
3619
  }
3038
3620
  } else if (step.action === "fill" && loc && step.value) {
3039
- const isFile = await loc.evaluate((el) => el.tagName === "INPUT" && el.type === "file").catch(() => false);
3040
- if (isFile) {
3621
+ const inputType = await loc.evaluate((el) => el.tagName === "INPUT" ? (el.type || "").toLowerCase() : null).catch(() => null);
3622
+ if (inputType === "file") {
3041
3623
  console.log(` \u{1F4C2} Detectado input de archivo. Subiendo: "${step.value}"`);
3042
3624
  const filePath = step.value.replace(/^"|"$/g, "").trim();
3043
3625
  await loc.setInputFiles(filePath);
3626
+ } else if (inputType === "date" || inputType === "time" || inputType === "datetime-local") {
3627
+ console.log(` \u{1F39B}\uFE0F Detectado input nativo (${inputType}). Usando estrategia segura para: "${step.value}"`);
3628
+ try {
3629
+ await loc.fill(step.value, { timeout: 3e3 });
3630
+ await loc.evaluate((el) => {
3631
+ el.dispatchEvent(new Event("input", { bubbles: true }));
3632
+ el.dispatchEvent(new Event("change", { bubbles: true }));
3633
+ });
3634
+ } catch (err) {
3635
+ if (err.message && err.message.includes("Malformed value")) {
3636
+ console.log(` \u26A0\uFE0F Malformed value detectado en input nativo. Usando fallback de teclado...`);
3637
+ await loc.click({ force: true });
3638
+ await page.keyboard.press("Control+a");
3639
+ const digits = step.value.replace(/[:-T/]/g, "");
3640
+ await page.keyboard.type(digits, { delay: 50 });
3641
+ await loc.evaluate((el) => {
3642
+ el.dispatchEvent(new Event("input", { bubbles: true }));
3643
+ el.dispatchEvent(new Event("change", { bubbles: true }));
3644
+ });
3645
+ } else {
3646
+ throw err;
3647
+ }
3648
+ }
3649
+ await loc.press("Tab").catch(() => {
3650
+ });
3044
3651
  } else {
3045
3652
  await loc.click({ timeout: 5e3 }).catch(() => {
3046
3653
  });
@@ -3050,14 +3657,50 @@ function captureValidationRule(errorMessage, context) {
3050
3657
  }
3051
3658
  await page.waitForTimeout(500);
3052
3659
  } else if (step.action === "wait") {
3053
- await page.waitForTimeout(3e3);
3660
+ const waitMs = Math.min(parseInt(step.value || "3000", 10) || 3e3, 5e3);
3661
+ await page.waitForTimeout(waitMs);
3662
+ if (stepCount > 5) {
3663
+ try {
3664
+ const dockManager = await page.locator("igc-dockmanager").count().catch(() => 0);
3665
+ if (dockManager > 0) {
3666
+ const panels = await page.locator("igc-dockmanager [slot]").all();
3667
+ let emptyPanelCount = 0;
3668
+ for (const panel of panels.slice(0, 6)) {
3669
+ const txt = (await panel.innerText().catch(() => "")).trim();
3670
+ const isVisible = await panel.isVisible().catch(() => false);
3671
+ if (isVisible && txt.length < 10)
3672
+ emptyPanelCount++;
3673
+ }
3674
+ const panelCount = Math.min(panels.length, 6);
3675
+ if (panelCount >= 2 && emptyPanelCount >= panelCount * 0.6) {
3676
+ const blankUrl = page.url();
3677
+ const blankEvidencePath = path2.join(contextDir, `portal-blank-dashboard-${Date.now()}.png`);
3678
+ await page.screenshot({ path: blankEvidencePath, fullPage: true }).catch(() => {
3679
+ });
3680
+ console.error(`
3681
+ \u{1F6D1} [BLANK_DASHBOARD] El portal carg\xF3 el shell pero los widgets est\xE1n vac\xEDos.`);
3682
+ console.error(` >> URL: ${blankUrl}`);
3683
+ console.error(` >> ${emptyPanelCount} de ${panelCount} paneles est\xE1n en blanco.`);
3684
+ console.error(` >> Tipo: portal_rendering_failure | Bug del portal confirmado.`);
3685
+ console.error(` >> Evidencia: ${blankEvidencePath}`);
3686
+ hasCriticalError = true;
3687
+ failReason = "portal_rendering_failure";
3688
+ isFinished = true;
3689
+ history.push(`Turno ${stepCount}: \u{1F6D1} [BLANK_DASHBOARD] El portal carg\xF3 la p\xE1gina ${blankUrl} pero los widgets/tablas NO se renderizaron (${emptyPanelCount}/${panelCount} paneles vac\xEDos). Esto es un BUG DEL PORTAL detectado autom\xE1ticamente por Arcality QA. El agente NO puede continuar. Evidencia guardada: ${blankEvidencePath}`);
3690
+ saveMissionResults();
3691
+ break;
3692
+ }
3693
+ }
3694
+ } catch {
3695
+ }
3696
+ }
3054
3697
  }
3055
3698
  const descForHistory = (step.description || step.selector || "").substring(0, 80);
3056
3699
  const valStr = step.value ? ` con valor "${step.value}"` : "";
3057
3700
  history.push(`Turno ${stepCount}: ${step.action} en "${descForHistory}"${valStr}`);
3058
3701
  urlHistory.push(page.url());
3059
3702
  const compName = (step.description || "").substring(0, 80);
3060
- if (compName.length < 100) {
3703
+ if (compName.length > 0) {
3061
3704
  stepsData.push({
3062
3705
  url: urlBeforeAction,
3063
3706
  // USAR URL CAPTURADA ANTES
@@ -3069,16 +3712,76 @@ function captureValidationRule(errorMessage, context) {
3069
3712
  });
3070
3713
  }
3071
3714
  } catch (e) {
3072
- console.warn(` \u26A0\uFE0F Acci\xF3n fallida: ${e.message}`);
3073
- history.push(`Turno ${stepCount}: \u274C FALL\xD3 ${step.action} en "${step.description}" - Error: ${e.message}. REGLA MANDATORIA: Debes investigar la causa usando 'inspect_element_details' o 'capture_console_errors' antes de cualquier otro paso.`);
3074
- containsError = true;
3075
- const errorCount = history.filter((h) => h.includes("FALL\xD3") && h.includes(step.description)).length;
3076
- if (errorCount >= 2) {
3077
- console.error(`>>ARCALITY_STATUS>> \u{1F6D1} Demasiados fallos t\xE9cnicos en este elemento "${step.description}". Abortando misi\xF3n.`);
3715
+ const errMsg = e.message || "";
3716
+ if (errMsg.includes("[BLANK_STATE_CRASH]")) {
3717
+ console.error(`
3718
+ \u{1F6D1} [BLANK_STATE] El portal carg\xF3 en blanco despu\xE9s de la acci\xF3n "${step.description || step.action}".`);
3719
+ console.error(` >> Causa: Error de renderizado del portal (White Screen). El agente no puede continuar.`);
3078
3720
  hasCriticalError = true;
3721
+ failReason = "portal_render_failure";
3079
3722
  isFinished = true;
3723
+ history.push(`Turno ${stepCount}: \u{1F6D1} [BLANK_STATE_CRASH] El portal qued\xF3 en blanco tras navegar. Esto es un bug del portal, no del agente.`);
3724
+ saveMissionResults();
3080
3725
  break;
3081
3726
  }
3727
+ const isPageClosed = errMsg.includes("Target page, context or browser has been closed") || errMsg.includes("page has been closed") || errMsg.includes("Session closed") || page.isClosed();
3728
+ if (isPageClosed) {
3729
+ console.error(`
3730
+ \u{1F6D1} [PAGE CRASH] La pesta\xF1a colaps\xF3 durante la acci\xF3n "${step.description || step.action}"`);
3731
+ console.error(` >> Error: ${errMsg.substring(0, 150)}`);
3732
+ console.error(` >> Tipo: portal_page_crash | El portal cerr\xF3 el contexto del navegador inesperadamente.`);
3733
+ hasCriticalError = true;
3734
+ failReason = "portal_page_crash";
3735
+ isFinished = true;
3736
+ saveMissionResults();
3737
+ break;
3738
+ }
3739
+ const isPlaywrightTimeout = errMsg.includes("Timeout") && errMsg.includes("ms exceeded");
3740
+ const elementDesc = step.description || "";
3741
+ if (isPlaywrightTimeout && elementDesc) {
3742
+ const prevTimeoutOnSameEl = history.filter(
3743
+ (h) => h.includes("STALE_IDX") && h.includes(`IDX:${step.idx}`)
3744
+ ).length;
3745
+ if (prevTimeoutOnSameEl >= 1) {
3746
+ console.error(`
3747
+ \u{1F6D1} [STALE IDX] El elemento "${elementDesc.substring(0, 60)}" produjo Timeout 2 veces.`);
3748
+ console.error(` >> El DOM fue re-renderizado mientras la IA razonaba. El IDX original ya no existe.`);
3749
+ console.error(` >> Tipo: stale_element_timeout | Abortando para evitar ciclo infinito.`);
3750
+ hasCriticalError = true;
3751
+ failReason = "stale_element_timeout";
3752
+ isFinished = true;
3753
+ history.push(`Turno ${stepCount}: \u{1F6D1} [STALE_IDX] Timeout REPETIDO en "${elementDesc}". El elemento fue destruido por un re-render de React/Angular. La misi\xF3n no puede continuar sin intervenci\xF3n manual.`);
3754
+ saveMissionResults();
3755
+ break;
3756
+ } else {
3757
+ history.push(`Turno ${stepCount}: \u274C [STALE_IDX] FALL\xD3 ${step.action} en "${elementDesc}" \u2014 Timeout: El elemento con IDX:${step.idx} ya NO existe en el DOM porque la p\xE1gina se re-renderiz\xF3 mientras razonabas. ACCI\xD3N OBLIGATORIA: En tu pr\xF3ximo turno, NO uses el mismo IDX. La percepci\xF3n ya te dar\xE1 los IDX actualizados. Busca el elemento por su texto visible, no por IDX.`);
3758
+ }
3759
+ } else {
3760
+ history.push(`Turno ${stepCount}: \u274C FALL\xD3 ${step.action} en "${elementDesc}" - Error: ${errMsg.substring(0, 200)}. REGLA MANDATORIA: Debes investigar la causa usando 'inspect_element_details' o 'capture_console_errors' antes de cualquier otro paso.`);
3761
+ }
3762
+ console.warn(` \u26A0\uFE0F Acci\xF3n fallida: ${errMsg.substring(0, 200)}`);
3763
+ containsError = true;
3764
+ guideIdx = 999999;
3765
+ if (stepsData.length > 0) {
3766
+ console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Falla detectada. Invalidando Gu\xEDa de \xC9xito para sincronizar con IA...`);
3767
+ stepsDataBackup = [...stepsData];
3768
+ stepsData.length = 0;
3769
+ }
3770
+ if (!page.isClosed() && (step.type === "li" || (step.description || "").toLowerCase().includes("[li]"))) {
3771
+ console.log(` \u{1F4A1} Sugerencia: El elemento de lista fall\xF3. Intentando cerrar dropdown con Escape...`);
3772
+ await page.keyboard.press("Escape").catch(() => {
3773
+ });
3774
+ }
3775
+ if (!isPlaywrightTimeout) {
3776
+ const errorCount = history.filter((h) => h.includes("FALL\xD3") && h.includes(elementDesc)).length;
3777
+ if (errorCount >= 2) {
3778
+ console.error(`>>ARCALITY_STATUS>> \u{1F6D1} Demasiados fallos t\xE9cnicos en este elemento "${elementDesc}". Abortando misi\xF3n.`);
3779
+ hasCriticalError = true;
3780
+ failReason = "repeated_action_failure";
3781
+ isFinished = true;
3782
+ break;
3783
+ }
3784
+ }
3082
3785
  }
3083
3786
  }
3084
3787
  }
@@ -3112,7 +3815,7 @@ function captureValidationRule(errorMessage, context) {
3112
3815
  }
3113
3816
  }
3114
3817
  }
3115
- if (!hasVisibleFailure) {
3818
+ if (!hasVisibleFailure && !hasCriticalError) {
3116
3819
  aiMarkedSuccess = true;
3117
3820
  hasCriticalError = false;
3118
3821
  saveMissionResults();
@@ -3121,6 +3824,11 @@ function captureValidationRule(errorMessage, context) {
3121
3824
  console.log(`>>ARCALITY_STATUS>> \u{1F504} Gu\xEDa de \xC9xito restaurada (${stepsDataBackup.length} pasos recuperados).`);
3122
3825
  }
3123
3826
  await persistCollectiveMemory();
3827
+ } else if (hasCriticalError) {
3828
+ console.log(`>>ARCALITY_STATUS>> \u{1F6D1} Finalizando por error cr\xEDtico: ${failReason || "unknown"}`);
3829
+ aiMarkedSuccess = false;
3830
+ isFinished = true;
3831
+ saveMissionResults();
3124
3832
  } else {
3125
3833
  console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F El agente intent\xF3 finalizar pero se detect\xF3 un ERROR en alert/toast.`);
3126
3834
  aiMarkedSuccess = false;
@@ -3140,7 +3848,18 @@ function captureValidationRule(errorMessage, context) {
3140
3848
  if (await fb.isVisible()) {
3141
3849
  const rawTxt = await fb.innerText();
3142
3850
  const txt = rawTxt.toLowerCase();
3143
- if (failureKeywords.test(txt)) {
3851
+ if (systemErrorKeywords.test(rawTxt)) {
3852
+ console.error(`
3853
+ \u{1F6D1} [SYSTEM ERROR EN BUCLE PRINCIPAL] Portal report\xF3 error de sistema: "${rawTxt.substring(0, 100)}"`);
3854
+ console.error(` >> Tipo: portal_internal_error | El runner detendr\xE1 la misi\xF3n.`);
3855
+ hasCriticalError = true;
3856
+ failReason = "portal_internal_error";
3857
+ isFinished = true;
3858
+ aiMarkedSuccess = false;
3859
+ history.push(`Turno ${stepCount}: \u{1F6D1} [ERROR_SIST\xC9MICO] El portal devolvi\xF3 un error de servidor en la UI: "${rawTxt.trim().substring(0, 200)}". Este error no puede ser corregido por el agente. Requiere intervenci\xF3n en el backend.`);
3860
+ saveMissionResults();
3861
+ break;
3862
+ } else if (failureKeywords.test(txt)) {
3144
3863
  console.log(`>>ARCALITY_STATUS>> \u26A0\uFE0F Feedback de ERROR detectado: "${rawTxt.substring(0, 60)}..."`);
3145
3864
  aiMarkedSuccess = false;
3146
3865
  captureValidationRule(rawTxt.trim(), `URL: ${page.url()}`);
@@ -3152,13 +3871,20 @@ function captureValidationRule(errorMessage, context) {
3152
3871
  history.push(`Turno ${stepCount}: \u{1F6D1} [ERROR_CR\xCDTICO] Feedback de validaci\xF3n/error visible en UI: "${rawTxt.trim()}"`);
3153
3872
  } else if (successKeywords.test(txt)) {
3154
3873
  if (txt !== lastSeenSuccessToast) {
3155
- console.log(`>>ARCALITY_STATUS>> \u2728 \xC9xito visual detectado: "${txt.substring(0, 50)}..."`);
3156
- aiMarkedSuccess = true;
3157
- isFinished = true;
3158
- hasCriticalError = false;
3159
- await persistCollectiveMemory();
3160
- saveMissionResults();
3161
- break;
3874
+ const isOnWizardForm = currentUrl.includes("/new") || currentUrl.includes("/create") || currentUrl.includes("/edit");
3875
+ const isStrongFinalSuccess = txt.includes("completado") || txt.includes("registro exitoso") || txt.includes("creado exitosamente");
3876
+ if (isOnWizardForm && !isStrongFinalSuccess && stepCount < maxSteps - 2) {
3877
+ console.log(`>>ARCALITY_STATUS>> \u{1F4A1} Toast parcial detectado en wizard ("${txt.substring(0, 30)}"). Delegando confirmaci\xF3n a la IA para evitar falso positivo.`);
3878
+ lastSeenSuccessToast = txt;
3879
+ } else {
3880
+ console.log(`>>ARCALITY_STATUS>> \u2728 \xC9xito visual detectado: "${txt.substring(0, 50)}..."`);
3881
+ aiMarkedSuccess = true;
3882
+ isFinished = true;
3883
+ hasCriticalError = false;
3884
+ await persistCollectiveMemory();
3885
+ saveMissionResults();
3886
+ break;
3887
+ }
3162
3888
  } else {
3163
3889
  console.log(`>>ARCALITY_STATUS>> \u{1F4A1} Mensaje de \xE9xito persistente ignorado.`);
3164
3890
  }
@@ -3170,6 +3896,50 @@ function captureValidationRule(errorMessage, context) {
3170
3896
  if (aiMarkedSuccess) {
3171
3897
  hasCriticalError = false;
3172
3898
  await persistCollectiveMemory();
3899
+ if (!usedDirectReuse && stepsData.length > 0) {
3900
+ const normalizedPrompt = prompt.toLowerCase().trim();
3901
+ const patternKey = crypto2.createHash("sha256").update(normalizedPrompt).digest("hex");
3902
+ if (!hasCriticalError) {
3903
+ const isDuplicate = patternContext !== "" || usedDirectReuse;
3904
+ if (isDuplicate) {
3905
+ console.log(`>>ARCALITY_STATUS>> \u2139\uFE0F No se guardar\xE1 el patr\xF3n. duplicate_skipped: true (ya existe uno similar o se us\xF3 directamente)`);
3906
+ }
3907
+ const stepsToSave = stepsData.length > 0 ? stepsData : stepsDataBackup;
3908
+ const hasCompleteSteps = stepsToSave.length >= stepsDataBackup.length;
3909
+ if (!isDuplicate && hasCompleteSteps) {
3910
+ console.log(`>>ARCALITY_STATUS>> \u{1F4BE} Guardando nuevo patr\xF3n de ejecuci\xF3n (${stepsToSave.length} pasos, pattern_saved: true)...`);
3911
+ const sanitizedSteps = stepsToSave.map((step, idx) => {
3912
+ const s = JSON.parse(JSON.stringify(step));
3913
+ s.step_index = idx;
3914
+ if (s.action_data && s.action_data.value) {
3915
+ if (s.action_data.value === process.env.LOGIN_USER)
3916
+ s.action_data.value = "{{username}}";
3917
+ else if (s.action_data.value === process.env.LOGIN_PASSWORD)
3918
+ s.action_data.value = "{{password}}";
3919
+ else if (s.componentType === "password" || s.action_data.value.length > 30)
3920
+ s.action_data.value = "{{secret}}";
3921
+ }
3922
+ return s;
3923
+ });
3924
+ await savePromptPattern({
3925
+ original_prompt: prompt,
3926
+ pattern_key: patternKey,
3927
+ name: prompt.length > 50 ? prompt.substring(0, 47) + "..." : prompt,
3928
+ pattern_json: JSON.stringify({
3929
+ history,
3930
+ steps: sanitizedSteps.length,
3931
+ steps_technical: sanitizedSteps,
3932
+ assertions: [],
3933
+ variables: {}
3934
+ }),
3935
+ generated_playwright_code: ""
3936
+ }).catch(() => {
3937
+ });
3938
+ } else if (!isDuplicate && !hasCompleteSteps) {
3939
+ console.warn(`>>ARCALITY_STATUS>> \u26A0\uFE0F Patr\xF3n NO guardado: stepsData (${stepsToSave.length}) < stepsDataBackup (${stepsDataBackup.length}). El patr\xF3n existente en BD es m\xE1s completo y se preserva.`);
3940
+ }
3941
+ }
3942
+ }
3173
3943
  console.log("\u270D\uFE0F [HUMANIZANDO] Generando resumen de \xE9xito para el reporte...");
3174
3944
  const summary = await agent.humanizeMissionResult(prompt, history);
3175
3945
  if (summary) {
@@ -3221,13 +3991,33 @@ Evidence: ${JSON.stringify(v.evidence).substring(0, 400)}`,
3221
3991
  } catch (e) {
3222
3992
  console.error("Error in Security Scan:", e.message);
3223
3993
  }
3994
+ if (!aiMarkedSuccess && !failReason) {
3995
+ if (stepCount >= maxSteps)
3996
+ failReason = "max_steps_reached";
3997
+ else
3998
+ failReason = "unknown_failure";
3999
+ }
3224
4000
  saveMissionResults();
3225
4001
  if (!aiMarkedSuccess) {
3226
4002
  console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Uso total de tokens esta misi\xF3n: IN=${totalMissionUsage.input_tokens} | OUT=${totalMissionUsage.output_tokens} | Cache writes=${totalMissionUsage.cache_creation_input_tokens} | Cache reads=${totalMissionUsage.cache_read_input_tokens}`);
3227
4003
  await persistCollectiveMemory(true).catch(() => {
3228
4004
  });
3229
- throw new Error("Misi\xF3n no completada o finalizada con errores.");
4005
+ throw new Error(`Misi\xF3n no completada o finalizada con errores. Raz\xF3n: ${failReason}`);
3230
4006
  } else {
3231
4007
  console.log(`>>ARCALITY_STATUS>> \u{1F4CA} Uso total de tokens esta misi\xF3n: IN=${totalMissionUsage.input_tokens} | OUT=${totalMissionUsage.output_tokens} | Cache writes=${totalMissionUsage.cache_creation_input_tokens} | Cache reads=${totalMissionUsage.cache_read_input_tokens}`);
3232
4008
  }
4009
+ if (fs2.existsSync(contextDir)) {
4010
+ try {
4011
+ const files = fs2.readdirSync(contextDir);
4012
+ for (const file of files) {
4013
+ if (file.endsWith(".json") || file.endsWith(".png")) {
4014
+ fs2.unlinkSync(path2.join(contextDir, file));
4015
+ }
4016
+ }
4017
+ fs2.rmSync(contextDir, { recursive: true, force: true });
4018
+ console.log(`>>ARCALITY_STATUS>> \u{1F9F9} Carpeta context eliminada (Memoria ef\xEDmera completada).`);
4019
+ } catch (err) {
4020
+ console.warn(`No se pudo limpiar la carpeta context: ${err}`);
4021
+ }
4022
+ }
3233
4023
  });