@openlife/cli 1.7.10 → 1.7.12

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.
@@ -0,0 +1,178 @@
1
+ "use strict";
2
+ // src/cli/ProviderSelector.ts
3
+ // Reusable arrow-key radio menu — used by ConfigTui and any TUI that
4
+ // needs a "pick one of N options" interaction.
5
+ //
6
+ // Matches the Hermes Agent provider-selector UX:
7
+ // - ↑↓ navigate
8
+ // - ENTER / SPACE select
9
+ // - ESC cancel
10
+ // - currently-active option marked with `(•)` and `← currently active`
11
+ // - other options marked with `(o)`
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.selectOne = selectOne;
47
+ exports.numberedChoice = numberedChoice;
48
+ const readline = __importStar(require("readline"));
49
+ const MatrixTheme_1 = require("./MatrixTheme");
50
+ /**
51
+ * Show the menu, await user input, return the selected value (or null on ESC).
52
+ *
53
+ * In non-TTY contexts (CI, piped), prints the menu once and returns
54
+ * `options[initialIndex].value` without blocking — keeps scripts working.
55
+ */
56
+ async function selectOne(opts) {
57
+ const activeIdx = opts.options.findIndex(o => o.value === opts.active);
58
+ let cursor = opts.initialIndex ?? (activeIdx >= 0 ? activeIdx : 0);
59
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
60
+ process.stdout.write(renderMenu(opts.title, opts.options, cursor, opts.active));
61
+ return opts.options[cursor]?.value ?? null;
62
+ }
63
+ return new Promise((resolve) => {
64
+ const previousRaw = process.stdin.isRaw === true;
65
+ try {
66
+ readline.emitKeypressEvents(process.stdin);
67
+ process.stdin.setRawMode(true);
68
+ }
69
+ catch {
70
+ // Non-TTY edge case — fall back to first option.
71
+ resolve(opts.options[cursor]?.value ?? null);
72
+ return;
73
+ }
74
+ process.stdout.write(MatrixTheme_1.ANSI.hideCursor);
75
+ let firstDraw = true;
76
+ const draw = () => {
77
+ const block = renderMenu(opts.title, opts.options, cursor, opts.active);
78
+ if (!firstDraw) {
79
+ // Move cursor up by the number of lines previously printed so we
80
+ // overwrite the menu in place rather than scroll.
81
+ const lineCount = block.split('\n').length;
82
+ process.stdout.write(`\x1b[${lineCount}A`);
83
+ }
84
+ firstDraw = false;
85
+ process.stdout.write(block);
86
+ };
87
+ const cleanup = () => {
88
+ process.stdin.removeListener('keypress', onKey);
89
+ try {
90
+ process.stdin.setRawMode(previousRaw);
91
+ }
92
+ catch { /* ignore */ }
93
+ process.stdout.write(MatrixTheme_1.ANSI.showCursor);
94
+ process.stdout.write(MatrixTheme_1.ANSI.reset);
95
+ };
96
+ const onKey = (_str, key) => {
97
+ if (!key)
98
+ return;
99
+ if (key.ctrl && key.name === 'c') {
100
+ cleanup();
101
+ process.stdout.write('\n');
102
+ resolve(null);
103
+ return;
104
+ }
105
+ if (key.name === 'escape') {
106
+ cleanup();
107
+ process.stdout.write('\n');
108
+ resolve(null);
109
+ return;
110
+ }
111
+ if (key.name === 'up' || key.name === 'k') {
112
+ cursor = (cursor - 1 + opts.options.length) % opts.options.length;
113
+ draw();
114
+ return;
115
+ }
116
+ if (key.name === 'down' || key.name === 'j') {
117
+ cursor = (cursor + 1) % opts.options.length;
118
+ draw();
119
+ return;
120
+ }
121
+ if (key.name === 'return' || key.name === 'space') {
122
+ cleanup();
123
+ process.stdout.write('\n');
124
+ resolve(opts.options[cursor]?.value ?? null);
125
+ return;
126
+ }
127
+ };
128
+ process.stdin.on('keypress', onKey);
129
+ draw();
130
+ });
131
+ }
132
+ function renderMenu(title, options, cursor, active) {
133
+ const useColor = (0, MatrixTheme_1.supportsColor)();
134
+ const head = useColor ? MatrixTheme_1.MATRIX.head : '';
135
+ const body = useColor ? MatrixTheme_1.MATRIX.body : '';
136
+ const tail = useColor ? MatrixTheme_1.MATRIX.tail : '';
137
+ const reset = useColor ? MatrixTheme_1.ANSI.reset : '';
138
+ const lines = [];
139
+ lines.push(`${head}${title}${reset}`);
140
+ lines.push(`${tail}↑↓ navigate · ENTER select · ESC cancel${reset}`);
141
+ lines.push('');
142
+ for (let i = 0; i < options.length; i++) {
143
+ const opt = options[i];
144
+ const isCursor = i === cursor;
145
+ const isActive = opt.value === active;
146
+ const marker = isActive ? '(•)' : '(o)';
147
+ const arrow = isCursor ? '▶' : ' ';
148
+ const hint = opt.hint ? `${tail} — ${opt.hint}${reset}` : '';
149
+ const activeTag = isActive ? ` ${tail}← currently active${reset}` : '';
150
+ const color = isCursor ? head : body;
151
+ lines.push(` ${color}${arrow} ${marker} ${opt.label}${reset}${hint}${activeTag}`);
152
+ }
153
+ lines.push('');
154
+ return lines.join('\n');
155
+ }
156
+ /**
157
+ * Convenience: numbered 1/2/3 choice prompt — used after provider
158
+ * selection for the `1) Use existing 2) Reauth 3) Cancel` flow.
159
+ */
160
+ async function numberedChoice(question, choices) {
161
+ const useColor = (0, MatrixTheme_1.supportsColor)();
162
+ const head = useColor ? MatrixTheme_1.MATRIX.head : '';
163
+ const body = useColor ? MatrixTheme_1.MATRIX.body : '';
164
+ const reset = useColor ? MatrixTheme_1.ANSI.reset : '';
165
+ process.stdout.write(`\n${head}${question}${reset}\n`);
166
+ for (const c of choices) {
167
+ process.stdout.write(` ${body}${c.key}) ${c.label}${reset}\n`);
168
+ }
169
+ const keys = choices.map(c => c.key).join('/');
170
+ return new Promise((resolve) => {
171
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
172
+ rl.question(`\n ${body}choice [${keys}]:${reset} `, (answer) => {
173
+ rl.close();
174
+ const k = answer.trim();
175
+ resolve(choices.find(c => c.key === k)?.key ?? null);
176
+ });
177
+ });
178
+ }
package/dist/index.js CHANGED
@@ -537,11 +537,10 @@ program
537
537
  .action(async (mensagemArgs, options) => {
538
538
  const mensagem = mensagemArgs.join(' ');
539
539
  console.log(`\n> Você: "${mensagem}"\n`);
540
- const designHandled = tryHandleDesignConversation(mensagem);
541
- if (designHandled) {
542
- console.log(`\n> OPEN-LIFE: ${designHandled}\n`);
543
- process.exit(0);
544
- }
540
+ // Hermes parity: `ask` must send the user's message to the agent runtime.
541
+ // Conversational shortcuts such as design import/generate belong to explicit
542
+ // subcommands, not to the main chat path, otherwise OpenLife appears to
543
+ // "answer" without invoking the configured GPT-5.5 executor.
545
544
  const timeoutMs = Number(process.env.OPENLIFE_ASK_TIMEOUT_MS || 90000);
546
545
  const classifier = new IntentClassifier_1.IntentClassifier();
547
546
  const { Gatekeeper } = require('./orchestrator/Gatekeeper');
@@ -1397,7 +1396,28 @@ program
1397
1396
  program
1398
1397
  .command('update')
1399
1398
  .description('Atualiza dependências, recompila o core e valida status do sistema (modo dev)')
1400
- .action(async () => {
1399
+ .option('--global', 'Self-update via npm global install (npm i -g @openlife/cli@latest)')
1400
+ .action(async (opts) => {
1401
+ if (opts.global) {
1402
+ const { execFileSync } = require('child_process');
1403
+ try {
1404
+ const current = require('../package.json').version;
1405
+ const latest = String(execFileSync('npm', ['view', '@openlife/cli', 'version'], { encoding: 'utf-8' })).trim();
1406
+ if (latest === current) {
1407
+ console.log(JSON.stringify({ ok: true, status: 'up-to-date', version: current }));
1408
+ return;
1409
+ }
1410
+ console.log(`🔄 Self-update ${current} → ${latest}`);
1411
+ execFileSync('npm', ['install', '-g', '@openlife/cli@latest'], { stdio: 'inherit' });
1412
+ const after = String(execFileSync('openlife', ['--version'], { encoding: 'utf-8' })).trim();
1413
+ console.log(JSON.stringify({ ok: true, from: current, to: after }));
1414
+ }
1415
+ catch (e) {
1416
+ console.error(JSON.stringify({ ok: false, error: 'self_update_failed', detail: errMsg(e) }));
1417
+ process.exitCode = 1;
1418
+ }
1419
+ return;
1420
+ }
1401
1421
  console.log('🔄 OPEN-LIFE update: npm install + build + system status');
1402
1422
  try {
1403
1423
  const { stdout, stderr } = await exec('npm install && npm run build && node bin/openlife.js system status', { cwd: process.cwd(), maxBuffer: 1024 * 1024 * 10 });
@@ -2805,4 +2825,43 @@ profileCmd.command('import <file>').description('Import a profile from a JSON fi
2805
2825
  process.exitCode = 1;
2806
2826
  }
2807
2827
  });
2808
- program.parse(process.argv);
2828
+ // `openlife config` — interactive configuration TUI (provider, keys, telegram, voice, daemon)
2829
+ program
2830
+ .command('config')
2831
+ .description('Configurações interativas (provider, API keys, Telegram, voz, daemon)')
2832
+ .option('--focus <section>', 'Open directly into a section: provider | api-keys | telegram | voice | daemon')
2833
+ .action(async (opts) => {
2834
+ const { runConfig } = require('./cli/ConfigTui');
2835
+ await runConfig({ focus: opts.focus });
2836
+ });
2837
+ // `openlife chat` — explicit alias for the interactive chat TUI (matches docs)
2838
+ program
2839
+ .command('chat')
2840
+ .description('Inicia o chat interativo no terminal (Matrix-themed REPL)')
2841
+ .action(async () => {
2842
+ const { runChat } = require('./cli/ChatTui');
2843
+ await runChat();
2844
+ });
2845
+ // Bare invocation (no subcommand + interactive TTY) → launch chat TUI.
2846
+ // Piped, CI, or `--help` flows fall through to the standard Commander parse.
2847
+ const isBareInteractive = process.argv.length === 2 &&
2848
+ process.stdout.isTTY === true &&
2849
+ process.stdin.isTTY === true &&
2850
+ !process.env.OPENLIFE_DISABLE_CHAT_TUI;
2851
+ if (isBareInteractive) {
2852
+ // Defer to ChatTui — never returns under normal use.
2853
+ (async () => {
2854
+ try {
2855
+ const { runChat } = require('./cli/ChatTui');
2856
+ await runChat();
2857
+ }
2858
+ catch (e) {
2859
+ const msg = e instanceof Error ? e.message : String(e);
2860
+ console.error(`[chat] failed to start: ${msg}`);
2861
+ process.exitCode = 1;
2862
+ }
2863
+ })();
2864
+ }
2865
+ else {
2866
+ program.parse(process.argv);
2867
+ }
@@ -11,11 +11,15 @@ class MemoryProviderRegistry {
11
11
  constructor() {
12
12
  this.providers = {
13
13
  local: new LocalMemoryProvider_1.LocalMemoryProvider(),
14
- mempalace: new MempalaceProvider_1.MempalaceProvider(),
15
14
  mem0: new Mem0Provider_1.Mem0Provider(),
16
15
  'zep-graphiti': new ZepGraphitiProvider_1.ZepGraphitiProvider(),
17
16
  'redis-ams': new RedisAgentMemoryProvider_1.RedisAgentMemoryProvider()
18
17
  };
18
+ // MemPalace é opcional: registra apenas se o binário Python existir.
19
+ // Evita shell-out por turno em runtimes (Railway) que não têm mempalace instalado.
20
+ if (MempalaceProvider_1.MempalaceProvider.isAvailable()) {
21
+ this.providers.mempalace = new MempalaceProvider_1.MempalaceProvider();
22
+ }
19
23
  }
20
24
  get(name) {
21
25
  return this.providers[name] || null;
@@ -35,21 +35,69 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MempalaceProvider = void 0;
37
37
  const child_process = __importStar(require("child_process"));
38
+ const fs = __importStar(require("fs"));
39
+ const os = __importStar(require("os"));
40
+ const path = __importStar(require("path"));
38
41
  const util_1 = require("util");
39
- const exec = (0, util_1.promisify)(child_process.exec);
42
+ const execFile = (0, util_1.promisify)(child_process.execFile);
43
+ const DEFAULT_BIN = '~/.venvs/mempalace/bin/mempalace';
44
+ function expandHome(p) {
45
+ if (!p)
46
+ return p;
47
+ if (p === '~')
48
+ return os.homedir();
49
+ if (p.startsWith('~/'))
50
+ return path.join(os.homedir(), p.slice(2));
51
+ return p;
52
+ }
53
+ /**
54
+ * MemPalace é uma integração OPCIONAL. Em produção (Railway) o binário Python
55
+ * normalmente não existe; tentar shell-out adiciona latência e ruído de log a
56
+ * cada turno de conversa. `isAvailable()` resolve o path uma única vez e
57
+ * retorna false silenciosamente quando o binário não está instalado, para que
58
+ * o MemoryProviderRegistry possa pular o registro sem custo.
59
+ */
40
60
  class MempalaceProvider {
41
61
  mempalacePath;
42
- constructor(mempalacePath = process.env.MEMPALACE_BIN_PATH || '~/.venvs/mempalace/bin/mempalace') {
62
+ static cachedAvailability = null;
63
+ static cachedPath = null;
64
+ constructor(mempalacePath = process.env.MEMPALACE_BIN_PATH || DEFAULT_BIN) {
43
65
  this.mempalacePath = mempalacePath;
44
66
  }
45
67
  name() {
46
68
  return 'mempalace';
47
69
  }
70
+ static isAvailable(rawPath) {
71
+ const configured = (rawPath ?? process.env.MEMPALACE_BIN_PATH ?? DEFAULT_BIN).trim();
72
+ if (configured === '' || configured.toLowerCase() === 'off' || configured.toLowerCase() === 'disabled') {
73
+ MempalaceProvider.cachedAvailability = false;
74
+ MempalaceProvider.cachedPath = null;
75
+ return false;
76
+ }
77
+ if (MempalaceProvider.cachedAvailability !== null && MempalaceProvider.cachedPath === configured) {
78
+ return MempalaceProvider.cachedAvailability;
79
+ }
80
+ const resolved = expandHome(configured);
81
+ let ok = false;
82
+ try {
83
+ ok = fs.existsSync(resolved) && fs.statSync(resolved).isFile();
84
+ }
85
+ catch {
86
+ ok = false;
87
+ }
88
+ MempalaceProvider.cachedAvailability = ok;
89
+ MempalaceProvider.cachedPath = configured;
90
+ return ok;
91
+ }
92
+ static resetAvailabilityCache() {
93
+ MempalaceProvider.cachedAvailability = null;
94
+ MempalaceProvider.cachedPath = null;
95
+ }
48
96
  async search(input) {
49
- const safeQuery = input.query.replace(/"/g, '\\"');
50
- const command = `bash -c "${this.mempalacePath} search \\\"${safeQuery}\\\""`;
97
+ if (!MempalaceProvider.isAvailable(this.mempalacePath))
98
+ return [];
51
99
  try {
52
- const { stdout } = await exec(command);
100
+ const { stdout } = await execFile(expandHome(this.mempalacePath), ['search', input.query], { timeout: 5000 });
53
101
  if (!stdout.trim())
54
102
  return [];
55
103
  return [{
@@ -39,6 +39,7 @@ const util_1 = require("util");
39
39
  const fs = __importStar(require("fs"));
40
40
  const path = __importStar(require("path"));
41
41
  const LocalMemoryProvider_1 = require("./LocalMemoryProvider");
42
+ const MempalaceProvider_1 = require("./MempalaceProvider");
42
43
  const MemoryOrchestrator_1 = require("./MemoryOrchestrator");
43
44
  const ToolsetGuard_1 = require("../orchestrator/toolset/ToolsetGuard");
44
45
  const exec = (0, util_1.promisify)(child_process.exec);
@@ -57,20 +58,23 @@ class OmniMemory {
57
58
  if (hits.length) {
58
59
  return hits.map(hit => `- [${hit.provider}] ${hit.record.summary || hit.record.content}`).join('\n');
59
60
  }
61
+ if (!MempalaceProvider_1.MempalaceProvider.isAvailable(this.mempalacePath)) {
62
+ return '';
63
+ }
60
64
  console.log(`[OMNI-MEMORY] Buscando no MemPalace: "${query}"...`);
61
65
  try {
62
66
  const safeQuery = query.replace(/"/g, '\\"');
63
67
  const command = `bash -c "${this.mempalacePath} search \\\"${safeQuery}\\\""`;
64
- const { stdout } = await exec(command);
68
+ const timeoutMs = Number(process.env.OPENLIFE_MEMORY_SEARCH_TIMEOUT_MS || 1500);
69
+ const { stdout } = await exec(command, { timeout: timeoutMs });
65
70
  if (!stdout.trim() || stdout.includes("No results found")) {
66
- return "Nenhuma memória encontrada no Palácio para este termo.";
71
+ return "";
67
72
  }
68
73
  const citation = `\n\n[RAG de Evidências: Buscado em ${new Date().toISOString()}]`;
69
74
  return stdout + citation;
70
75
  }
71
- catch (error) {
72
- console.warn(`[OMNI-MEMORY] Aviso ao buscar no MemPalace (pode ser ausência de resultados):`, error);
73
- return "Busca na Omni-Memory concluída com alertas ou sem resultados diretos.";
76
+ catch (_error) {
77
+ return "";
74
78
  }
75
79
  }
76
80
  async saveFact(fact, metadata, namespace) {
@@ -36,18 +36,43 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.Brain = void 0;
39
+ exports.Brain = exports.CodexTimeoutError = void 0;
40
40
  const child_process = __importStar(require("child_process"));
41
41
  const util_1 = require("util");
42
+ const fs = __importStar(require("fs"));
43
+ const path = __importStar(require("path"));
42
44
  const openai_1 = __importDefault(require("openai"));
43
45
  const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
44
46
  const generative_ai_1 = require("@google/generative-ai");
45
47
  const ModelManager_1 = require("./ModelManager");
46
48
  const ToolsetGuard_1 = require("./toolset/ToolsetGuard");
47
49
  const execFile = (0, util_1.promisify)(child_process.execFile);
48
- const DEFAULT_MODEL_TIMEOUT_MS = Number(process.env.OPENLIFE_MODEL_TIMEOUT_MS || 120000);
50
+ const DEFAULT_MODEL_TIMEOUT_MS = Number(process.env.OPENLIFE_MODEL_TIMEOUT_MS || 60000);
49
51
  const REASONING_MODE = String(process.env.OPENLIFE_REASONING_MODE || 'errors').toLowerCase(); // off | errors | always
52
+ // Conversational fast path must fail fast so the Gateway can surface a
53
+ // timeout-aware reply to the user. Deep engineering missions keep the full
54
+ // 60s budget. Both are env-overridable.
55
+ function getCodexFastTimeoutMs() {
56
+ return Number(process.env.OPENLIFE_CODEX_FAST_TIMEOUT_MS || 30000);
57
+ }
58
+ function getCodexDeepTimeoutMs() {
59
+ return Number(process.env.OPENLIFE_CODEX_DEEP_TIMEOUT_MS || DEFAULT_MODEL_TIMEOUT_MS);
60
+ }
61
+ class CodexTimeoutError extends Error {
62
+ depth;
63
+ timeoutMs;
64
+ model;
65
+ constructor(depth, timeoutMs, model) {
66
+ super(`CODEX_TIMEOUT_${depth.toUpperCase()}_${timeoutMs}MS_${model || 'default'}`);
67
+ this.name = 'CodexTimeoutError';
68
+ this.depth = depth;
69
+ this.timeoutMs = timeoutMs;
70
+ this.model = model || 'default';
71
+ }
72
+ }
73
+ exports.CodexTimeoutError = CodexTimeoutError;
50
74
  class Brain {
75
+ static codexQueue = Promise.resolve();
51
76
  openai = null;
52
77
  anthropic = null;
53
78
  geminiApi = null;
@@ -87,21 +112,38 @@ class Brain {
87
112
  return true;
88
113
  return false;
89
114
  }
90
- async think(systemPrompt, userMessage) {
115
+ async think(systemPrompt, userMessage, opts) {
91
116
  const config = this.modelManager.getModelConfig();
92
117
  const modelsToTry = [config.primary, ...config.fallbacks].filter((model, index, arr) => arr.findIndex(m => m.raw === model.raw) === index);
118
+ return this.runModelChain(systemPrompt, userMessage, modelsToTry, 'deep', opts);
119
+ }
120
+ /** Fast conversational path. Keeps the configured model chain order intact. */
121
+ async thinkFast(systemPrompt, userMessage, opts) {
122
+ const config = this.modelManager.getModelConfig();
123
+ const models = [config.primary, ...config.fallbacks].filter((model, index, arr) => arr.findIndex(m => m.raw === model.raw) === index);
124
+ return this.runModelChain(systemPrompt, userMessage, models, 'fast', opts);
125
+ }
126
+ async runModelChain(systemPrompt, userMessage, modelsToTry, depth = 'deep', opts) {
127
+ const emitReasoning = (chunk) => {
128
+ try {
129
+ opts?.onReasoning?.(chunk);
130
+ }
131
+ catch { /* never let a callback break the chain */ }
132
+ };
133
+ emitReasoning(`🧭 chain: ${modelsToTry.map(m => m.raw).join(' → ')}`);
93
134
  const failures = [];
94
135
  for (let i = 0; i < modelsToTry.length; i++) {
95
136
  const modelIdent = modelsToTry[i];
96
137
  try {
97
138
  console.log(`[BRAIN] Tentando modelo ${i === 0 ? 'PRIMÁRIO' : 'FALLBACK ' + i}: ${modelIdent.raw}...`);
139
+ emitReasoning(`🔎 trying ${i === 0 ? 'primary' : `fallback ${i}`}: ${modelIdent.raw}`);
98
140
  let out = '';
99
141
  switch (modelIdent.provider) {
100
142
  case 'openai-api':
101
143
  out = await this.thinkWithOpenAIAPI(systemPrompt, userMessage, modelIdent.name);
102
144
  break;
103
145
  case 'openai-cli':
104
- out = await this.thinkWithOpenAICLI(systemPrompt, userMessage, modelIdent.name);
146
+ out = await this.thinkWithOpenAICLI(systemPrompt, userMessage, modelIdent.name, depth);
105
147
  break;
106
148
  case 'anthropic':
107
149
  out = await this.thinkWithAnthropic(systemPrompt, userMessage, modelIdent.name);
@@ -134,15 +176,32 @@ class Brain {
134
176
  const reason = error instanceof Error ? error.message : String(error);
135
177
  failures.push({ model: modelIdent.raw, reason });
136
178
  console.error(`[BRAIN ERROR - ${modelIdent.raw}]`, reason);
137
- if (i !== modelsToTry.length - 1)
179
+ emitReasoning(`✗ ${modelIdent.raw} failed: ${reason.slice(0, 120)}`);
180
+ if (i !== modelsToTry.length - 1) {
138
181
  console.log(`[BRAIN] Rotacionando para o próximo fallback da cadeia...`);
182
+ emitReasoning('↻ rotating to next fallback');
183
+ }
139
184
  }
140
185
  }
141
186
  const concise = failures.slice(-3).map(f => `- ${f.model}: ${f.reason}`).join('\n');
187
+ const strictGpt55Only = modelsToTry.length === 1 && modelsToTry[0]?.raw === 'openai-cli/gpt-5.5';
188
+ if (strictGpt55Only) {
189
+ return [
190
+ 'Executor GPT-5.5 indisponível no momento.',
191
+ '',
192
+ 'Falhas recentes:',
193
+ concise,
194
+ '',
195
+ 'Ação recomendada:',
196
+ '1) Reautentique o Codex OAuth no mesmo runtime onde o OpenLife está rodando.',
197
+ '2) Valide com: codex login status',
198
+ '3) Valide execução real com: codex exec --model gpt-5.5 "responda apenas OK"'
199
+ ].join('\n');
200
+ }
142
201
  const guidance = [
143
- '1) Configure ao menos um provedor API no Railway (ex.: GEMINI_API_KEY ou OPENAI_API_KEY).',
144
- '2) Não dependa de CLI providers (codex/gemini) no container sem binário/auth.',
145
- '3) Rode: openlife doctor e valide a cadeia de modelos.'
202
+ '1) Valide as credenciais/binários dos provedores configurados.',
203
+ '2) Ajuste models.json apenas se a política do produto permitir fallbacks.',
204
+ '3) Rode: openlife doctor e openlife models status.'
146
205
  ].join('\n');
147
206
  const summary = REASONING_MODE !== 'off'
148
207
  ? `\n\nResumo operacional:\n- Objetivo: responder com robustez por fallback.\n- Estratégia: rotação sequencial de provedores.\n- Resultado: todos os provedores tentados falharam.`
@@ -193,19 +252,73 @@ class Brain {
193
252
  throw this.formatProviderError('openai-api', model, err, { keyEnvVar: 'OPENAI_API_KEY', expectedKeyPrefix: 'sk-' });
194
253
  }
195
254
  }
196
- async thinkWithOpenAICLI(systemPrompt, userMessage, model) {
255
+ async thinkWithOpenAICLI(systemPrompt, userMessage, model, depth = 'deep') {
197
256
  (0, ToolsetGuard_1.assertToolsetAllowed)('delegation', 'Brain.thinkWithOpenAICLI');
198
- const args = ['exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check'];
257
+ const outputFile = path.join(process.cwd(), '.openlife', `codex-last-message-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.txt`);
258
+ fs.mkdirSync(path.dirname(outputFile), { recursive: true });
259
+ const args = ['exec', '--dangerously-bypass-approvals-and-sandbox', '--skip-git-repo-check', '--color', 'never', '--output-last-message', outputFile];
260
+ if (String(process.env.OPENLIFE_CODEX_EPHEMERAL || 'true').toLowerCase() !== 'false')
261
+ args.push('--ephemeral');
199
262
  if (model && model !== 'default')
200
263
  args.push('--model', model);
201
- args.push(`${systemPrompt}\n\nMensagem: ${userMessage}`);
264
+ const prompt = depth === 'fast'
265
+ ? [
266
+ 'Você é Lara, agente conversacional do OpenLife.',
267
+ 'Responda diretamente em português natural e conciso.',
268
+ 'Não use ferramentas, não leia arquivos e não execute comandos para responder esta mensagem.',
269
+ 'Se não souber algo específico, seja honesta e diga o que falta.',
270
+ '',
271
+ `Mensagem: ${userMessage}`
272
+ ].join('\n')
273
+ : `${systemPrompt}\n\nMensagem: ${userMessage}`;
274
+ args.push(prompt);
275
+ const timeoutMs = depth === 'fast' ? getCodexFastTimeoutMs() : getCodexDeepTimeoutMs();
276
+ // Deep missions remain serialized through the static queue to avoid
277
+ // contending Codex CLI invocations. Fast conversational calls bypass
278
+ // the queue so a long-running deep mission can not block a greeting.
279
+ let release = () => { };
280
+ if (depth === 'deep') {
281
+ const previous = Brain.codexQueue;
282
+ Brain.codexQueue = new Promise((resolve) => { release = resolve; });
283
+ await previous;
284
+ }
202
285
  try {
203
- const { stdout } = await execFile('codex', args, { maxBuffer: 1024 * 1024 * 10, timeout: DEFAULT_MODEL_TIMEOUT_MS });
204
- return stdout.trim() || 'Sem resposta do Codex CLI.';
286
+ const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));
287
+ const command = process.platform === 'win32' ? 'codex' : 'timeout';
288
+ const commandArgs = process.platform === 'win32'
289
+ ? args
290
+ : ['-k', '2s', `${timeoutSeconds}s`, 'codex', ...args];
291
+ const { stdout } = await execFile(command, commandArgs, { maxBuffer: 1024 * 1024 * 10, timeout: timeoutMs + 3000, killSignal: 'SIGKILL' });
292
+ const lastMessage = fs.existsSync(outputFile) ? fs.readFileSync(outputFile, 'utf-8').trim() : '';
293
+ try {
294
+ fs.unlinkSync(outputFile);
295
+ }
296
+ catch { /* ignore cleanup */ }
297
+ const finalText = lastMessage || stdout.trim();
298
+ if (!finalText)
299
+ throw new Error(`EMPTY_CODEX_RESPONSE_${model || 'default'}`);
300
+ return finalText;
205
301
  }
206
302
  catch (err) {
303
+ try {
304
+ if (fs.existsSync(outputFile))
305
+ fs.unlinkSync(outputFile);
306
+ }
307
+ catch { /* ignore cleanup */ }
308
+ // execFile signals a timeout via either ETIMEDOUT or by killing the
309
+ // child (signal !== null). Surface a typed error so callers can
310
+ // present a depth-aware message.
311
+ const e = err;
312
+ const errCode = e.code;
313
+ const isTimeout = errCode === 'ETIMEDOUT' || errCode === 124 || errCode === '124' || (e?.killed === true && !!e?.signal);
314
+ if (isTimeout) {
315
+ throw new CodexTimeoutError(depth, timeoutMs, model);
316
+ }
207
317
  throw this.formatProviderError('openai-cli', model, err);
208
318
  }
319
+ finally {
320
+ release();
321
+ }
209
322
  }
210
323
  async thinkWithAnthropic(systemPrompt, userMessage, model) {
211
324
  if (!this.anthropic)