arisa 2.3.50 → 2.3.52

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arisa",
3
- "version": "2.3.50",
3
+ "version": "2.3.52",
4
4
  "description": "Arisa - dynamic agent runtime with daemon/core architecture that evolves through user interaction",
5
5
  "keywords": [
6
6
  "tinyclaw",
package/src/core/index.ts CHANGED
@@ -27,7 +27,6 @@ import {
27
27
  import { transcribeAudio, describeImage, generateSpeech, isMediaConfigured, isSpeechConfigured } from "./media";
28
28
  import { detectFiles } from "./file-detector";
29
29
 
30
- import { addExchange, getForeignContext, clearHistory, getLastBackend } from "./history";
31
30
  import { getOnboarding, checkDeps } from "./onboarding";
32
31
  import { initScheduler, addTask, cancelAllChatTasks } from "./scheduler";
33
32
  import { detectScheduleIntent } from "./intent";
@@ -60,13 +59,6 @@ function getBackend(chatId: string): "claude" | "codex" {
60
59
  const current = backendState.get(chatId);
61
60
  if (current) return preferInstalled(current);
62
61
 
63
- const fromHistory = getLastBackend(chatId);
64
- if (fromHistory) {
65
- const resolved = preferInstalled(fromHistory);
66
- backendState.set(chatId, resolved);
67
- return resolved;
68
- }
69
-
70
62
  return preferInstalled(defaultBackend());
71
63
  }
72
64
 
@@ -164,7 +156,6 @@ ${messageText}`;
164
156
  if (msg.command === "/reset") {
165
157
  const { writeFileSync } = await import("fs");
166
158
  writeFileSync(config.resetFlagPath, "reset");
167
- clearHistory(msg.chatId);
168
159
  const { resetRouterState } = await import("./router");
169
160
  resetRouterState();
170
161
  const response: CoreResponse = { text: "Conversation reset! Next message will start a fresh conversation." };
@@ -361,27 +352,22 @@ ${messageText}`;
361
352
  const backend = getBackend(msg.chatId);
362
353
  const canFallback = backend === "codex" ? deps.claude : deps.codex;
363
354
  let agentResponse: string;
364
- let historyResponse: string | null = null;
365
355
  let usedBackend: "claude" | "codex" = backend;
366
356
 
367
- // Inject cross-backend context if switching
368
- const foreignCtx = getForeignContext(msg.chatId, backend);
369
- const enrichedMessage = foreignCtx ? foreignCtx + messageText : messageText;
370
-
371
- log.info(`Routing | backend: ${backend} | foreignCtx: ${!!foreignCtx} | enrichedChars: ${enrichedMessage.length}`);
357
+ log.info(`Routing | backend: ${backend} | messageChars: ${messageText.length}`);
372
358
 
373
359
  if (backend === "codex") {
374
360
  try {
375
- agentResponse = await processWithCodex(enrichedMessage);
361
+ agentResponse = await processWithCodex(messageText);
376
362
  if (agentResponse.startsWith("Error processing with Codex") && canFallback) {
377
363
  log.warn("Codex failed, falling back to Claude");
378
- agentResponse = await processWithClaude(enrichedMessage, msg.chatId);
364
+ agentResponse = await processWithClaude(messageText, msg.chatId);
379
365
  usedBackend = "claude";
380
366
  }
381
367
  } catch (error) {
382
368
  if (canFallback) {
383
369
  log.warn(`Codex threw, falling back to Claude: ${error}`);
384
- agentResponse = await processWithClaude(enrichedMessage, msg.chatId);
370
+ agentResponse = await processWithClaude(messageText, msg.chatId);
385
371
  usedBackend = "claude";
386
372
  } else {
387
373
  agentResponse = "Error processing with Codex. Please try again.";
@@ -389,23 +375,20 @@ ${messageText}`;
389
375
  }
390
376
  } else {
391
377
  try {
392
- agentResponse = await processWithClaude(enrichedMessage, msg.chatId);
378
+ agentResponse = await processWithClaude(messageText, msg.chatId);
393
379
  if (agentResponse.startsWith("Error:") && canFallback) {
394
380
  log.warn("Claude failed, falling back to Codex");
395
- agentResponse = await processWithCodex(enrichedMessage);
381
+ agentResponse = await processWithCodex(messageText);
396
382
  usedBackend = "codex";
397
383
  }
398
384
  if (isClaudeRateLimitResponse(agentResponse) && canFallback) {
399
385
  log.warn("Claude credits exhausted, falling back to Codex");
400
- const codexResponse = await processWithCodex(enrichedMessage);
386
+ const codexResponse = await processWithCodex(messageText);
401
387
  if (isCodexAuthRequiredResponse(codexResponse)) {
402
388
  agentResponse = `${agentResponse}\n---CHUNK---\n${codexResponse}`;
403
389
  } else {
404
390
  agentResponse = `Claude is out of credits right now, so I switched this reply to Codex.\n---CHUNK---\n${codexResponse}`;
405
- historyResponse = codexResponse;
406
391
  usedBackend = "codex";
407
- // Persist the switch so subsequent messages don't keep re-injecting
408
- // cross-backend context while Claude has no credits.
409
392
  backendState.set(msg.chatId, "codex");
410
393
  }
411
394
  }
@@ -413,7 +396,7 @@ ${messageText}`;
413
396
  const errMsg = error instanceof Error ? error.message : String(error);
414
397
  if (canFallback) {
415
398
  log.warn(`Claude threw, falling back to Codex: ${errMsg}`);
416
- agentResponse = await processWithCodex(enrichedMessage);
399
+ agentResponse = await processWithCodex(messageText);
417
400
  usedBackend = "codex";
418
401
  } else {
419
402
  agentResponse = `Claude error: ${errMsg.slice(0, 200)}`;
@@ -421,9 +404,6 @@ ${messageText}`;
421
404
  }
422
405
  }
423
406
 
424
- // Log exchange for shared history
425
- addExchange(msg.chatId, messageText, historyResponse ?? agentResponse, usedBackend);
426
-
427
407
  log.info(`Response | backend: ${usedBackend} | responseChars: ${agentResponse.length}`);
428
408
  log.debug(`Response raw >>>>\n${agentResponse}\n<<<<`);
429
409
 
@@ -12,7 +12,6 @@
12
12
  */
13
13
 
14
14
  import { selectModel } from "./router";
15
- import { getRecentHistory } from "./history";
16
15
  import { shouldContinue } from "./context";
17
16
  import { config } from "../shared/config";
18
17
  import { createLogger } from "../shared/logger";
@@ -127,12 +126,10 @@ async function processNext() {
127
126
 
128
127
  async function runClaude(message: string, chatId: string): Promise<string> {
129
128
  const model = selectModel(message);
130
- const historyContext = getRecentHistory(chatId);
131
129
  const start = Date.now();
132
- const prompt = withSoul(historyContext + message);
130
+ const prompt = withSoul(message);
133
131
 
134
- const historyCount = historyContext ? historyContext.split("\nUser: ").length - 1 : 0;
135
- log.info(`Model: ${model.model} (${model.reason}) | History: ${historyCount} exchanges`);
132
+ log.info(`Model: ${model.model} (${model.reason})`);
136
133
 
137
134
  const args = ["--dangerously-skip-permissions", "--output-format", "text"];
138
135
 
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * @module daemon/setup
3
- * @role Interactive first-run setup with inquirer prompts.
3
+ * @role Idempotent startup setup runs every boot, checks real state.
4
4
  * @responsibilities
5
5
  * - Check required config (TELEGRAM_BOT_TOKEN)
6
6
  * - Check optional config (OPENAI_API_KEY)
7
7
  * - Detect / install missing CLIs (Claude, Codex)
8
- * - Run interactive login flows for installed CLIs
8
+ * - Check CLI auth and offer login if needed
9
9
  * - Persist tokens to both .env and encrypted DB
10
10
  * @dependencies shared/paths, shared/secrets, shared/ai-cli
11
11
  * @effects Reads stdin, writes runtime .env, spawns install/login processes
@@ -18,7 +18,6 @@ import { secrets, setSecret } from "../shared/secrets";
18
18
  import { isAgentCliInstalled, buildBunWrappedAgentCliCommand, type AgentCliName } from "../shared/ai-cli";
19
19
 
20
20
  const ENV_PATH = join(dataDir, ".env");
21
- const SETUP_DONE_KEY = "ARISA_SETUP_COMPLETE";
22
21
 
23
22
  const CLI_PACKAGES: Record<AgentCliName, string> = {
24
23
  claude: "@anthropic-ai/claude-code",
@@ -32,6 +31,8 @@ function loadExistingEnv(): Record<string, string> {
32
31
  const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.+)$/);
33
32
  if (match) vars[match[1]] = match[2].trim();
34
33
  }
34
+ // Clean up legacy flag — state is now derived from actual config
35
+ delete vars["ARISA_SETUP_COMPLETE"];
35
36
  return vars;
36
37
  }
37
38
 
@@ -61,8 +62,6 @@ export async function runSetup(): Promise<boolean> {
61
62
  const telegramSecret = await secrets.telegram();
62
63
  const openaiSecret = await secrets.openai();
63
64
  let changed = false;
64
- const setupDone = vars[SETUP_DONE_KEY] === "1" || process.env[SETUP_DONE_KEY] === "1";
65
- const isFirstRun = !setupDone;
66
65
 
67
66
  // Try to load inquirer for interactive mode
68
67
  let inq: typeof import("@inquirer/prompts") | null = null;
@@ -80,7 +79,7 @@ export async function runSetup(): Promise<boolean> {
80
79
  const hasOpenAI = !!(vars.OPENAI_API_KEY || process.env.OPENAI_API_KEY || openaiSecret);
81
80
 
82
81
  if (!hasTelegram) {
83
- if (isFirstRun) console.log("\n🔧 Arisa Setup\n");
82
+ console.log("\n🔧 Arisa Setup\n");
84
83
 
85
84
  let token: string;
86
85
  if (inq) {
@@ -109,170 +108,119 @@ export async function runSetup(): Promise<boolean> {
109
108
  console.log(`[setup] TELEGRAM_BOT_TOKEN found in ${src}`);
110
109
  }
111
110
 
112
- if (!hasOpenAI && isFirstRun) {
113
- let key: string;
114
- if (inq) {
115
- key = await inq.input({
116
- message: "OpenAI API Key (optional — voice + image, enter to skip):",
117
- });
118
- } else {
119
- console.log("\nOpenAI API Key (optional — enables voice transcription + image analysis).");
120
- key = await readLine("OPENAI_API_KEY (enter to skip): ");
121
- }
111
+ if (!hasOpenAI) {
112
+ if (process.stdin.isTTY) {
113
+ let key: string;
114
+ if (inq) {
115
+ key = await inq.input({
116
+ message: "OpenAI API Key (optional — voice + image, enter to skip):",
117
+ });
118
+ } else {
119
+ console.log("\nOpenAI API Key (optional enables voice transcription + image analysis).");
120
+ key = await readLine("OPENAI_API_KEY (enter to skip): ");
121
+ }
122
122
 
123
- if (key.trim()) {
124
- vars.OPENAI_API_KEY = key.trim();
125
- await setSecret("OPENAI_API_KEY", key.trim()).catch((e) =>
126
- console.warn(`[setup] Could not persist OPENAI_API_KEY to encrypted DB: ${e}`)
127
- );
128
- console.log("[setup] OPENAI_API_KEY saved to .env + encrypted DB");
129
- changed = true;
123
+ if (key.trim()) {
124
+ vars.OPENAI_API_KEY = key.trim();
125
+ await setSecret("OPENAI_API_KEY", key.trim()).catch((e) =>
126
+ console.warn(`[setup] Could not persist OPENAI_API_KEY to encrypted DB: ${e}`)
127
+ );
128
+ console.log("[setup] OPENAI_API_KEY saved to .env + encrypted DB");
129
+ changed = true;
130
+ }
130
131
  }
131
- } else if (hasOpenAI) {
132
+ } else {
132
133
  const src = openaiSecret ? "encrypted DB" : vars.OPENAI_API_KEY ? ".env" : "env var";
133
134
  console.log(`[setup] OPENAI_API_KEY found in ${src}`);
134
135
  }
135
136
 
136
- // Save tokens
137
- if (!setupDone) {
138
- vars[SETUP_DONE_KEY] = "1";
139
- changed = true;
140
- }
141
137
  if (changed) {
142
138
  saveEnv(vars);
143
139
  console.log(`\nConfig saved to ${ENV_PATH}`);
144
140
  }
145
141
 
146
- // ─── Phase 2: CLI Installation + Auth ───────────────────────────
142
+ // ─── Phase 2: CLI Installation ──────────────────────────────────
147
143
 
148
144
  if (process.stdin.isTTY) {
149
- if (isFirstRun) {
150
- // First run: offer to install missing CLIs + login
151
- await setupClis(inq, vars);
152
- } else {
153
- // Subsequent runs: check if any installed CLI needs auth, offer login
154
- await checkCliAuth(inq, vars);
155
- }
156
- }
157
-
158
- return true;
159
- }
160
-
161
- async function setupClis(inq: typeof import("@inquirer/prompts") | null, vars: Record<string, string>) {
162
- let claudeInstalled = isAgentCliInstalled("claude");
163
- let codexInstalled = isAgentCliInstalled("codex");
164
-
165
- console.log("\nCLI Status:");
166
- console.log(` ${claudeInstalled ? "✓" : "✗"} Claude${claudeInstalled ? "" : " — not installed"}`);
167
- console.log(` ${codexInstalled ? "" : "✗"} Codex${codexInstalled ? "" : " — not installed"}`);
168
-
169
- // Install missing CLIs
170
- const missing: AgentCliName[] = [];
171
- if (!claudeInstalled) missing.push("claude");
172
- if (!codexInstalled) missing.push("codex");
173
-
174
- if (missing.length > 0) {
175
- let toInstall: AgentCliName[] = [];
145
+ let claudeInstalled = isAgentCliInstalled("claude");
146
+ let codexInstalled = isAgentCliInstalled("codex");
147
+
148
+ console.log("\nCLI Status:");
149
+ console.log(` ${claudeInstalled ? "✓" : "✗"} Claude${claudeInstalled ? "" : " not installed"}`);
150
+ console.log(` ${codexInstalled ? "✓" : "✗"} Codex${codexInstalled ? "" : " — not installed"}`);
151
+
152
+ const missing: AgentCliName[] = [];
153
+ if (!claudeInstalled) missing.push("claude");
154
+ if (!codexInstalled) missing.push("codex");
155
+
156
+ if (missing.length > 0) {
157
+ let toInstall: AgentCliName[] = [];
158
+
159
+ if (inq) {
160
+ toInstall = await inq.checkbox({
161
+ message: "Install missing CLIs? (space to select, enter to confirm)",
162
+ choices: missing.map((cli) => ({
163
+ name: `${cli === "claude" ? "Claude" : "Codex"} (${CLI_PACKAGES[cli]})`,
164
+ value: cli as AgentCliName,
165
+ checked: true,
166
+ })),
167
+ });
168
+ } else {
169
+ const answer = await readLine("\nInstall missing CLIs? (Y/n): ");
170
+ if (answer.toLowerCase() !== "n") toInstall = missing;
171
+ }
176
172
 
177
- if (inq) {
178
- toInstall = await inq.checkbox({
179
- message: "Install missing CLIs? (space to select, enter to confirm)",
180
- choices: missing.map((cli) => ({
181
- name: `${cli === "claude" ? "Claude" : "Codex"} (${CLI_PACKAGES[cli]})`,
182
- value: cli as AgentCliName,
183
- checked: true,
184
- })),
185
- });
186
- } else {
187
- // Non-inquirer fallback: install all
188
- const answer = await readLine("\nInstall missing CLIs? (Y/n): ");
189
- if (answer.toLowerCase() !== "n") toInstall = missing;
190
- }
173
+ for (const cli of toInstall) {
174
+ console.log(`\nInstalling ${cli}...`);
175
+ const ok = await installCli(cli);
176
+ console.log(ok ? ` ✓ ${cli} installed` : ` ✗ ${cli} install failed`);
177
+ }
191
178
 
192
- for (const cli of toInstall) {
193
- console.log(`\nInstalling ${cli}...`);
194
- const ok = await installCli(cli);
195
- console.log(ok ? ` ✓ ${cli} installed` : ` ✗ ${cli} install failed`);
179
+ // Refresh status
180
+ claudeInstalled = isAgentCliInstalled("claude");
181
+ codexInstalled = isAgentCliInstalled("codex");
196
182
  }
197
183
 
198
- // Refresh status
199
- claudeInstalled = isAgentCliInstalled("claude");
200
- codexInstalled = isAgentCliInstalled("codex");
201
- }
202
-
203
- // Login CLIs
204
- if (claudeInstalled) {
205
- let doLogin = true;
206
- if (inq) {
207
- doLogin = await inq.confirm({ message: "Log in to Claude?", default: true });
208
- } else {
209
- const answer = await readLine("\nLog in to Claude? (Y/n): ");
210
- doLogin = answer.toLowerCase() !== "n";
211
- }
212
- if (doLogin) {
213
- console.log();
214
- await runInteractiveLogin("claude", vars);
215
- }
216
- }
184
+ // ─── Phase 3: CLI Authentication ────────────────────────────────
217
185
 
218
- if (codexInstalled) {
219
- let doLogin = true;
220
- if (inq) {
221
- doLogin = await inq.confirm({ message: "Log in to Codex?", default: true });
222
- } else {
223
- const answer = await readLine("\nLog in to Codex? (Y/n): ");
224
- doLogin = answer.toLowerCase() !== "n";
225
- }
226
- if (doLogin) {
227
- console.log();
228
- await runInteractiveLogin("codex", vars);
229
- }
230
- }
186
+ const installed: AgentCliName[] = [];
187
+ if (claudeInstalled) installed.push("claude");
188
+ if (codexInstalled) installed.push("codex");
231
189
 
232
- if (!claudeInstalled && !codexInstalled) {
233
- console.log("\n⚠ No CLIs installed. Arisa needs at least one to work.");
234
- console.log(" The daemon will auto-install them in the background.\n");
235
- } else {
236
- console.log("\n✓ Setup complete!\n");
237
- }
238
- }
190
+ for (const cli of installed) {
191
+ const authed = await isCliAuthenticated(cli);
192
+ if (authed) {
193
+ console.log(`[setup] ${cli} authenticated`);
194
+ continue;
195
+ }
239
196
 
240
- /**
241
- * On non-first runs, check if installed CLIs are authenticated.
242
- * If not, offer to login interactively.
243
- */
244
- async function checkCliAuth(inq: typeof import("@inquirer/prompts") | null, vars: Record<string, string>) {
245
- const clis: AgentCliName[] = [];
246
- if (isAgentCliInstalled("claude")) clis.push("claude");
247
- if (isAgentCliInstalled("codex")) clis.push("codex");
248
- if (clis.length === 0) return;
249
-
250
- for (const cli of clis) {
251
- const authed = await isCliAuthenticated(cli);
252
- if (authed) {
253
- console.log(`[setup] ${cli} ✓ authenticated`);
254
- continue;
197
+ console.log(`[setup] ${cli} ✗ not authenticated`);
198
+ let doLogin = true;
199
+ if (inq) {
200
+ doLogin = await inq.confirm({ message: `Log in to ${cli === "claude" ? "Claude" : "Codex"}?`, default: true });
201
+ } else {
202
+ const answer = await readLine(`\nLog in to ${cli === "claude" ? "Claude" : "Codex"}? (Y/n): `);
203
+ doLogin = answer.toLowerCase() !== "n";
204
+ }
205
+ if (doLogin) {
206
+ console.log();
207
+ await runInteractiveLogin(cli, vars);
208
+ }
255
209
  }
256
210
 
257
- console.log(`[setup] ${cli} not authenticated`);
258
- let doLogin = true;
259
- if (inq) {
260
- doLogin = await inq.confirm({ message: `Log in to ${cli === "claude" ? "Claude" : "Codex"}?`, default: true });
261
- } else {
262
- const answer = await readLine(`\nLog in to ${cli === "claude" ? "Claude" : "Codex"}? (Y/n): `);
263
- doLogin = answer.toLowerCase() !== "n";
264
- }
265
- if (doLogin) {
266
- console.log();
267
- await runInteractiveLogin(cli, vars);
211
+ if (installed.length === 0) {
212
+ console.log("\n⚠ No CLIs installed. Arisa needs at least one to work.");
213
+ console.log(" The daemon will auto-install them in the background.\n");
268
214
  }
269
215
  }
216
+
217
+ return true;
270
218
  }
271
219
 
272
220
  /**
273
221
  * Quick probe: is this CLI authenticated?
274
222
  * Claude: check CLAUDE_CODE_OAUTH_TOKEN env/.env, or `claude auth status`
275
- * Codex: no simple auth check, assume OK if installed
223
+ * Codex: check OPENAI_API_KEY
276
224
  */
277
225
  async function isCliAuthenticated(cli: AgentCliName): Promise<boolean> {
278
226
  try {
@@ -317,7 +265,6 @@ async function installCli(cli: AgentCliName): Promise<boolean> {
317
265
  }
318
266
  }
319
267
 
320
-
321
268
  async function runInteractiveLogin(cli: AgentCliName, vars: Record<string, string>): Promise<boolean> {
322
269
  const args = cli === "claude"
323
270
  ? ["setup-token"]
@@ -1,193 +0,0 @@
1
- /**
2
- * @module core/history
3
- * @role Shared conversation history across backends (Claude/Codex).
4
- * @responsibilities
5
- * - Log each user↔backend exchange with backend tag
6
- * - Provide "foreign" context: exchanges from the OTHER backend
7
- * that the current backend hasn't seen
8
- * - Persist to disk, load on startup
9
- * @dependencies shared/config
10
- * @effects Reads/writes runtime history.jsonl
11
- */
12
-
13
- import { existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync } from "fs";
14
- import { join, dirname } from "path";
15
- import { config } from "../shared/config";
16
- import { createLogger } from "../shared/logger";
17
-
18
- const log = createLogger("core");
19
-
20
- const HISTORY_PATH = join(config.arisaDir, "history.jsonl");
21
- const MAX_ENTRIES_PER_CHAT = 50;
22
- const FOREIGN_CONTEXT_MAX_AGE_MS = 30 * 60 * 1000;
23
- const CODEX_SWITCH_NOTICE = "Claude is out of credits right now, so I switched this reply to Codex.";
24
-
25
- interface Exchange {
26
- ts: number;
27
- chatId: string;
28
- user: string;
29
- response: string;
30
- backend: "claude" | "codex";
31
- }
32
-
33
- let history: Exchange[] = [];
34
-
35
- // Load persisted history on import
36
- try {
37
- if (existsSync(HISTORY_PATH)) {
38
- const lines = readFileSync(HISTORY_PATH, "utf8").split("\n").filter(Boolean);
39
- history = lines.map((l) => {
40
- const entry = JSON.parse(l) as Exchange;
41
- return { ...entry, response: normalizeResponse(entry.response) };
42
- });
43
- log.info(`Loaded ${history.length} history entries`);
44
- }
45
- } catch (e) {
46
- log.warn(`Failed to load history: ${e}`);
47
- }
48
-
49
- export function addExchange(
50
- chatId: string,
51
- user: string,
52
- response: string,
53
- backend: "claude" | "codex",
54
- ) {
55
- const normalizedResponse = normalizeResponse(response);
56
- const entry: Exchange = { ts: Date.now(), chatId, user, response: normalizedResponse, backend };
57
- history.push(entry);
58
-
59
- // Prune old entries per chat
60
- const chatEntries = history.filter((e) => e.chatId === chatId);
61
- if (chatEntries.length > MAX_ENTRIES_PER_CHAT) {
62
- const toRemove = chatEntries.length - MAX_ENTRIES_PER_CHAT;
63
- let removed = 0;
64
- history = history.filter((e) => {
65
- if (e.chatId === chatId && removed < toRemove) {
66
- removed++;
67
- return false;
68
- }
69
- return true;
70
- });
71
- }
72
-
73
- // Persist
74
- try {
75
- const dir = dirname(HISTORY_PATH);
76
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
77
- // Rewrite full file after prune to keep it clean
78
- writeFileSync(HISTORY_PATH, history.map((e) => JSON.stringify(e)).join("\n") + "\n");
79
- } catch (e) {
80
- log.warn(`Failed to persist history: ${e}`);
81
- }
82
- }
83
-
84
- /**
85
- * Returns context string with exchanges from the OTHER backend
86
- * that happened since the current backend was last used.
87
- * Returns empty string if there's nothing to inject.
88
- */
89
- export function getForeignContext(
90
- chatId: string,
91
- currentBackend: "claude" | "codex",
92
- limit = 10,
93
- ): string {
94
- const chatHistory = history.filter((e) => e.chatId === chatId);
95
- if (chatHistory.length === 0) return "";
96
-
97
- // Find last exchange handled by current backend
98
- let lastOwnIdx = -1;
99
- for (let i = chatHistory.length - 1; i >= 0; i--) {
100
- if (chatHistory[i].backend === currentBackend) {
101
- lastOwnIdx = i;
102
- break;
103
- }
104
- }
105
-
106
- const cutoff = Date.now() - FOREIGN_CONTEXT_MAX_AGE_MS;
107
-
108
- // Get foreign exchanges since then
109
- const foreign = chatHistory
110
- .slice(lastOwnIdx + 1)
111
- .filter((e) => e.backend !== currentBackend && e.ts >= cutoff);
112
-
113
- if (foreign.length === 0) return "";
114
-
115
- const otherName = currentBackend === "claude" ? "Codex" : "Claude";
116
- const lines = foreign
117
- .slice(-limit)
118
- .map((e) => `User: ${e.user}\n${otherName}: ${normalizeResponse(e.response)}`)
119
- .join("\n\n");
120
-
121
- return `[Contexto previo con ${otherName}]\n${lines}\n[Fin del contexto previo]\n\n`;
122
- }
123
-
124
- /**
125
- * Returns recent conversation history for this chat, formatted as User/Assistant pairs.
126
- * Trims oldest entries first if total exceeds maxChars.
127
- * Returns "" for new conversations.
128
- */
129
- export function getRecentHistory(
130
- chatId: string,
131
- limit = 10,
132
- maxChars = 8000,
133
- ): string {
134
- const chatHistory = history.filter((e) => e.chatId === chatId);
135
- if (chatHistory.length === 0) return "";
136
-
137
- const recent = chatHistory.slice(-limit);
138
-
139
- // Format exchanges
140
- const formatted = recent.map(
141
- (e) => `User: ${e.user}\nAssistant: ${normalizeResponse(e.response)}`,
142
- );
143
-
144
- // Trim oldest entries if total exceeds maxChars
145
- let total = formatted.join("\n\n").length;
146
- while (formatted.length > 1 && total > maxChars) {
147
- formatted.shift();
148
- total = formatted.join("\n\n").length;
149
- }
150
-
151
- if (formatted.length === 0) return "";
152
-
153
- return `[Conversation history]\n${formatted.join("\n\n")}\n[End of conversation history]\n\n`;
154
- }
155
-
156
- /**
157
- * Removes all history entries for this chat from memory and disk.
158
- */
159
- export function clearHistory(chatId: string): void {
160
- const before = history.length;
161
- history = history.filter((e) => e.chatId !== chatId);
162
- const removed = before - history.length;
163
-
164
- if (removed > 0) {
165
- log.info(`Cleared ${removed} history entries for chat ${chatId}`);
166
- try {
167
- writeFileSync(HISTORY_PATH, history.map((e) => JSON.stringify(e)).join("\n") + "\n");
168
- } catch (e) {
169
- log.warn(`Failed to persist history after clear: ${e}`);
170
- }
171
- }
172
- }
173
-
174
- export function getLastBackend(chatId: string): "claude" | "codex" | null {
175
- for (let i = history.length - 1; i >= 0; i--) {
176
- if (history[i].chatId === chatId) {
177
- return history[i].backend;
178
- }
179
- }
180
- return null;
181
- }
182
-
183
- function normalizeResponse(response: string): string {
184
- const cleaned = response
185
- .replace(/\n---CHUNK---\n/g, "\n")
186
- .replace(new RegExp(`^${escapeRegExp(CODEX_SWITCH_NOTICE)}\\s*`, "m"), "")
187
- .trim();
188
- return cleaned;
189
- }
190
-
191
- function escapeRegExp(s: string): string {
192
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
193
- }