terminal-smart-cli 0.98.0 → 0.99.0

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
@@ -756,6 +756,10 @@ function browserApprovalRequest(input = {}) {
756
756
  async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools = false, toolsOverride = null, onRetry = null, creditBudget = null, maxCompletionTokens = null, tries = 4 }) {
757
757
  // RESILIÊNCIA: o gateway CDC pode reiniciar/oscilar no meio de uma missão longa.
758
758
  // withRetry cobre conn/timeout/5xx (backoff+jitter); NUNCA re-tenta no_credits/auth.
759
+ // _tsPinned/_tsTask (marcas do resumo e da tarefa fixos) são internas: provedores estritos
760
+ // recusam campo extra na mensagem.
761
+ const wireMessages = (messages || []).map(m => (m && typeof m === 'object' && ('_tsPinned' in m || '_tsTask' in m))
762
+ ? (({ _tsPinned, _tsTask, ...rest }) => rest)(m) : m);
759
763
  return withRetry(async () => {
760
764
  const ctrl = new AbortController();
761
765
  let timer;
@@ -770,7 +774,7 @@ async function llm({ baseUrl, key, messages, model, signalMs = 180000, noTools =
770
774
  ? { 'X-TS-Credit-Budget': String(Math.floor(Number(creditBudget))) }
771
775
  : {}),
772
776
  },
773
- body: JSON.stringify({ model: model || DEFAULT_EXECUTOR, messages,
777
+ body: JSON.stringify({ model: model || DEFAULT_EXECUTOR, messages: wireMessages,
774
778
  ...(Number.isFinite(Number(maxCompletionTokens)) && Number(maxCompletionTokens) > 0
775
779
  ? { max_completion_tokens: Math.max(32, Math.min(Math.floor(Number(maxCompletionTokens)), 16000)) }
776
780
  : {}),
@@ -965,7 +969,7 @@ async function _subAgent({ task, k, model, cwd, lang, onStep }) {
965
969
  let inp = {}; try { inp = JSON.parse((tc.function && tc.function.arguments) || '{}'); } catch (_) {}
966
970
  if (onStep) onStep({ name: ' ↳ ' + nm, detail: argsShort(nm, inp) });
967
971
  const res = READONLY.has(nm) ? await tools.execute(nm, inp, { baseDir: cwd, token: k }) : { erro: 'ferramenta não permitida no sub-agente (só leitura).' };
968
- msgs.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(res).slice(0, TOOL_RESULT_CAP) });
972
+ msgs.push({ role: 'tool', tool_call_id: tc.id, content: intelligence.compactToolResult(res, TOOL_RESULT_CAP) });
969
973
  }
970
974
  }
971
975
  // Esgotou os passos ainda chamando ferramentas? Fecha com UM resumo (sem ferramentas) —
@@ -981,25 +985,19 @@ async function _subAgent({ task, k, model, cwd, lang, onStep }) {
981
985
  return { resumo: out || (lang !== 'en' ? '(o sub-agente não produziu um resumo)' : '(sub-agent produced no summary)'), _tin: tin, _tout: tout, _tcach: tcach, _billed: billed };
982
986
  }
983
987
 
984
- // Aprovação remota via Telegram (modo --yes): envia o pedido e faz poll até
985
- // decisão/expiração. Qualquer falha = não aprovado (fail-safe).
986
- async function _remoteApprove(comando, token, onRemote) {
987
- let st = null;
988
- try { st = await api('/api/cli/approval/start', { method: 'POST', token, body: { comando, host: os.hostname() }, timeoutMs: 20000 }); }
989
- catch (_) { return { approved: false, remoteTried: false }; }
990
- if (!st || !st.success || !st.id) return { approved: false, remoteTried: false }; // sem canal
991
- onRemote({ id: st.id, ttl: st.ttl || 120 });
992
- const t0 = Date.now();
993
- while (Date.now() - t0 < 150000) {
994
- await new Promise(r => setTimeout(r, 3000));
995
- let p = null;
996
- try { p = await api('/api/cli/approval/poll', { method: 'POST', token, body: { id: st.id }, timeoutMs: 15000 }); } catch (_) { continue; }
997
- if (p && p.status === 'approved') return { approved: true, remoteTried: true };
998
- if (p && ['refused', 'expired', 'not_found'].includes(p.status)) return { approved: false, remoteTried: true };
999
- }
1000
- return { approved: false, remoteTried: true };
988
+ // Motivo curto e SEM segredo de uma falha do resumo automático (vai pra tela, pro
989
+ // stream-json e pra ACP): código + mensagem, mascarados e cortados.
990
+ function compactFailReason(e) {
991
+ const code = e && e.code ? String(e.code) : '';
992
+ const msg = String((e && e.message) || e || '').replace(/\s+/g, ' ').trim();
993
+ const txt = code && !msg.includes(code) ? `${code}: ${msg}` : (msg || code);
994
+ return core.redactSecrets(txt || 'erro desconhecido').slice(0, 160);
1001
995
  }
1002
996
 
997
+ // Em --yes não há ninguém no terminal para aprovar: o que exige aprovação é RECUSADO na hora
998
+ // (fail-safe). A aprovação é sempre LOCAL — no terminal (askApprove) ou no editor (ACP).
999
+ // O aviso/aprovação pelo celular virá pelo hub; enquanto isso, nada sai desta máquina.
1000
+
1003
1001
  /**
1004
1002
  * Roda o agente. opts:
1005
1003
  * token (sessão ts) · lang · yes (auto-aprova NÃO-destrutivos; destrutivo recusa)
@@ -1008,9 +1006,14 @@ async function _remoteApprove(comando, token, onRemote) {
1008
1006
  * onStep({name, detail, auto}) — passo iniciado (pra UI; auto=destrutivo aprovado em full-auto)
1009
1007
  * onStepDone({name, ok, status, evidence}) — passo concluído (✓/✗ por etapa)
1010
1008
  * askApprove(cmd|{kind,cmd}) → Promise<boolean|'all'> ('all' liga o full-auto na sessão)
1009
+ * compactFirst — compacta o histórico retomado antes do primeiro passo (sessão perto do limite)
1010
+ * noProactiveCompact — a pessoa escolheu "Continuar assim" na folha de 95%: sem compactação
1011
+ * pelo limiar (só no limite útil ou num 400 de contexto)
1012
+ * Retorno: context = {used, window, usable, measured} da ÚLTIMA chamada; compaction =
1013
+ * {count, ok, reason} (ok=false quando algum resumo automático falhou e a parte antiga se perdeu).
1011
1014
  */
1012
1015
  async function run(task, opts = {}) {
1013
- const { token, lang = 'pt', yes = false, autoAll = false, model = null, confineDir = null, onStep = () => {}, onStepDone = () => {}, askApprove = async () => false, onThinking = () => {}, onRemote = () => {} } = opts;
1016
+ const { token, lang = 'pt', yes = false, autoAll = false, model = null, confineDir = null, onStep = () => {}, onStepDone = () => {}, askApprove = async () => false, onThinking = () => {} } = opts;
1014
1017
  // Costura exclusiva da API programática/testes: permite simular o gateway sem
1015
1018
  // expor uma flag no CLI nem alterar o caminho normal de produção.
1016
1019
  const llmCall = typeof opts.llmFn === 'function' ? opts.llmFn : llm;
@@ -1108,11 +1111,18 @@ async function run(task, opts = {}) {
1108
1111
  // o gate por plano no backend (402 plan_limit → mensagem de upgrade).
1109
1112
  const k = opts.keyOverride || await keyring.resolve(token, { feature: 'cli_agent' });
1110
1113
  if (!k || !k.key) throw new ApiError('ai_key', {});
1114
+ // Segredos desta execução que não estão em ~/.ts (a chave de IA em uso, inclusive a da nuvem):
1115
+ // somam-se aos do ts na máscara das saídas de ferramenta e da aprovação remota.
1116
+ const _runSecrets = [k.key];
1117
+ const _maskOut = (x) => tools.maskKnownSecrets(x, [token, ..._runSecrets]);
1111
1118
  // A chave BYOK ainda é validada antes da primeira chamada. No modo Automático,
1112
1119
  // ela escolhe o provedor, mas os papéis continuam obedecendo ao plano. Para
1113
1120
  // escolher um modelo BYOK específico fora dessa regra, o usuário usa --modelo.
1114
1121
  const erroModeloByok = validarModeloByok(k);
1115
1122
  if (erroModeloByok) throw erroModeloByok;
1123
+ // X-TS-Credit-Budget só faz sentido no gateway do TS (créditos legados). Com chave
1124
+ // própria ou assinatura pessoal, o cabeçalho iria para um provedor de terceiros.
1125
+ const _creditBudget = (n) => (k.source === 'cloud' ? n : null);
1116
1126
  let selectedModel = model;
1117
1127
  // BYOK: o catálogo do roteador é do GATEWAY (ids tipo "deepseek-v4-flash"); o provedor
1118
1128
  // do usuário tem os seus ("deepseek-ai/deepseek-v4-flash") e devolve 404 pro id errado.
@@ -1366,13 +1376,20 @@ async function run(task, opts = {}) {
1366
1376
  { role: 'system', content: systemPrompt(lang, cwd) + _memBlock + _busBlock + _interopBlock + skillsBlock + sugestaoBlock + evolveBlock + _erroBlock + planBlock + strictScopeBlock + _auditBlock + _mcpBlock + _hookCtx + _sharedChannelBlock },
1367
1377
  ...(_projectBrief ? [{ role: 'user', content: _projectBrief }] : []),
1368
1378
  ...(_memoryRecallMessage ? [_memoryRecallMessage] : []),
1369
- { role: 'user', content: taskText },
1379
+ // _tsTask: a tarefa desta execução é FIXA — sobrevive à compactação e aos cortes da sessão
1380
+ { role: 'user', content: taskText, _tsTask: true },
1370
1381
  ];
1371
1382
  // CONTINUAR sessão anterior (ts agente --continuar): reaproveita o histórico, MAS com o system
1372
- // prompt FRESCO (memória/skills atualizadas) + a nova tarefa no fim.
1383
+ // prompt FRESCO (memória/skills atualizadas) + a nova tarefa no fim. O corte preserva o
1384
+ // resumo fixo da compactação anterior e nunca começa numa resposta 'tool' órfã.
1373
1385
  if (Array.isArray(opts.priorMessages) && opts.priorMessages.length) {
1374
- const convo = opts.priorMessages.filter(m => m && m.role && m.role !== 'system').slice(-40);
1375
- messages = [messages[0], ...(_projectBrief ? [{ role: 'user', content: _projectBrief }] : []), ...(_memoryRecallMessage ? [_memoryRecallMessage] : []), ...convo, { role: 'user', content: taskText }];
1386
+ // A tarefa da sessão anterior vira histórico comum (sem a marca): a fixa agora é a nova.
1387
+ // Saídas de ferramenta da sessão salva passam pela máscara dos segredos conhecidos (sessões
1388
+ // gravadas antes dela existir, ou um segredo novo desde então).
1389
+ const convo = core.safeTail(opts.priorMessages.filter(m => m && m.role && m.role !== 'system'), 40)
1390
+ .map(m => (core.isPinnedTask(m) ? (({ _tsTask, ...rest }) => rest)(m) : m))
1391
+ .map(m => (m.role === 'tool' && typeof m.content === 'string' ? Object.assign({}, m, { content: _maskOut(m.content) }) : m));
1392
+ messages = [messages[0], ...(_projectBrief ? [{ role: 'user', content: _projectBrief }] : []), ...(_memoryRecallMessage ? [_memoryRecallMessage] : []), ...convo, { role: 'user', content: taskText, _tsTask: true }];
1376
1393
  }
1377
1394
  const acc = { inTok: 0, outTok: 0, cachedTok: 0 };
1378
1395
  const _roleTrace = [];
@@ -1478,7 +1495,7 @@ async function run(task, opts = {}) {
1478
1495
  try {
1479
1496
  const reply = await llmCall({
1480
1497
  baseUrl: k.baseUrl, key: k.key, messages, model: selectedModel,
1481
- toolsOverride: mainTools, creditBudget: Math.max(1, Math.min(maxCredits - charged - _visionCredits, _phaseBudgets.execution.credits - _phaseUsage.execution.credits)),
1498
+ toolsOverride: mainTools, creditBudget: _creditBudget(Math.max(1, Math.min(maxCredits - charged - _visionCredits, _phaseBudgets.execution.credits - _phaseUsage.execution.credits))),
1482
1499
  ...extra, maxCompletionTokens: executorCompletionCap(extra.maxCompletionTokens), signalMs, tries: 1,
1483
1500
  });
1484
1501
  if (model && forcedModelMismatch(model, reply && reply.model)) {
@@ -1532,7 +1549,7 @@ async function run(task, opts = {}) {
1532
1549
  onStep({ name: 'agente_' + role, detail: roleModel, phase, model: roleModel });
1533
1550
  const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, model: roleModel, noTools: true, signalMs,
1534
1551
  maxCompletionTokens: role === 'planner' ? 2200 : 1400,
1535
- creditBudget: Math.max(1, Math.min(maxCredits - charged - _visionCredits, _phaseBudgets[phase].credits - _phaseUsage[phase].credits)), tries: 1,
1552
+ creditBudget: _creditBudget(Math.max(1, Math.min(maxCredits - charged - _visionCredits, _phaseBudgets[phase].credits - _phaseUsage[phase].credits))), tries: 1,
1536
1553
  messages: [{ role: 'system', content: contract.prompt + '\n' + system }, { role: 'user', content: user }] });
1537
1554
  if (roleForcedModel && forcedModelMismatch(roleForcedModel, r && r.model)) {
1538
1555
  const err = new ApiError(`Modelo forçado não foi respeitado: solicitado "${roleForcedModel}", recebido "${r.model}". A missão foi interrompida sem fallback.`, { status: 409, code: 'model_substituted' });
@@ -1601,16 +1618,45 @@ async function run(task, opts = {}) {
1601
1618
  });
1602
1619
  } catch (_) {}
1603
1620
  };
1604
- let lastCtx = { used: 0, window: ctxWindow }; // ocupação REAL da janela (prompt_tokens da última chamada)
1621
+ // Ocupação da janela na ÚLTIMA chamada (não a soma do turno). measured=true só quando o
1622
+ // provedor devolveu prompt_tokens; sem isso é a estimativa chars/4 do histórico.
1623
+ // Modo assinatura (personal): o prompt enviado é recortado no cliente (personal-agent-llm,
1624
+ // ~23.800 caracteres) e o usage vem somado do backend — nem o histórico inteiro nem a janela do
1625
+ // modelo do gateway medem o que vai de fato. O medidor fica DESLIGADO (used:0, personal:true):
1626
+ // sem rodapé, sem aviso de 80% e sem a folha de 95%. A compactação segue pela estimativa.
1627
+ const _toolsTok = k.source === 'personal' ? 0 : Math.ceil(JSON.stringify(mainTools || []).length / 4);
1628
+ const _ctxEstimate = () => estMsgsTok(messages) + _toolsTok;
1629
+ const _ctxSnapshot = (used, measured) => (k.source === 'personal'
1630
+ ? { used: 0, window: ctxWindow, usable: core.usableWindow(ctxWindow), measured: false, personal: true }
1631
+ : { used, window: ctxWindow, usable: core.usableWindow(ctxWindow), measured: !!measured });
1632
+ let lastCtx = _ctxSnapshot(0, false);
1633
+ const _progress = (extra = {}) => Object.assign({ inTok: acc.inTok, outTok: acc.outTok, cachedTok: acc.cachedTok, steps, model: usedModel, credits: charged + _visionCredits, elapsedMs: Date.now() - missionStartedAt, context: lastCtx }, extra);
1605
1634
 
1606
1635
  // Compacta o histórico quando cruza o limiar (ou quando force=true após um 400 de
1607
- // contexto): mensagens antigas viram UM resumo denso; as últimas KEEP_TAIL ficam
1608
- // íntegras. Falhou o resumo? Fallback determinístico (descarta com aviso) — nunca lança.
1636
+ // contexto ou a pedido do chamador): mensagens antigas viram UM resumo denso, FIXO no
1637
+ // índice 1; as últimas KEEP_TAIL ficam íntegras. A chamada usa o MESMO llmCall/k da
1638
+ // execução. Falhou o resumo? A parte antiga é descartada, mas a falha fica VISÍVEL:
1639
+ // passo bloqueado com o motivo + out.compaction.ok=false. Nunca lança.
1609
1640
  let _lastCompactLen = 0; // guarda anti-thrash: só recompacta depois do histórico CRESCER
1641
+ let pinnedSummary = null;
1642
+ const _compaction = { count: 0, ok: true, reason: null };
1643
+ const TASK_PIN_MAX = 8000;
1644
+ const _pinnedTaskCopy = (m) => {
1645
+ const c = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '');
1646
+ if (c.length <= TASK_PIN_MAX) return m;
1647
+ return { role: 'user', _tsTask: true, content: c.slice(0, TASK_PIN_MAX) + (lang !== 'en'
1648
+ ? '\n[… pedido cortado aqui para caber no contexto]' : '\n[… request cut here to fit the context]') };
1649
+ };
1610
1650
  async function _compactIfNeeded(force) {
1611
1651
  try {
1612
- const used = estMsgsTok(messages);
1613
- if (!force && used < ctxWindow * COMPACT_AT) return;
1652
+ // gatilho: o maior entre a estimativa (mensagens + esquemas de ferramentas) e o
1653
+ // prompt medido na última chamada — o medido inclui o que a estimativa não vê.
1654
+ const used = Math.max(_ctxEstimate(), Number(lastCtx.used) || 0);
1655
+ // "Continuar assim" na folha de 95% (opts.noProactiveCompact): o limiar proativo não vale
1656
+ // nesta execução — compacta só quando o histórico já não cabe no limite útil (a próxima
1657
+ // chamada estouraria) ou depois de um 400 de contexto (force).
1658
+ const threshold = opts.noProactiveCompact ? core.usableWindow(ctxWindow) : ctxWindow * COMPACT_AT;
1659
+ if (!force && used < threshold) return;
1614
1660
  if (messages.length < KEEP_TAIL + 4) return; // pouco histórico — nada útil a resumir
1615
1661
  // recém-compactado e pouca coisa nova? não compacta de novo (compactar toda iteração
1616
1662
  // destrói a memória de trabalho do agente — ele começa a reler arquivos em loop)
@@ -1619,33 +1665,70 @@ async function run(task, opts = {}) {
1619
1665
  let cut = Math.max(1, messages.length - KEEP_TAIL);
1620
1666
  while (cut < messages.length && messages[cut].role === 'tool') cut++;
1621
1667
  if (cut >= messages.length - 1) return;
1622
- const old = messages.slice(1, cut), tail = messages.slice(cut);
1623
- onStep({ name: 'compactar_contexto', detail: `~${Math.round(used / 1000)}k tok → resumo (${Math.round(100 * used / ctxWindow)}% da janela)` });
1668
+ // o resumo anterior entra INTEIRO (sem o corte de 1500 das mensagens comuns) e é
1669
+ // substituído pelo novo; nunca fica duplicado nem é tratado como conversa.
1670
+ const previous = messages.slice(1).filter(m => core.isPinnedSummary(m)).map(m => core.pinnedSummaryText(m)).filter(Boolean).join('\n\n');
1671
+ const old = messages.slice(1, cut).filter(m => !core.isPinnedSummary(m));
1672
+ const tail = messages.slice(cut).filter(m => !core.isPinnedSummary(m));
1673
+ // a tarefa desta execução entra no bloco (dá o objetivo ao resumo) e, se caiu na parte
1674
+ // antiga, volta logo depois do resumo, FIXA — sem ela o rabo costuma ter só assistant/
1675
+ // tool e o modelo perde a missão. Pedido enorme (pipe, @arquivo) volta cortado.
1676
+ const taskOld = old.find(m => core.isPinnedTask(m)) || null;
1624
1677
  const lines = old.map(m => {
1625
1678
  let c = typeof m.content === 'string' ? m.content : JSON.stringify(m.content || '');
1626
1679
  if (m.role === 'tool') c = c.slice(0, 300);
1627
1680
  if (m.tool_calls) c += ' ' + m.tool_calls.map(t => `[chamou ${(t.function && t.function.name) || '?'}(${String((t.function && t.function.arguments) || '').slice(0, 120)})]`).join(' ');
1628
1681
  return `${m.role}: ${c.slice(0, 1500)}`;
1629
- }).join('\n').slice(0, 60000);
1630
- let resumo = '';
1682
+ }).join('\n');
1683
+ const prevBlock = previous ? (lang !== 'en'
1684
+ ? `RESUMO ANTERIOR (mantenha o que continua válido e atualize com a conversa que veio depois):\n${previous}\n\nCONVERSA DESDE O RESUMO ANTERIOR:\n`
1685
+ : `PREVIOUS SUMMARY (keep what is still valid and update it with the conversation that followed):\n${previous}\n\nCONVERSATION SINCE THE PREVIOUS SUMMARY:\n`) : '';
1686
+ const block = prevBlock + lines.slice(0, Math.max(0, 60000 - prevBlock.length));
1687
+ onThinking(_progress({ compacting: true }));
1688
+ let resumo = '', failReason = null;
1631
1689
  try {
1632
1690
  const sys = lang !== 'en'
1633
1691
  ? 'Resuma a conversa de agente abaixo em UM bloco curto e denso (máx ~300 palavras): objetivo, o que já foi feito (arquivos/comandos e resultados), decisões tomadas e o que falta. Preserve caminhos de arquivos e fatos técnicos EXATOS. Sem preâmbulo.'
1634
1692
  : 'Summarize the agent conversation below into ONE short dense block (max ~300 words): goal, what was done (files/commands and results), decisions, and what remains. Preserve EXACT file paths and technical facts. No preamble.';
1635
- const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, model: selectedModel, noTools: true, signalMs: 60000, maxCompletionTokens: 900, creditBudget: Math.max(1, maxCredits - charged - _visionCredits), messages: [{ role: 'system', content: sys }, { role: 'user', content: lines }] });
1693
+ // purpose:'compact' — o modo pessoal manda o bloco inteiro (não o recorte de 8 mensagens).
1694
+ const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, model: selectedModel, noTools: true, purpose: 'compact',
1695
+ signalMs: opts.personalSubscription ? 120000 : 60000, maxCompletionTokens: 900,
1696
+ creditBudget: _creditBudget(Math.max(1, maxCredits - charged - _visionCredits)),
1697
+ messages: [{ role: 'system', content: sys }, { role: 'user', content: block }] });
1636
1698
  const u = r.usage || {};
1637
1699
  acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
1638
1700
  charged += (r.billing && r.billing.charged) || 0;
1639
- resumo = String(r.msg.content || '').replace(/<think>[\s\S]*?<\/think>/gi, '').trim().slice(0, 6000);
1640
- } catch (_) {}
1641
- if (!resumo) resumo = lang !== 'en'
1642
- ? '(resumo automático indisponível — a parte antiga do histórico foi descartada por limite de contexto)'
1643
- : '(auto-summary unavailable — older history was dropped due to the context limit)';
1644
- const label = lang !== 'en'
1645
- ? 'RESUMO DA CONVERSA ATÉ AQUI (contexto antigo, condensado automaticamente):\n'
1646
- : 'CONVERSATION SUMMARY SO FAR (older context, auto-condensed):\n';
1647
- messages = [messages[0], { role: 'user', content: label + resumo }, ...tail];
1701
+ resumo = String((r.msg && r.msg.content) || '').replace(/<think>[\s\S]*?<\/think>/gi, '').trim().slice(0, 6000);
1702
+ if (!resumo) failReason = lang !== 'en' ? 'o modelo devolveu um resumo vazio' : 'the model returned an empty summary';
1703
+ } catch (e) { failReason = compactFailReason(e); }
1704
+ _compaction.count += 1;
1705
+ const pctTxt = `~${Math.round(used / 1000)}k tok (${Math.round(100 * used / ctxWindow)}% ${lang !== 'en' ? 'da janela' : 'of the window'})`;
1706
+ if (!resumo) {
1707
+ _compaction.ok = false;
1708
+ _compaction.reason = failReason;
1709
+ // sem resumo novo, o anterior (se havia) continua valendo — só a parte nova se perde
1710
+ const lostNote = lang !== 'en'
1711
+ ? '(o resumo automático da parte seguinte falhou; essa parte do histórico foi descartada por limite de contexto)'
1712
+ : '(the auto-summary of the following part failed; that part of the history was dropped due to the context limit)';
1713
+ resumo = previous
1714
+ ? (previous.includes(lostNote) ? previous : previous + '\n\n' + lostNote)
1715
+ : (lang !== 'en'
1716
+ ? '(resumo automático indisponível — a parte antiga do histórico foi descartada por limite de contexto)'
1717
+ : '(auto-summary unavailable — older history was dropped due to the context limit)');
1718
+ // UM evento só, já como bloqueado: a ACP o abre como 'failed' e o stream-json como
1719
+ // 'blocked' — nada de 'completed' seguido de falha para o mesmo passo.
1720
+ onStep({ name: 'compactar_contexto', blocked: true, detail: lang !== 'en'
1721
+ ? `resumo falhou (${failReason}); parte antiga descartada · ${pctTxt}`
1722
+ : `summary failed (${failReason}); older part dropped · ${pctTxt}` });
1723
+ } else {
1724
+ onStep({ name: 'compactar_contexto', detail: `${pctTxt} → ${lang !== 'en' ? 'resumo' : 'summary'}` });
1725
+ }
1726
+ pinnedSummary = { role: 'user', content: (lang !== 'en' ? core.SUMMARY_LABEL.pt : core.SUMMARY_LABEL.en) + resumo, _tsPinned: true };
1727
+ messages = [messages[0], pinnedSummary, ...(taskOld ? [_pinnedTaskCopy(taskOld)] : []), ...tail];
1648
1728
  _lastCompactLen = messages.length;
1729
+ // o medido da última chamada ficou velho: até a próxima chamada vale a estimativa
1730
+ lastCtx = _ctxSnapshot(_ctxEstimate(), false);
1731
+ onThinking(_progress({ compacting: false }));
1649
1732
  } catch (_) { /* compactação é best-effort — jamais derruba a run */ }
1650
1733
  }
1651
1734
  // cobrança Smart Credits (best-effort — o teto da sk-hub já protege no gateway)
@@ -1672,7 +1755,7 @@ async function run(task, opts = {}) {
1672
1755
  _planDecision = preflight;
1673
1756
  finalText = `Preciso de uma decisão sua antes de continuar: ${_planDecision.question}`;
1674
1757
  _logEp('human');
1675
- return { text: finalText, steps, credits: 0, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx,
1758
+ return { text: finalText, steps, credits: 0, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, compaction: Object.assign({}, _compaction),
1676
1759
  needHuman: { motivo: _planDecision.reason, o_que_fazer: _planDecision.question },
1677
1760
  orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace }, completion: { ok: false, reason: 'human_decision' }, missionCache: _missionCache.stats() };
1678
1761
  }
@@ -1702,7 +1785,7 @@ async function run(task, opts = {}) {
1702
1785
  finalText = `Preciso de uma decisão sua antes de continuar: ${_planDecision.question}`;
1703
1786
  await _bill();
1704
1787
  _logEp('human');
1705
- return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx,
1788
+ return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, compaction: Object.assign({}, _compaction),
1706
1789
  needHuman: { motivo: _planDecision.reason || 'Falta uma escolha material para executar com segurança.', o_que_fazer: _planDecision.question },
1707
1790
  orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace }, completion: { ok: false, reason: 'human_decision' }, missionCache: _missionCache.stats() };
1708
1791
  }
@@ -1725,8 +1808,10 @@ async function run(task, opts = {}) {
1725
1808
  guardStopped = _guard();
1726
1809
  if (guardStopped) break;
1727
1810
  // passa o snapshot de progresso pra a status line ao vivo (tempo é contado no cliente)
1728
- onThinking({ iter, inTok: acc.inTok, outTok: acc.outTok, cachedTok: acc.cachedTok, steps, model: usedModel, credits: charged + _visionCredits, elapsedMs: Date.now() - missionStartedAt });
1729
- await _compactIfNeeded(false); // proativo: compacta ao cruzar ~65% da janela
1811
+ onThinking(_progress({ iter }));
1812
+ // proativo: compacta ao cruzar ~65% da janela. compactFirst = o chamador decidiu compactar
1813
+ // antes do primeiro passo (sessão retomada já perto do limite útil, item 22).
1814
+ await _compactIfNeeded(iter === 0 && !!opts.compactFirst);
1730
1815
  let r;
1731
1816
  // gateway oscilou → mostra "reconectando" em vez de morrer calado (confiabilidade visível)
1732
1817
  const _onGwRetry = (e) => onStep({ name: 'gateway', detail: (lang !== 'en' ? 'reconectando ' : 'reconnecting ') + e.attempt + '/' + e.tries + ' (' + e.reason + ')', retry: true });
@@ -1758,7 +1843,10 @@ async function run(task, opts = {}) {
1758
1843
  _phaseUsage.execution.tokens += (u.prompt_tokens || 0) + (u.completion_tokens || 0);
1759
1844
  _phaseUsage.execution.attempts += 1;
1760
1845
  acc.cachedTok += (u.prompt_tokens_details && u.prompt_tokens_details.cached_tokens) || u.cached_tokens || 0;
1761
- lastCtx = { used: u.prompt_tokens || estMsgsTok(messages), window: ctxWindow };
1846
+ // modo pessoal: o usage do backend vem somado no turno (Codex) ou sem cache (Claude) e o
1847
+ // prompt é recortado no cliente — não serve como medida; fica a estimativa do histórico.
1848
+ const _measured = k.source !== 'personal' && Number(u.prompt_tokens) > 0;
1849
+ lastCtx = _ctxSnapshot(_measured ? Number(u.prompt_tokens) : _ctxEstimate(), _measured);
1762
1850
  if (r.model) usedModel = r.model;
1763
1851
 
1764
1852
  const tcs = r.msg.tool_calls || [];
@@ -1919,8 +2007,8 @@ async function run(task, opts = {}) {
1919
2007
 
1920
2008
  // ENFORCEMENT do modo só-leitura PRIMEIRO: bloqueia ferramentas que alteram/rodam ANTES de
1921
2009
  // qualquer efeito colateral. Não basta OMITIR a ferramenta (o modelo pode chamá-la mesmo
1922
- // assim); e se o gate destrutivo abaixo rodasse primeiro, chegaria a pedir aprovação (até no
1923
- // Telegram, em --yes) por um comando que jamais executaria.
2010
+ // assim); e se o gate destrutivo abaixo rodasse primeiro, chegaria a pedir aprovação
2011
+ // por um comando que jamais executaria.
1924
2012
  // (tools MCP read-only — needsApproval=false — são permitidas em roMode; as mutantes não)
1925
2013
  if (roMode && !READONLY.has(name) && name !== 'explorar' && !(_mcp.route[name] && !_mcp.route[name].needsApproval)) {
1926
2014
  onStep({ name, detail: argsShort(name, input), blocked: true });
@@ -1930,8 +2018,8 @@ async function run(task, opts = {}) {
1930
2018
  }
1931
2019
  // RECUSA INSTANTÂNEA de AUTO-DESTRUTIVOS (vem ANTES do gate de aprovação): comandos que
1932
2020
  // matam a própria missão (o processo do agente / o dev server que ela usa) ou a máquina
1933
- // são recusados NA HORA — sem askApprove, sem poll remoto no Telegram (~150s POR tentativa
1934
- // desperdiçados no incidente do jogo, em que o modelo insistia em "taskkill /f /im node").
2021
+ // são recusados NA HORA — sem askApprove (antes, a espera da aprovação desperdiçava ~150s POR
2022
+ // tentativa no incidente do jogo, em que o modelo insistia em "taskkill /f /im node").
1935
2023
  // Só o comando LOCAL (executar_comando roda NESTE processo/cwd); o remoto tem outro pid/cwd
1936
2024
  // e segue pelo gate de aprovação comum.
1937
2025
  if (result === undefined && name === 'executar_comando') {
@@ -1948,6 +2036,12 @@ async function run(task, opts = {}) {
1948
2036
  const sd = tools.selfDestructiveReason(String(input.comando || ''), { cwd });
1949
2037
  if (sd) { onStep({ name, detail: argsShort(name, input), blocked: true }); result = { erro: sd }; }
1950
2038
  }
2039
+ // Credenciais do ts (~/.ts/config.json, providers.json, mcp*, sessions, agente) lidas por
2040
+ // shell: recusa como o ler_arquivo — nem aprovação nem --yolo liberam, o conteúdo iria pro modelo.
2041
+ if (result === undefined && name === 'executar_comando') {
2042
+ const tr = tools.tsSecretReadReason(String(input.comando || ''), { cwd });
2043
+ if (tr) { onStep({ name, detail: argsShort(name, input), blocked: true }); result = { erro: tr }; }
2044
+ }
1951
2045
  // Defesa no dispatcher também cobre executar_remoto; o executor local tem
1952
2046
  // a mesma checagem para que nenhum chamador consiga contorná-la.
1953
2047
  if (result === undefined && ['executar_comando', 'executar_remoto'].includes(name)) {
@@ -1989,22 +2083,22 @@ async function run(task, opts = {}) {
1989
2083
  // explícita: full-auto não publica em produção silenciosamente.
1990
2084
  if (result === undefined && name === 'executar_remoto' && isRemoteDeployCommand(input.comando)) {
1991
2085
  const label = `DEPLOY REMOTO → ${String(input.comando || '').slice(0, 1200)}`;
1992
- let approved = false, remoteTried = false;
2086
+ let approved = false;
1993
2087
  if (!yes) {
1994
2088
  const dec = await askApprove({ kind: 'deploy', cmd: label, warning: 'Este comando pode publicar, reiniciar ou alterar o ambiente remoto.' });
1995
2089
  approved = decideApproval({ autoAll: false, decision: dec }).approved;
1996
- } else ({ approved, remoteTried } = await _remoteApprove(label, token, onRemote));
2090
+ }
1997
2091
  if (!approved) {
1998
2092
  result = { erro: yes
1999
- ? (remoteTried ? 'RECUSADO: o dono negou ou não respondeu ao deploy remoto.' : 'RECUSADO: deploy remoto exige aprovação remota no modo --yes.')
2093
+ ? 'RECUSADO automaticamente: deploy remoto exige a aprovação de quem está no terminal, e em --yes não há quem aprove. Explique ao usuário e sugira rodar interativamente.'
2000
2094
  : 'O usuário recusou o deploy remoto. Não publique nem reinicie o ambiente.' };
2001
2095
  onStep({ name, detail: argsShort(name, input), blocked: true });
2002
2096
  }
2003
2097
  }
2004
2098
  // Gate de segurança: destrutivo → humano decide (só se ainda não foi bloqueado acima).
2005
- // Interativo pergunta no terminal; em --yes (cron) tenta APROVAÇÃO REMOTA no Telegram do dono.
2099
+ // Interativo pergunta no terminal; em --yes (cron) não há quem aprove: recusa.
2006
2100
  if (result === undefined && !_polAllow && (name === 'executar_comando' || name === 'executar_remoto') && tools.isDestructive(input.comando)) {
2007
- let approved = false, remoteTried = false;
2101
+ let approved = false;
2008
2102
  if (autoApproveDestructive) {
2009
2103
  // FULL-AUTO não abre mão do anti-catástrofe: os auto-destrutivos de máquina/processo/
2010
2104
  // pasta já caíram no gate acima (selfDestructiveReason). O que sobra é ~/.ts por shell
@@ -2026,13 +2120,11 @@ async function run(task, opts = {}) {
2026
2120
  const dec = await askApprove({ kind: 'destructive', cmd: _cmd, warning: core.globalStateWarning(_cmd) });
2027
2121
  const d = decideApproval({ autoAll: false, decision: dec });
2028
2122
  approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
2029
- } else ({ approved, remoteTried } = await _remoteApprove(String(input.comando || ''), token, onRemote));
2123
+ }
2030
2124
  // result === undefined evita sobrescrever a mensagem específica do bloqueio ~/.ts do full-auto
2031
2125
  if (!approved && result === undefined) {
2032
2126
  result = { erro: yes
2033
- ? (remoteTried
2034
- ? 'RECUSADO: o dono NEGOU (ou não respondeu em 2 min) a aprovação remota deste comando destrutivo. Não repita; siga sem ele e relate no resumo.'
2035
- : 'RECUSADO automaticamente: comando destrutivo não roda em modo --yes (sem Telegram vinculado pra aprovação remota). Explique ao usuário e sugira rodar interativamente ou vincular o Telegram.')
2127
+ ? 'RECUSADO automaticamente: comando destrutivo não roda em modo --yes (não há quem aprove no terminal). Não repita; siga sem ele, relate no resumo e sugira rodar interativamente (ou com --yolo, se o dono quiser aprovar tudo).'
2036
2128
  : 'O usuário RECUSOU este comando destrutivo. Não repita; proponha uma alternativa segura ou explique o que seria necessário.' };
2037
2129
  onStep({ name, detail: argsShort(name, input), blocked: true });
2038
2130
  }
@@ -2046,14 +2138,14 @@ async function run(task, opts = {}) {
2046
2138
  const payload = JSON.stringify(input || {});
2047
2139
  const provider = name.includes('outlook') || name.includes('microsoft365') ? 'MICROSOFT 365' : 'GOOGLE WORKSPACE';
2048
2140
  const label = `${provider} → ${name}: ${payload.length > 1500 ? payload.slice(0, 1500) + ` …(+${payload.length - 1500} chars)` : payload}`;
2049
- let approved = false, remoteTried = false;
2141
+ let approved = false;
2050
2142
  if (!yes) {
2051
2143
  const dec = await askApprove({ kind: 'cloud_mutation', cmd: label, warning: name.includes('enviar_') ? 'O e-mail será enviado ao destinatário.' : `Dados da conta ${provider} serão alterados.` });
2052
2144
  approved = decideApproval({ autoAll: false, decision: dec }).approved;
2053
- } else ({ approved, remoteTried } = await _remoteApprove(label, token, onRemote));
2145
+ }
2054
2146
  if (!approved) {
2055
2147
  result = { erro: yes
2056
- ? (remoteTried ? `RECUSADO: o dono negou ou não respondeu à alteração ${provider}.` : `RECUSADO: alteração ${provider} exige aprovação remota no modo --yes.`)
2148
+ ? `RECUSADO automaticamente: alteração ${provider} exige a aprovação de quem está no terminal, e em --yes não há quem aprove.`
2057
2149
  : `O usuário recusou a alteração na conta ${provider}. Não repita nesta sessão.` };
2058
2150
  onStep({ name, detail: argsShort(name, input), blocked: true });
2059
2151
  }
@@ -2067,7 +2159,7 @@ async function run(task, opts = {}) {
2067
2159
  ? String(input.caminho || '')
2068
2160
  : JSON.stringify(input || {}).slice(0, 600);
2069
2161
  const label = `DISPOSITIVO Android → ${name}: ${safe}`;
2070
- let approved = false, remoteTried = false;
2162
+ let approved = false;
2071
2163
  if (autoApproveDestructive) {
2072
2164
  approved = true;
2073
2165
  onStep({ name, detail: argsShort(name, input), auto: true });
@@ -2076,14 +2168,10 @@ async function run(task, opts = {}) {
2076
2168
  const d = decideApproval({ autoAll: false, decision: dec });
2077
2169
  approved = d.approved;
2078
2170
  if (d.enableAll) autoApproveDestructive = true;
2079
- } else {
2080
- ({ approved, remoteTried } = await _remoteApprove(label, token, onRemote));
2081
2171
  }
2082
2172
  if (!approved) {
2083
2173
  result = { erro: yes
2084
- ? (remoteTried
2085
- ? 'RECUSADO: o dono negou ou não respondeu à ação no dispositivo.'
2086
- : 'RECUSADO automaticamente: ação em dispositivo físico exige aprovação remota.')
2174
+ ? 'RECUSADO automaticamente: ação em dispositivo físico exige a aprovação de quem está no terminal, e em --yes não há quem aprove.'
2087
2175
  : 'O usuário recusou a ação no dispositivo. Não repita nesta sessão.' };
2088
2176
  onStep({ name, detail: argsShort(name, input), blocked: true });
2089
2177
  }
@@ -2095,14 +2183,14 @@ async function run(task, opts = {}) {
2095
2183
  if (result === undefined && name === 'navegador') {
2096
2184
  const browserRequest = browserApprovalRequest(input);
2097
2185
  if (browserRequest) {
2098
- let approved = false, remoteTried = false;
2186
+ let approved = false;
2099
2187
  if (!yes) {
2100
2188
  const dec = await askApprove(browserRequest);
2101
2189
  approved = decideApproval({ autoAll: false, decision: dec }).approved;
2102
- } else ({ approved, remoteTried } = await _remoteApprove(browserRequest.cmd, token, onRemote));
2190
+ }
2103
2191
  if (!approved) {
2104
2192
  result = { erro: yes
2105
- ? (remoteTried ? 'RECUSADO: o dono negou ou não respondeu à ação no navegador.' : 'RECUSADO: ação no navegador exige aprovação remota no modo --yes.')
2193
+ ? 'RECUSADO automaticamente: ação no navegador exige a aprovação de quem está no terminal, e em --yes não há quem aprove.'
2106
2194
  : 'O usuário recusou a ação no navegador. A página continua apenas em modo de leitura.' };
2107
2195
  onStep({ name: 'navegador', detail: browserRequest.cmd, blocked: true });
2108
2196
  }
@@ -2140,22 +2228,22 @@ async function run(task, opts = {}) {
2140
2228
  }
2141
2229
  }
2142
2230
  // Gate de aprovação MCP: tool mutante (annotations/verbo de escrita) → humano decide,
2143
- // mesmo fluxo do destrutivo (interativo pergunta; --yes tenta o Telegram).
2231
+ // mesmo fluxo do destrutivo (interativo pergunta; --yes recusa).
2144
2232
  if (result === undefined && _mcp.route[name] && _mcp.route[name].needsApproval) {
2145
2233
  // input COMPLETO no label (nunca esconde payload atrás de truncamento — o gate nativo
2146
2234
  // mostra o comando inteiro; o MCP faz igual, só marca quantos chars quando é enorme).
2147
2235
  const _js = JSON.stringify(input);
2148
2236
  const _lbl = 'MCP ' + _mcp.route[name].server + ' → ' + _mcp.route[name].realName + ' ' + (_js.length > 1500 ? _js.slice(0, 1500) + ' …(+' + (_js.length - 1500) + ' chars)' : _js);
2149
- let approved = false, remoteTried = false;
2237
+ let approved = false;
2150
2238
  if (autoApproveDestructive) { approved = true; onStep({ name, detail: argsShort(name, input), auto: true }); }
2151
2239
  else if (!yes) {
2152
2240
  const dec = await askApprove(_lbl);
2153
2241
  const d = decideApproval({ autoAll: false, decision: dec });
2154
2242
  approved = d.approved; if (d.enableAll) autoApproveDestructive = true;
2155
- } else ({ approved, remoteTried } = await _remoteApprove(_lbl, token, onRemote));
2243
+ }
2156
2244
  if (!approved) {
2157
2245
  result = { erro: yes
2158
- ? (remoteTried ? 'RECUSADO: o dono NEGOU (ou não respondeu) a aprovação remota desta ferramenta MCP mutante. Não repita; siga sem ela.' : 'RECUSADO automaticamente: ferramenta MCP mutante não roda em --yes sem aprovação remota (Telegram). Siga sem ela.')
2246
+ ? 'RECUSADO automaticamente: ferramenta MCP mutante não roda em --yes (não há quem aprove no terminal). Siga sem ela.'
2159
2247
  : 'O usuário RECUSOU esta ferramenta MCP. Não repita; proponha uma alternativa.' };
2160
2248
  onStep({ name, detail: argsShort(name, input), blocked: true });
2161
2249
  }
@@ -2302,6 +2390,8 @@ async function run(task, opts = {}) {
2302
2390
  if (result === undefined) {
2303
2391
  onStep({ name, detail: argsShort(name, input) });
2304
2392
  result = await tools.execute(name, input, { confineDir, baseDir: cwd, token, allowedTools: _allowedToolNames, trustedReadPaths: _trustedSkillPaths });
2393
+ // o executor já mascara os segredos do ts; aqui entra também a chave de IA da execução
2394
+ result = _maskOut(result);
2305
2395
  steps++; _ran = true;
2306
2396
  // TOOLRESULT TIPADO (core.classifyToolResult): classe de erro DETERMINÍSTICA. A verdade
2307
2397
  // sobre "deu certo?" vem daqui, não da narrativa do modelo. 'blocked' = política (gate),
@@ -2368,7 +2458,7 @@ async function run(task, opts = {}) {
2368
2458
  const _cacheClass = core.classifyToolResult(result);
2369
2459
  if (_cacheClass.ok && READONLY.has(name)) {
2370
2460
  const _resultCap = name === 'ler_arquivos' ? 24000 : TOOL_RESULT_CAP;
2371
- _missionCache.remember(name, input, { toolCallId: tc.id, content: JSON.stringify(result).slice(0, _resultCap) });
2461
+ _missionCache.remember(name, input, { toolCallId: tc.id, content: intelligence.compactToolResult(result, _resultCap) });
2372
2462
  } else if (_cacheClass.ok && !READONLY.has(name)) {
2373
2463
  _missionCache.invalidate();
2374
2464
  }
@@ -2383,6 +2473,9 @@ async function run(task, opts = {}) {
2383
2473
  && _researchCalls >= Math.ceil(RESEARCH_MAX * 2 / 3) && _researchCalls <= RESEARCH_MAX) {
2384
2474
  result = Object.assign({}, result, { _aviso: 'Você já pesquisou ' + _researchCalls + 'x (limite ' + RESEARCH_MAX + '). Se já tem o suficiente, PARE de pesquisar e ENTREGUE o resultado agora.' });
2385
2475
  }
2476
+ // REDE DE SEGURANÇA por valor (tools.maskKnownSecrets): o que sai de MCP, navegador, sub-agente
2477
+ // e das mensagens de bloqueio passa pela mesma máscara antes do status, do log e do modelo.
2478
+ result = _maskOut(result);
2386
2479
  // STATUS POR ETAPA (UX): ✓ verde / ✗ vermelho por passo, a partir do ToolResult TIPADO
2387
2480
  // (core.classifyToolResult) — o usuário vê na hora o que deu certo/errado (antes as ⚙
2388
2481
  // não indicavam resultado nenhum). Só para passos que REALMENTE rodaram (não gate/bloqueio).
@@ -2390,12 +2483,18 @@ async function run(task, opts = {}) {
2390
2483
  // BLOQUEIO HUMANO: o agente pediu uma ação externa que só o usuário faz →
2391
2484
  // encerra o turno devolvendo o pedido; a missão pausa e chama o usuário.
2392
2485
  if (result && result._needHuman) {
2486
+ // Sessão retomável (--continuar "feito, continua"): devolve o histórico, com resposta a
2487
+ // ESTA chamada (o pedido ao humano) e às que sobraram no lote (não executadas) — um
2488
+ // tool_call sem resposta faria o provedor recusar a retomada com 400.
2489
+ messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify({ aguardando_humano: true, motivo: result.motivo, o_que_fazer: result.o_que_fazer }) });
2490
+ for (const rest of tcs.slice(tcs.indexOf(tc) + 1)) messages.push({ role: 'tool', tool_call_id: rest.id, content: JSON.stringify({ erro: 'Não executado: o agente parou para pedir uma decisão humana.' }) });
2393
2491
  await _bill();
2394
2492
  _logEp('human');
2395
- return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, cwd, context: lastCtx, needHuman: { motivo: result.motivo, o_que_fazer: result.o_que_fazer }, orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace } };
2493
+ return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, compaction: Object.assign({}, _compaction), needHuman: { motivo: result.motivo, o_que_fazer: result.o_que_fazer }, orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace } };
2396
2494
  }
2397
2495
  const _resultCap = name === 'ler_arquivos' ? 24000 : TOOL_RESULT_CAP;
2398
- const _rawToolContent = _cachedContent || JSON.stringify(result).slice(0, _resultCap);
2496
+ // Era .slice() no JSON: cortava o FIM (onde está o erro) e entregava JSON quebrado.
2497
+ const _rawToolContent = _maskOut(_cachedContent || intelligence.compactToolResult(result, _resultCap));
2399
2498
  messages.push({ role: 'tool', tool_call_id: tc.id, content: isUntrustedToolOutput(name)
2400
2499
  ? untrustedToolEnvelope(name, _rawToolContent, lang)
2401
2500
  : _rawToolContent });
@@ -2427,8 +2526,8 @@ async function run(task, opts = {}) {
2427
2526
  ? `Interrompi esta missão porque ela atingiu ${labels[guardStopped.kind] || 'o limite de segurança'}. Foram executados ${steps} passo(s) e consumidos ${charged + _visionCredits} crédito(s). O que já foi concluído permanece válido; a etapa que falhou não foi declarada como pronta. Revise o último erro antes de continuar ou aumente o orçamento explicitamente.`
2428
2527
  : `I stopped this mission because it reached ${labels[guardStopped.kind] || 'the safety limit'}. ${steps} step(s) ran and ${charged + _visionCredits} credit(s) were consumed. Completed work remains valid; the failed step was not reported as done. Review the last error before continuing or explicitly raise the budget.`;
2429
2528
  if (guardStopped.kind === 'credits' && guardStopped.required) finalText += lang !== 'en'
2430
- ? ` A próxima chamada precisava de ${guardStopped.required} crédito(s), mas havia ${guardStopped.available} disponível(is). Retome com --max-creditos ${suggestedBudget} ou mais.`
2431
- : ` The next call required ${guardStopped.required} credit(s), but ${guardStopped.available} were available. Resume with --max-creditos ${suggestedBudget} or more.`;
2529
+ ? ` A próxima chamada precisava de ${guardStopped.required} crédito(s), mas havia ${guardStopped.available} disponível(is). Retome com --orcamento ${suggestedBudget} ou mais (créditos legados da IA do Terminal Smart).`
2530
+ : ` The next call required ${guardStopped.required} credit(s), but ${guardStopped.available} were available. Resume with --orcamento ${suggestedBudget} or more (Terminal Smart AI legacy credits).`;
2432
2531
  }
2433
2532
  // Se as ferramentas e os critérios já provaram a entrega, não faça mais uma
2434
2533
  // chamada ao provedor apenas para reescrever o resumo. Além de custar, essa
@@ -2450,7 +2549,7 @@ async function run(task, opts = {}) {
2450
2549
  try {
2451
2550
  const signalMs = modelCallWindow({ maxDurationMs, elapsedMs: Date.now() - missionStartedAt, capMs: modelCallCap(opts.personalSubscription, _complexTask) });
2452
2551
  if (!signalMs) throw new ApiError('model_timeout', { code: 'timeout' });
2453
- const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, messages: [...messages, { role: 'user', content: pedido }], model: selectedModel, noTools: true, signalMs, maxCompletionTokens: 900, tries: 1, creditBudget: Math.max(1, maxCredits - charged - _visionCredits) });
2552
+ const r = await llmCall({ baseUrl: k.baseUrl, key: k.key, messages: [...messages, { role: 'user', content: pedido }], model: selectedModel, noTools: true, signalMs, maxCompletionTokens: 900, tries: 1, creditBudget: _creditBudget(Math.max(1, maxCredits - charged - _visionCredits)) });
2454
2553
  const u = r.usage || {};
2455
2554
  acc.inTok += u.prompt_tokens || 0; acc.outTok += u.completion_tokens || 0;
2456
2555
  charged += (r.billing && r.billing.charged) || 0;
@@ -2573,7 +2672,7 @@ async function run(task, opts = {}) {
2573
2672
  }));
2574
2673
  await Promise.all(_syncTasks);
2575
2674
  } catch (_) {}
2576
- return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, toolErrors: _toolErrs, lastToolError: _lastErr, guard: guardStopped, completion, verification: verifyReport, observability: _observability, report: _finalReport, orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace, phaseBudgets: _phaseBudgets, phaseUsage: _phaseUsage }, missionCache: _missionCache.stats() };
2675
+ return { text: finalText, steps, credits: charged + _visionCredits, tokens: acc, model: usedModel, actions, messages, cwd, context: lastCtx, compaction: Object.assign({}, _compaction), toolErrors: _toolErrs, lastToolError: _lastErr, guard: guardStopped, completion, verification: verifyReport, observability: _observability, report: _finalReport, orchestration: { plan: _planDecision, roles: _roleTrace, inspections: _inspectionTrace, phaseBudgets: _phaseBudgets, phaseUsage: _phaseUsage }, missionCache: _missionCache.stats() };
2577
2676
  }
2578
2677
 
2579
- module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, independentInspectorDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, powershellFileChainIssue, reconcileBillingClaims, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizeFileToolInput, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, modelCallCap, explicitExclusiveFileTargets, executorFallbackChain, forcedModelMismatch, isTransientModelError, executorCompletionCap, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand, browserApprovalRequest } };
2678
+ module.exports = { run, llm, _test: { systemPrompt, projectBrief, inspectionGateDecision, independentInspectorDecision, candidateAdmitsIncomplete, isInspectionCommand, parseStageJson, powershellFileChainIssue, reconcileBillingClaims, materialDecisionPreflight, actionExpectedForTask, requiresActionEvidence, shouldRunPlanner, isComplexTask, validatePlannerDecision, planUpgradeLimit, commandRecoveryHint, windowsUnsupportedUnixCommand, mutationBatchConflicts, normalizeFileToolInput, normalizePlannerDecision, normalizeInspectorDecision, winFor, estMsgsTok, COMPACT_AT, KEEP_TAIL, loopSig, isCycle, loopDecision, decideApproval, failureFingerprint, missionGuardDecision, missionBudgetSuggestion, missionTokenCap, missionTimeCap, modelCallWindow, modelCallCap, explicitExclusiveFileTargets, executorFallbackChain, forcedModelMismatch, isTransientModelError, executorCompletionCap, completionGateDecision, canCloseFromProofs, taskScopedToolDefs, explicitTaskWorkdir, restrictedGatewayDecision, validarModeloByok, scopeToolDefs, parseTextToolCalls, isUntrustedToolOutput, untrustedToolEnvelope, isRemoteDeployCommand, browserApprovalRequest, compactFailReason } };