terminal-smart-cli 0.97.65 → 0.97.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/agent.js CHANGED
@@ -89,11 +89,18 @@ function missionGuardDecision({ credits = 0, maxCredits = DEFAULT_MISSION_CREDIT
89
89
  // primeiro e os fallbacks permanecem dentro do contrato do plano.
90
90
  function planUpgradeLimit(plan) {
91
91
  const id = intelligence.normalizeAgentPlan ? intelligence.normalizeAgentPlan(plan) : String(plan || 'free').toLowerCase();
92
- return id === 'pro' ? 2 : (id === 'basic' ? 1 : 0);
92
+ // O Free passou de 0 para 1 degrau. Não é upgrade comercial: a contingência
93
+ // do Free é Luna (0.10/0.60), MAIS BARATA que o principal DeepSeek Pro
94
+ // (0.63/1.26). Com 0 degraus a contingência anunciada no plano ("DeepSeek Pro
95
+ // no chat, com contingência Luna") era cortada pelo slice e nunca existia.
96
+ return id === 'pro' ? 2 : 1;
93
97
  }
94
98
 
95
99
  function executorFallbackChain({ selectedModel, forced = false, source = 'cloud', plan = 'free', complex = false } = {}) {
96
- const first = String(selectedModel || DEFAULT_EXECUTOR).trim();
100
+ // Um id aposentado pode chegar aqui de config antiga, de histórico ou do
101
+ // roteador de outra versão. Converter na entrada é o que impede a cadeia
102
+ // inteira de apontar para um modelo que o gateway não serve mais.
103
+ const first = intelligence.supportedModelId(String(selectedModel || DEFAULT_EXECUTOR).trim());
97
104
  if (forced) return [first];
98
105
  const role = intelligence.agentRoleContract('executor', plan, { complex });
99
106
  // O roteador pode selecionar um executor mais específico que o contrato padrão.
@@ -2296,7 +2303,13 @@ async function run(task, opts = {}) {
2296
2303
  if (!_tr.ok && _tr.status !== 'blocked') {
2297
2304
  const _shellHint = name === 'executar_comando' ? commandRecoveryHint(input.comando, _tr.evidence) : '';
2298
2305
  if (_shellHint && result && typeof result === 'object') result = Object.assign({}, result, { _recovery: _shellHint });
2299
- _toolErrs.push({ tool: name, class: _tr.errorClass, retryable: _tr.retryable, evidence: _tr.evidence });
2306
+ // O sinal estruturado vem do ToolResult tipado — exitCode, errno e hadStderr, que
2307
+ // o core ja calculava e descartava — mais o pathKind, derivado AQUI a partir dos
2308
+ // argumentos da ferramenta: so o resultado ('relative' | 'absolute') viaja, o
2309
+ // caminho em si nunca sai da maquina.
2310
+ _toolErrs.push({ tool: name, class: _tr.errorClass, retryable: _tr.retryable, evidence: _tr.evidence,
2311
+ exitCode: _tr.exitCode, errno: _tr.errno, hadStderr: _tr.hadStderr,
2312
+ pathKind: require('./evolution-telemetry').pathKindOf(input) });
2300
2313
  const _failureKey = failureFingerprint(name, _tr, result);
2301
2314
  const _equivalentFailures = (_failureCounts.get(_failureKey) || 0) + 1;
2302
2315
  _failureCounts.set(_failureKey, _equivalentFailures);
package/lib/core.js CHANGED
@@ -95,7 +95,10 @@ const MODEL_WINDOWS = {
95
95
  'minimax': 200000, 'qwen': 64000, 'gemini': 500000, 'mimo': 128000,
96
96
  };
97
97
  const CTX_WINDOW_DEFAULT = 100000; // modelo desconhecido → conservador
98
- const DEFAULT_EXECUTOR = 'deepseek-v4-flash'; // executor padrão (venceu o bake-off de código)
98
+ // deepseek-v4-flash venceu o bake-off de código em 2026-08 e era o executor
99
+ // padrão, mas foi aposentado do gateway. O padrão passa a ser o executor do
100
+ // contrato oficial (AGENT_ROLES.executor), que é o mesmo nas três superfícies.
101
+ const DEFAULT_EXECUTOR = 'deepseek-v4-pro';
99
102
  // limiar/rabo têm override por env (TS_COMPACT_AT / TS_COMPACT_TAIL) — usado nos testes.
100
103
  const COMPACT_AT = Number(process.env.TS_COMPACT_AT) > 0 ? Number(process.env.TS_COMPACT_AT) : 0.65;
101
104
  // piso de 6: rabo curto demais apaga a memória de trabalho e o agente relê arquivos em loop
@@ -167,11 +170,44 @@ function classifyError(msg) {
167
170
  return 'unknown';
168
171
  }
169
172
 
170
- // raw = retorno de uma ferramenta (execute). Devolve { ok, status, errorClass, retryable, evidence }.
173
+ // raw = retorno de uma ferramenta (execute). Devolve
174
+ // { ok, status, errorClass, retryable, evidence, exitCode, errno, hadStderr }.
171
175
  // NÃO muta o raw. Convenções do TS: {erro} = falha; {codigo|exitCode}≠0 = falha de shell;
172
176
  // {written|created|resumo|stdout|...} = evidência de sucesso.
177
+ //
178
+ // Os três últimos campos entraram em 12/09/2026 e são o SINAL ESTRUTURADO que a telemetria
179
+ // do harness passou a aceitar. Eles já estavam aqui dentro: `exit` e `errMsg` eram
180
+ // calculados e descartados. Sem eles, três falhas com correções completamente diferentes
181
+ // chegavam ao banco como a mesma linha:
182
+ //
183
+ // ENOENT o arquivo não existe -> criar, ou corrigir o caminho
184
+ // EACCES existe, mas sem permissão -> permissão, ou outro caminho
185
+ // EISDIR pediram para ler um diretório -> usar listar_diretorio
186
+ //
187
+ // As três eram `not_found`. O mesmo vale para o código de saída: 127 é comando que não
188
+ // existe, 126 é arquivo sem permissão de execução, 1 é o programa rodou e reprovou.
189
+ //
190
+ // Nada aqui carrega conteúdo de usuário: errno é vocabulário do sistema operacional e sai
191
+ // de uma lista fechada, exitCode é um inteiro e hadStderr é booleano.
192
+ const ERRNOS_CONHECIDOS = new Set(['ENOENT', 'EACCES', 'EPERM', 'EISDIR', 'ENOTDIR', 'EEXIST',
193
+ 'ENOTEMPTY', 'ENOSPC', 'EROFS', 'EMFILE', 'ENFILE', 'ENAMETOOLONG', 'EBUSY', 'EIO', 'EINVAL',
194
+ 'EAGAIN', 'ETIMEDOUT', 'ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'EHOSTUNREACH', 'ENETUNREACH',
195
+ 'EADDRINUSE', 'ENOTFOUND', 'EAI_AGAIN']);
196
+
197
+ // Lista fechada, e não um regex solto: `\bE[A-Z]+\b` casaria com a palavra "ERROR", que
198
+ // aparece em quase toda mensagem de falha, e o campo viraria ruído em vez de sinal.
199
+ function extractErrno(msg) {
200
+ const candidatos = String(msg || '').toUpperCase().match(/\bE[A-Z_]{2,11}\b/g);
201
+ if (!candidatos) return '';
202
+ for (const c of candidatos) if (ERRNOS_CONHECIDOS.has(c)) return c;
203
+ return '';
204
+ }
205
+
173
206
  function classifyToolResult(raw) {
174
- if (raw == null) return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: '' };
207
+ if (raw == null) {
208
+ return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: '',
209
+ exitCode: -1, errno: '', hadStderr: false };
210
+ }
175
211
  // Alguns adaptadores legados retornam falhas como texto. Tratar todo texto como sucesso
176
212
  // permitia que "ERRO ao executar..." virasse etapa concluída. Mantém strings normais como
177
213
  // sucesso, mas reconhece prefixos inequívocos de falha/bloqueio.
@@ -181,13 +217,17 @@ function classifyToolResult(raw) {
181
217
  if (failedText) {
182
218
  const errorClass = classifyError(text);
183
219
  return { ok: false, status: errorClass === 'blocked' ? 'blocked' : 'failed', errorClass,
184
- retryable: RETRYABLE_CLASSES.has(errorClass), evidence: text.replace(/\s+/g, ' ').slice(0, 300) };
220
+ retryable: RETRYABLE_CLASSES.has(errorClass), evidence: text.replace(/\s+/g, ' ').slice(0, 300),
221
+ exitCode: -1, errno: extractErrno(text), hadStderr: false };
185
222
  }
186
- return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: text.slice(0, 200) };
223
+ return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: text.slice(0, 200),
224
+ exitCode: -1, errno: '', hadStderr: false };
187
225
  }
188
226
  const exit = raw.codigo != null ? raw.codigo : (raw.exitCode != null ? raw.exitCode : null);
189
227
  const errMsg = raw.erro || raw.error || (exit != null && exit !== 0 ? (raw.stderr || raw.stdout || ('exit ' + exit)) : '');
190
228
  const failed = !!raw.erro || !!raw.error || (exit != null && exit !== 0);
229
+ const exitCode = exit != null && Number.isFinite(Number(exit)) ? Number(exit) : -1;
230
+ const hadStderr = !!(raw.stderr && String(raw.stderr).trim());
191
231
  if (failed) {
192
232
  const errorClass = classifyError(errMsg);
193
233
  return {
@@ -196,10 +236,14 @@ function classifyToolResult(raw) {
196
236
  errorClass,
197
237
  retryable: RETRYABLE_CLASSES.has(errorClass),
198
238
  evidence: String(errMsg).replace(/\s+/g, ' ').slice(0, 300),
239
+ exitCode,
240
+ errno: extractErrno(errMsg),
241
+ hadStderr,
199
242
  };
200
243
  }
201
244
  const ev = raw.written || raw.created || raw.resumo || raw.arquivo || (raw.stdout != null ? String(raw.stdout).slice(0, 200) : '') || (raw.total != null ? (raw.total + ' resultado(s)') : '');
202
- return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: String(ev || '').replace(/\s+/g, ' ').slice(0, 200) };
245
+ return { ok: true, status: 'ok', errorClass: null, retryable: false, evidence: String(ev || '').replace(/\s+/g, ' ').slice(0, 200),
246
+ exitCode, errno: '', hadStderr };
203
247
  }
204
248
 
205
249
  // ── SEGREDOS: nunca imprimir chave/token na saída (B18) ───────────────────────
package/lib/eval.js CHANGED
@@ -24,7 +24,9 @@ const agent = require('./agent');
24
24
  const intelligence = require('./intelligence-core');
25
25
 
26
26
  // modelo default do JUIZ: barato e suficiente pra avaliar critério objetivo.
27
- const DEFAULT_JUDGE = 'deepseek-v4-flash';
27
+ // Era deepseek-v4-flash, aposentado. Luna custa 0.10/0.60, tem `review` no
28
+ // catálogo e é mais barata que o Flash aposentado era.
29
+ const DEFAULT_JUDGE = 'gpt-5.6-luna';
28
30
 
29
31
  // ── suíte ────────────────────────────────────────────────────────────────────
30
32
  function loadSuite(p) {
@@ -18,6 +18,54 @@ function sanitizePattern(value) {
18
18
  .replace(/\s+/g, ' ').trim().slice(0, 300);
19
19
  }
20
20
 
21
+ // ── Sinal estruturado, de vocabulário FECHADO ────────────────────────────────
22
+ //
23
+ // Estes quatro campos entraram em 12/09/2026, junto com as colunas correspondentes no
24
+ // servidor. Antes, o incidente carregava três categorias (ferramenta, classe, estágio) e
25
+ // um `pattern` derivado da própria classe — que, por derivar dela, não acrescentava
26
+ // entropia nenhuma ao fingerprint. ENOENT e EACCES na mesma ferramenta viravam a MESMA
27
+ // linha no banco, e o revisor automático respondia, com razão, que os dados agregados não
28
+ // permitiam determinar a causa.
29
+ //
30
+ // O vocabulário é fechado de propósito, e validado AQUI e de novo no servidor: são os dois
31
+ // únicos lugares em que um valor inesperado poderia virar texto livre, e texto livre nesta
32
+ // tabela é conteúdo de usuário vazando.
33
+ const ERRNOS_CONHECIDOS = new Set(['ENOENT', 'EACCES', 'EPERM', 'EISDIR', 'ENOTDIR', 'EEXIST',
34
+ 'ENOTEMPTY', 'ENOSPC', 'EROFS', 'EMFILE', 'ENFILE', 'ENAMETOOLONG', 'EBUSY', 'EIO', 'EINVAL',
35
+ 'EAGAIN', 'ETIMEDOUT', 'ECONNREFUSED', 'ECONNRESET', 'EPIPE', 'EHOSTUNREACH', 'ENETUNREACH',
36
+ 'EADDRINUSE', 'ENOTFOUND', 'EAI_AGAIN']);
37
+
38
+ function safeErrno(value) {
39
+ const code = String(value || '').trim().toUpperCase().replace(/[^A-Z_0-9]/g, '').slice(0, 20);
40
+ return ERRNOS_CONHECIDOS.has(code) ? code : '';
41
+ }
42
+
43
+ function safeExitCode(value) {
44
+ if (value === null || value === undefined || value === '') return -1;
45
+ const n = Number(value);
46
+ if (!Number.isFinite(n)) return -1;
47
+ return Math.max(-1, Math.min(255, Math.trunc(n)));
48
+ }
49
+
50
+ // Relativo ou absoluto responde a pergunta que o revisor fez em duas propostas diferentes:
51
+ // o harness errou o CAMINHO, ou errou o DIRETÓRIO DE TRABALHO? São correções distintas, e
52
+ // hoje as duas chegam como `not_found`.
53
+ //
54
+ // A derivação acontece aqui, no CLI, onde os argumentos da ferramenta existem — e só o
55
+ // resultado ('relative' | 'absolute') viaja. O caminho em si nunca sai da máquina.
56
+ function pathKindOf(args) {
57
+ if (!args || typeof args !== 'object') return '';
58
+ const alvo = args.caminho || args.arquivo || args.diretorio || args.origem || '';
59
+ const s = String(alvo).trim();
60
+ if (!s) return '';
61
+ return /^(?:[a-zA-Z]:[\\/]|\\\\|\/)/.test(s) ? 'absolute' : 'relative';
62
+ }
63
+
64
+ function safePathKind(value) {
65
+ const kind = String(value || '').trim().toLowerCase();
66
+ return kind === 'relative' || kind === 'absolute' ? kind : '';
67
+ }
68
+
21
69
  function buildEvents(toolErrors = [], actions = [], context = {}) {
22
70
  const recoveredTools = new Set((actions || []).map(action => action && action.name).filter(Boolean));
23
71
  const seen = new Set(); const events = [];
@@ -26,10 +74,18 @@ function buildEvents(toolErrors = [], actions = [], context = {}) {
26
74
  const errorClass = String(error.errorClass || error.class || 'unknown').slice(0, 80);
27
75
  const pattern = sanitizePattern(error.evidence || error.message || '');
28
76
  const stage = String(error.stage || 'execution').slice(0, 40);
29
- const fingerprint = crypto.createHash('sha256').update([tool, errorClass, stage, pattern].join('|')).digest('hex');
77
+ const errno = safeErrno(error.errno);
78
+ const exitCode = safeExitCode(error.exitCode);
79
+ const pathKind = safePathKind(error.pathKind);
80
+ const hadStderr = error.hadStderr === true;
81
+ // O fingerprint inclui o sinal novo: sem isso, duas causas diferentes continuariam
82
+ // deduplicadas aqui antes mesmo de chegar ao servidor.
83
+ const fingerprint = crypto.createHash('sha256')
84
+ .update([tool, errorClass, stage, pattern, errno, String(exitCode), pathKind].join('|')).digest('hex');
30
85
  if (seen.has(fingerprint)) continue;
31
86
  seen.add(fingerprint);
32
87
  events.push({ tool, errorClass, pattern, stage, fingerprint,
88
+ errno, exitCode, pathKind, hadStderr,
33
89
  retryable: error.retryable === true, recovered: recoveredTools.has(tool),
34
90
  surface: context.surface || 'cli', appVersion: context.appVersion || 'unknown', model: context.model || 'unknown' });
35
91
  if (events.length >= 20) break;
@@ -42,4 +98,4 @@ function enabled(config = {}, env = process.env) {
42
98
  return config.diagnosticsEnabled !== false;
43
99
  }
44
100
 
45
- module.exports = { sanitizePattern, buildEvents, enabled };
101
+ module.exports = { sanitizePattern, buildEvents, enabled, pathKindOf };
@@ -171,14 +171,27 @@ const MODEL_CATALOG = Object.freeze({
171
171
  });
172
172
 
173
173
  const MODEL_ALIASES = Object.freeze({
174
- deepseek: 'deepseek-v4-flash',
175
- 'deepseek-v4': 'deepseek-v4-flash',
174
+ deepseek: 'deepseek-v4-pro',
175
+ 'deepseek-v4': 'deepseek-v4-pro',
176
176
  glm: 'glm-4.6',
177
177
  mimo: 'mimo-v2.5',
178
- 'gemini-lite': 'gemini-2.5-flash-lite',
178
+ 'gemini-lite': 'gemini-2.5-flash',
179
179
  'claude-haiku': 'claude-haiku-4-5',
180
180
  });
181
181
 
182
+ // Modelos aposentados do gateway do produto. Eles PERMANECEM no catalogo porque
183
+ // historico de consumo antigo ainda precisa ser precificado com o preco que
184
+ // valia na epoca; o que eles nao podem mais e entrar em contrato ativo ou em
185
+ // roteamento. A conversao mora AQUI, em cada superficie, e nao na reescrita que
186
+ // o backend faz no gate do proxy: uma superficie que dependesse daquela correcao
187
+ // ficaria errada em offline, em BYOK e em qualquer caminho que nao passe pelo
188
+ // proxy — foi exatamente o caso do CLI, cuja cadeia de executor do plano Free
189
+ // apontava inteira para os dois ids abaixo.
190
+ const RETIRED_MODELS = Object.freeze({
191
+ 'deepseek-v4-flash': 'deepseek-v4-pro',
192
+ 'gemini-2.5-flash-lite': 'gemini-2.5-flash',
193
+ });
194
+
182
195
  // Equipe oficial do modo Automático: especialização e prova objetiva vêm antes
183
196
  // de cascatas longas de modelos. Premium fica fora até validação real.
184
197
  const OFFICIAL_AUTOMATIC_TEAM = Object.freeze({
@@ -194,23 +207,23 @@ const OFFICIAL_AUTOMATIC_TEAM = Object.freeze({
194
207
  const AGENT_ROLES = Object.freeze({
195
208
  classifier: Object.freeze({
196
209
  prompt: 'Classifique a intenção e devolva somente a estrutura pedida. Não execute ferramentas nem invente dados.',
197
- models: Object.freeze({ free: ['gemini-2.5-flash-lite'], basic: ['gemini-2.5-flash-lite', 'gpt-4o-mini'], pro: ['gemini-2.5-flash-lite', 'gpt-4o-mini'] }),
210
+ models: Object.freeze({ free: ['deepseek-v4-pro', 'gpt-5.6-luna'], basic: ['deepseek-v4-pro', 'gpt-4o-mini'], pro: ['deepseek-v4-pro', 'gpt-4o-mini'] }),
198
211
  }),
199
212
  planner: Object.freeze({
200
213
  prompt: 'Decomponha o objetivo em etapas executáveis, dependências, riscos, critérios de aceite e provas. Não execute a tarefa.',
201
- models: Object.freeze({ free: ['deepseek-v4-flash', 'gemini-2.5-flash-lite'], basic: ['gpt-5.6-luna', 'glm-5.2', 'deepseek-v4-pro'], pro: ['gpt-5.6-luna', 'glm-5.2', 'deepseek-v4-pro'] }),
214
+ models: Object.freeze({ free: ['deepseek-v4-pro', 'gpt-5.6-luna'], basic: ['gpt-5.6-luna', 'glm-5.2', 'deepseek-v4-pro'], pro: ['gpt-5.6-luna', 'glm-5.2', 'deepseek-v4-pro'] }),
202
215
  }),
203
216
  executor: Object.freeze({
204
217
  prompt: 'Execute somente a etapa recebida com as ferramentas autorizadas. Não declare sucesso sem resultado verificável.',
205
- models: Object.freeze({ free: ['deepseek-v4-flash', 'gemini-2.5-flash-lite'], basic: ['deepseek-v4-pro', 'gpt-5.6-luna', 'glm-5.2'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna', 'glm-5.2'] }),
218
+ models: Object.freeze({ free: ['deepseek-v4-pro', 'gpt-5.6-luna'], basic: ['deepseek-v4-pro', 'gpt-5.6-luna', 'glm-5.2'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna', 'glm-5.2'] }),
206
219
  }),
207
220
  inspector: Object.freeze({
208
221
  prompt: 'Inspecione em modo somente leitura e compare evidências com os critérios de aceite. Não altere nada e não elogie por cortesia.',
209
- models: Object.freeze({ free: ['gemini-2.5-flash-lite'], basic: ['gpt-5.6-luna', 'glm-5.2'], pro: ['gpt-5.6-luna', 'glm-5.2'] }),
222
+ models: Object.freeze({ free: ['deepseek-v4-pro', 'gpt-5.6-luna'], basic: ['gpt-5.6-luna', 'glm-5.2'], pro: ['gpt-5.6-luna', 'glm-5.2'] }),
210
223
  }),
211
224
  corrector: Object.freeze({
212
225
  prompt: 'Corrija apenas falhas confirmadas pelo inspetor, preserve o que funciona e repita as provas afetadas. Não amplie o escopo.',
213
- models: Object.freeze({ free: ['deepseek-v4-flash', 'gemini-2.5-flash-lite'], basic: ['deepseek-v4-pro', 'gpt-5.6-luna', 'glm-5.2'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna', 'glm-5.2'] }),
226
+ models: Object.freeze({ free: ['deepseek-v4-pro', 'gpt-5.6-luna'], basic: ['deepseek-v4-pro', 'gpt-5.6-luna', 'glm-5.2'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna', 'glm-5.2'] }),
214
227
  }),
215
228
  vision: Object.freeze({
216
229
  prompt: 'Extraia fatos observáveis da imagem e separe leitura, inferência e incerteza. Não invente texto ilegível.',
@@ -222,11 +235,11 @@ const AGENT_ROLES = Object.freeze({
222
235
  }),
223
236
  site_builder: Object.freeze({
224
237
  prompt: 'Crie ou edite o site completo, responsivo e acessível. Preserve o existente quando editar e valide a URL publicada.',
225
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro', 'gpt-5.6-luna'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna'] }),
238
+ models: Object.freeze({ free: ['deepseek-v4-pro', 'gpt-5.6-luna'], basic: ['deepseek-v4-pro', 'gpt-5.6-luna'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna'] }),
226
239
  }),
227
240
  cloud_box: Object.freeze({
228
241
  prompt: 'Opere a box com mudanças mínimas, registre comandos e valide serviço, porta e URL. Nunca exponha segredos.',
229
- models: Object.freeze({ free: ['deepseek-v4-flash'], basic: ['deepseek-v4-pro'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna'] }),
242
+ models: Object.freeze({ free: ['deepseek-v4-pro', 'gpt-5.6-luna'], basic: ['deepseek-v4-pro'], pro: ['deepseek-v4-pro', 'gpt-5.6-luna'] }),
230
243
  }),
231
244
  });
232
245
 
@@ -256,6 +269,14 @@ function normalizeModelId(model) {
256
269
  return upstream ? upstream[0] : transportFree;
257
270
  }
258
271
 
272
+ // Camada de compatibilidade: recebe qualquer id (alias, id do upstream, id
273
+ // aposentado) e devolve um id que o produto ainda serve. Use isto — e nao
274
+ // normalizeModelId — em qualquer ponto que va CHAMAR um modelo.
275
+ function supportedModelId(model) {
276
+ const id = normalizeModelId(model);
277
+ return RETIRED_MODELS[id] || id;
278
+ }
279
+
259
280
  function modelInfo(model, now) {
260
281
  const id = normalizeModelId(model);
261
282
  const info = MODEL_CATALOG[id] || {
@@ -512,7 +533,12 @@ function routeModel(task, reliability) {
512
533
  if (t.kind === 'code') required.add('code');
513
534
  if (t.kind === 'review') required.add('review');
514
535
  if (t.kind === 'vision') required.add('vision');
515
- const candidates = (t.candidates || Object.keys(MODEL_CATALOG)).map(normalizeModelId).filter(id => MODEL_CATALOG[id] && id !== 'smart');
536
+ // Aposentado nao volta por roteamento. Sem este filtro o CLI devolvia
537
+ // deepseek-v4-flash como melhor candidato de codigo e dependia do backend
538
+ // reescrever depois — o que nao acontece em BYOK nem offline.
539
+ const candidates = (t.candidates || Object.keys(MODEL_CATALOG)).map(supportedModelId)
540
+ .filter(id => MODEL_CATALOG[id] && id !== 'smart')
541
+ .filter((id, i, list) => list.indexOf(id) === i);
516
542
  const rel = reliability || {};
517
543
  const plan = String(t.plan || '').toLowerCase();
518
544
  const isPaid = plan && plan !== 'free' && plan !== 'gratuito';
@@ -527,12 +553,13 @@ function routeModel(task, reliability) {
527
553
  const score = (observed * 0.35) + (m.quality * qualityWeight) + ((1 / (1 + price)) * costWeight) - (missing * 2);
528
554
  return { id, score: +score.toFixed(6), missing };
529
555
  }).sort((a, b) => b.score - a.score);
530
- return { model: scored[0] ? scored[0].id : (isPaid ? 'deepseek-v4-pro' : 'deepseek-v4-flash'), ranked: scored };
556
+ return { model: scored[0] ? scored[0].id : 'deepseek-v4-pro', ranked: scored };
531
557
  }
532
558
 
533
559
  module.exports = {
534
560
  INTELLIGENCE_CONTRACT, PRICE_REVISION, MODEL_CATALOG, MODEL_ALIASES, DEFAULT_PRICE,
535
561
  MEMORY_TYPES, MEMORY_TRUST, CONTEXT_SOURCES, OFFICIAL_AUTOMATIC_TEAM, AGENT_ROLES,
562
+ RETIRED_MODELS, supportedModelId,
536
563
  normalizeModelId, modelInfo, legacyPriceMap, normalizeUsage, estimateCostUsd, catalogFreshness,
537
564
  createContextLedger, createMemoryRecord, scoreMemoryRecord, createHandoff, cleanOperationalText, buildOperationalSummary,
538
565
  normalizeAgentPlan, agentRoleContract, routeModel,
@@ -44,6 +44,18 @@ class MissionToolCache {
44
44
  stats() { return { entries: this.entries.size, hits: this.hits, invalidations: this.invalidations, charactersSaved: this.charactersSaved, estimatedTokensSaved: Math.ceil(this.charactersSaved / 4) }; }
45
45
  }
46
46
 
47
+ // O terceiro parâmetro é IGNORADO de propósito, e continua na assinatura só para
48
+ // não quebrar os três pontos de chamada (cli/lib/agent.js, app-electron/main.js e
49
+ // backend/server/index.js), que ainda o calculam para a métrica de economia.
50
+ //
51
+ // Ele era `originalStillVisible`, e a decisão de encurtar dependia dele. O cálculo
52
+ // nas três superfícies verificava se existia uma mensagem com o mesmo
53
+ // `tool_call_id` — ou seja, se o ID estava visível, NÃO se o conteúdo ainda estava.
54
+ // Transports de assinatura pessoal reconstroem a conversa preservando o id e
55
+ // descartando o payload: o check aprovava, o modelo recebia "use o resultado
56
+ // anterior", não achava nada, relia o mesmo arquivo, recebia o mesmo ponteiro — e a
57
+ // missão só terminava quando o watchdog a matava. Não há como esta função saber se
58
+ // o payload sobreviveu, porque ela não vê a conversa; então ela não aposta.
47
59
  function cacheReference(entry, tool, originalStillVisible) {
48
60
  // Sempre devolva o conteúdo ao modelo. Alguns transports de assinatura pessoal
49
61
  // reconstroem a conversa e preservam o id da tool call, mas não o payload antigo.
package/lib/router.js CHANGED
@@ -7,6 +7,13 @@
7
7
  const { api } = require('./api');
8
8
  const keyring = require('./keyring');
9
9
 
10
+ // Modelo da camada 2 (classificacao de intencao). Era gemini-2.5-flash-lite,
11
+ // aposentado do gateway: a chamada falhava e o catch devolvia "chat" para tudo,
12
+ // o que fazia pedido de acao virar conversa. gemini-2.5-flash e o substituto
13
+ // da mesma familia, tem `classification` no catalogo, e a chamada usa
14
+ // max_completion_tokens: 5 — o custo por classificacao segue irrelevante.
15
+ const INTENT_MODEL = 'gemini-2.5-flash';
16
+
10
17
  // Pergunta/conversa: forma interrogativa ou pedido de explicação
11
18
  const Q_RE = /^(como|por ?qu[eê]|o ?que|qual|quais|quando|onde|quem|ser[aá] que|devo|posso|vale a pena|existe|tem como|é melhor|what|how|why|which|when|where|explique|me explica|explica|o que significa|diferen[çc]a entre)\b|\?\s*$/i;
12
19
 
@@ -67,7 +74,10 @@ async function classify(msg, token) {
67
74
  method: 'POST', signal: ctrl.signal,
68
75
  headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + k.key },
69
76
  body: JSON.stringify({
70
- model: keyring.modeloPara(k, 'gemini-2.5-flash-lite'), stream: false, max_completion_tokens: 5,
77
+ // Era gemini-2.5-flash-lite, aposentado: o gateway devolvia erro ou o
78
+ // backend reescrevia, e a classificação de intenção caía silenciosamente
79
+ // no catch que devolve "chat" para tudo.
80
+ model: keyring.modeloPara(k, INTENT_MODEL), stream: false, max_completion_tokens: 5,
71
81
  messages: [
72
82
  { role: 'system', content: 'Classifique a intenção da mensagem de um usuário de um CLI DevOps. Responda APENAS uma palavra: "chat" (pergunta, conversa, pedido de explicação), "agente" (quer que algo seja FEITO nesta máquina: comandos, arquivos, diagnóstico, instalação) ou "orquestrar" (objetivo GRANDE com vários entregáveis para uma equipe de agentes).' },
73
83
  { role: 'user', content: String(msg).slice(0, 500) },
@@ -79,7 +89,7 @@ async function classify(msg, token) {
79
89
  const word = String(j?.choices?.[0]?.message?.content || '').toLowerCase();
80
90
  // cobra a classificação (mesmo padrão do web-intent) — best-effort
81
91
  const u = j?.usage || {};
82
- if (u.prompt_tokens && !k.billingAuthoritative) api('/api/credit/charge', { method: 'POST', token, body: { model: 'gemini-2.5-flash-lite', inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }).catch(() => {});
92
+ if (u.prompt_tokens && !k.billingAuthoritative) api('/api/credit/charge', { method: 'POST', token, body: { model: INTENT_MODEL, inTok: u.prompt_tokens || 0, outTok: u.completion_tokens || 0 } }).catch(() => {});
83
93
  if (word.includes('agente')) return 'agente';
84
94
  if (word.includes('orquestrar')) return 'run';
85
95
  return 'chat';
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "terminal-smart-cli",
3
- "version": "0.97.65",
3
+ "version": "0.97.67",
4
4
  "description": "Terminal Smart no seu terminal — pergunte, analise logs por pipe e orquestre agentes de IA. Comando: ts",
5
5
  "bin": {
6
6
  "ts": "bin/ts.js"
7
7
  },
8
8
  "scripts": {
9
- "test": "node test/durable-operation.test.js && node test/mission-tool-cache.test.js && node test/content-quality-runtime-files.test.js && node test/personal-agent-llm.test.js && node test/personal-inspector-efficiency.test.js && node test/personal-inspector-context.test.js && node test/planner-fallback.test.js && node test/cli-agent-regressions.test.js && node test/harness-hardening.test.js"
9
+ "test": "node scripts/run-tests.js"
10
10
  },
11
11
  "files": [
12
12
  "bin",