fix(v2): three real bugs found by an actual end-to-end run against production GLPI data

Ran the deployed workflow for real (temporary webhook trigger, briefly
activated, immediately deactivated and removed after each attempt) against
the real GLPI MySQL database and a real OpenAI credential. Found and fixed:

1. $env access is blocked on this n8n instance (N8N_BLOCK_ENV_ACCESS_IN_NODE).
   parseConfig($env) threw "access to env vars denied" on the very first
   Code node. Config now comes from the "Config - Coordenador GLPI v2" Set
   node instead (same pattern the V1 workflow already used) -- expanded
   that node with all 13 tunable parameters, and pointed the 3 affected
   node drivers (build-sql-candidatos, decide-needs-ai, select-top10) at
   it instead of $env.

2. Data Table "upsert" operation requires both `columns` (values) and a
   separate `filters` (match condition) -- I had only set columns with
   matchingColumns, which isn't enough; n8n refused to activate the
   workflow ("Missing or invalid required parameters: filters"). Added
   filters.conditions matching on ticket_id to both memory-writing Data
   Table nodes.

3. Real architecture bug: "Data Table - Buscar Memoria" sat inline in the
   41-candidate chain (Mesclar Contexto -> Buscar Memoria -> Decidir
   Necessidade de IA). A bulk "get all rows" Data Table node run with 41
   input items produced 41 identical fetches and its OWN output (the empty
   memory table on a first run) replaced the 41 candidates entirely --
   "Decidir Necessidade de IA" received 0 items and the whole main branch
   silently skipped. Moved the memory fetch to a parallel branch off
   Config (fires once) and added a real Merge node ("Merge - Aguardar
   Memoria") to guarantee both the candidates and the memory rows have
   executed before "Decidir Necessidade de IA" runs -- same synchronization
   pattern already used for context and for the shadow comparison.

Also fixed a cosmetic bug found in the real message output: buildTopMessage
produced a quadruple newline between the header and the first ticket block
([header, '', ...blocks].join('\n\n') double-counts the blank line already
inserted by the '\n\n' join). Added a regression test.

Verified with a real execution (id 13599, 24/08/2026 19:25 UTC) against the
live GLPI database: 41 real candidates, 30 sent to OpenAI (capped), 11
fallback-capped, memory table populated with 41 real rows including
genuine WAITING_VENDOR/WAITING_REQUESTER classifications with real
blocker_type/waiting_for fields extracted from real ticket task content,
and a real shadow_runs row comparing Legacy x V2 (4 tickets removed by V2:
2 for INSUFFICIENT_CONTEXT, 1 WAITING_REQUESTER, 1 WAITING_VENDOR; 4 added).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Gustavo
2026-08-24 16:30:13 -03:00
co-authored by Claude Sonnet 5
parent af48ea69d8
commit ea105d525c
7 changed files with 192 additions and 23 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ function buildTopMessage(selected, dateStr, config) {
];
return linhasDoBloco.join('\n');
});
const partesDaMensagem = [header, '', ...blocks];
const partesDaMensagem = [header, ...blocks];
return partesDaMensagem.join('\n\n');
}
+5
View File
@@ -40,6 +40,11 @@ test('digest de coordenacao vazio retorna null (nao manda lista igual todo dia)'
assert.equal(buildCoordinationDigest([], '24/08'), null);
});
test('mensagem nao tem linhas em branco duplicadas entre cabecalho e blocos', () => {
const msg = buildTopMessage([selectedItem({}), selectedItem({ ticket: { ...selectedItem({}).ticket, id: 38612 } })], '24/08', {});
assert.ok(!msg.includes('\n\n\n'), 'nao deveria haver 3+ quebras de linha seguidas');
});
test('digest de coordenacao lista apenas os itens passados (novos/alterados)', () => {
const digest = buildCoordinationDigest([{ ticket: { id: 37691 }, aiClassification: { reason: 'aguardando definicao' }, exclusionReason: 'needs_coordination' }], '24/08');
assert.ok(digest.includes('#37691'));
File diff suppressed because one or more lines are too long
+6 -2
View File
@@ -1,6 +1,10 @@
// @requires config.js
const config = parseConfig($env);
const glpiBaseUrl = String($('Config - Coordenador GLPI v2').first().json.glpi_base_url || '').replace(/\/$/, '').replace(/'/g, "''");
// Esta instancia n8n bloqueia acesso a $env em Code nodes (N8N_BLOCK_ENV_ACCESS_IN_NODE),
// entao a configuracao vem do node "Config - Coordenador GLPI v2" (mesmo padrao do V1),
// nao de variaveis de ambiente reais.
const configSource = $('Config - Coordenador GLPI v2').first().json;
const config = parseConfig(configSource);
const glpiBaseUrl = String(configSource.glpi_base_url || '').replace(/\/$/, '').replace(/'/g, "''");
const queryTemplate = /* __SQL_CANDIDATOS_V2__ */;
+5 -2
View File
@@ -1,8 +1,11 @@
// @requires config.js cache.js
const config = parseConfig($env);
// Instancia bloqueia $env em Code nodes -- config vem do node "Config - Coordenador GLPI v2".
const config = parseConfig($('Config - Coordenador GLPI v2').first().json);
const now = Date.now();
const items = $input.all().map((item) => item.json); // { ticket, contextHash }
// $input vem do Merge - Aguardar Memoria (so existe para GARANTIR que os candidatos e a
// memoria ja foram buscados antes deste node rodar); os dados de verdade sao lidos por nome.
const items = $('Code - Mesclar Contexto e Calcular Hash').all().map((item) => item.json); // { ticket, contextHash }
const memoryRows = $('Data Table - Buscar Memoria').all().map((item) => item.json);
const memoryByTicket = {};
for (const row of memoryRows) memoryByTicket[String(row.ticket_id)] = row;
+2 -1
View File
@@ -15,7 +15,8 @@ const scored = items.map((data) => ({
now: data.now,
}));
const config = scored.length ? scored[0].config : parseConfig($env);
// Instancia bloqueia $env em Code nodes -- config vem do node "Config - Coordenador GLPI v2".
const config = scored.length ? scored[0].config : parseConfig($('Config - Coordenador GLPI v2').first().json);
const { selected, excluded } = selectTop(scored, config.topN, config);
const selectedOut = selected.map((item) => ({ json: { ...item, _grupo: 'selected' } }));
+39 -9
View File
@@ -32,12 +32,20 @@
"parameters": {
"assignments": {
"assignments": [
{
"id": "glpi-base-url-v2",
"name": "glpi_base_url",
"type": "string",
"value": "https://glpi.suaempresa.com.br"
}
{ "id": "glpi-base-url-v2", "name": "glpi_base_url", "type": "string", "value": "https://glpi.suaempresa.com.br" },
{ "id": "cfg-ai-enabled", "name": "AI_ENABLED", "type": "boolean", "value": true },
{ "id": "cfg-ai-provider", "name": "AI_PROVIDER", "type": "string", "value": "openai" },
{ "id": "cfg-ai-model", "name": "AI_MODEL", "type": "string", "value": "gpt-4o-mini" },
{ "id": "cfg-candidate-pool-size", "name": "CANDIDATE_POOL_SIZE", "type": "number", "value": 60 },
{ "id": "cfg-ai-max-candidates", "name": "AI_MAX_CANDIDATES", "type": "number", "value": 30 },
{ "id": "cfg-top-n", "name": "TOP_N", "type": "number", "value": 10 },
{ "id": "cfg-cooldown-days", "name": "COOLDOWN_DAYS", "type": "number", "value": 1 },
{ "id": "cfg-ai-confidence-threshold", "name": "AI_CONFIDENCE_THRESHOLD", "type": "number", "value": 0.6 },
{ "id": "cfg-ai-max-context-chars", "name": "AI_MAX_CONTEXT_CHARS", "type": "number", "value": 4000 },
{ "id": "cfg-dev-mode", "name": "DEV_MODE", "type": "boolean", "value": true },
{ "id": "cfg-shadow-mode", "name": "SHADOW_MODE", "type": "boolean", "value": true },
{ "id": "cfg-dias-sem-interacao-alerta", "name": "DIAS_SEM_INTERACAO_ALERTA", "type": "number", "value": 2 },
{ "id": "cfg-horas-sla-risco", "name": "HORAS_SLA_RISCO", "type": "number", "value": 8 }
]
},
"options": {}
@@ -112,7 +120,7 @@
"name": "Data Table - Buscar Memoria",
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [320, -160],
"position": [-1120, 300],
"parameters": {
"resource": "row",
"operation": "get",
@@ -121,6 +129,14 @@
"returnAll": true
}
},
{
"id": "merge-aguardar-memoria",
"name": "Merge - Aguardar Memoria",
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [320, -160],
"parameters": { "mode": "append", "numberInputs": 2 }
},
{
"id": "code-decidir-necessidade-ia",
"name": "Code - Decidir Necessidade de IA",
@@ -259,6 +275,11 @@
"resource": "row",
"operation": "upsert",
"dataTableId": { "mode": "name", "value": "coordenador_glpi_memoria" },
"filters": {
"conditions": [
{ "keyName": "ticket_id", "condition": "eq", "keyValue": "={{ $json.ticket_id }}" }
]
},
"columns": {
"mappingMode": "defineBelow",
"matchingColumns": ["ticket_id"],
@@ -306,6 +327,11 @@
"resource": "row",
"operation": "upsert",
"dataTableId": { "mode": "name", "value": "coordenador_glpi_memoria" },
"filters": {
"conditions": [
{ "keyName": "ticket_id", "condition": "eq", "keyValue": "={{ $json.ticket_id }}" }
]
},
"columns": {
"mappingMode": "defineBelow",
"matchingColumns": ["ticket_id"],
@@ -429,7 +455,8 @@
"Config - Coordenador GLPI v2": {
"main": [[
{ "node": "Code - Montar SQL Candidatos", "type": "main", "index": 0 },
{ "node": "Code - Montar SQL Legado", "type": "main", "index": 0 }
{ "node": "Code - Montar SQL Legado", "type": "main", "index": 0 },
{ "node": "Data Table - Buscar Memoria", "type": "main", "index": 0 }
]]
},
"Code - Montar SQL Candidatos": {
@@ -451,9 +478,12 @@
"main": [[{ "node": "Code - Mesclar Contexto e Calcular Hash", "type": "main", "index": 0 }]]
},
"Code - Mesclar Contexto e Calcular Hash": {
"main": [[{ "node": "Data Table - Buscar Memoria", "type": "main", "index": 0 }]]
"main": [[{ "node": "Merge - Aguardar Memoria", "type": "main", "index": 0 }]]
},
"Data Table - Buscar Memoria": {
"main": [[{ "node": "Merge - Aguardar Memoria", "type": "main", "index": 1 }]]
},
"Merge - Aguardar Memoria": {
"main": [[{ "node": "Code - Decidir Necessidade de IA", "type": "main", "index": 0 }]]
},
"Code - Decidir Necessidade de IA": {