@exreve/exk 1.0.44 → 1.0.46

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.
@@ -135,6 +135,9 @@ function lookupToolNameFromHistory(messages, toolUseId) {
135
135
  // (Do not read ANTHROPIC_* / CLAUDE_MODEL from the host environment — only this file + code default model.)
136
136
  const AI_CONFIG_PATH = path.join(os.homedir(), '.talk-to-code', 'ai-config.json');
137
137
  const DEFAULT_AI_MODEL = 'glm-5.1';
138
+ /** TTL cache for ai-config.json reads to avoid hitting disk on every call */
139
+ let _aiConfigCache = null;
140
+ const AI_CONFIG_TTL_MS = 5_000;
138
141
  const PROVIDERS = {
139
142
  zai: {
140
143
  apiKey: process.env.ZHIPU_API_KEY || '',
@@ -184,6 +187,10 @@ function resolveProvider(model, providerId) {
184
187
  };
185
188
  }
186
189
  function loadAiConfig() {
190
+ const now = Date.now();
191
+ if (_aiConfigCache && (now - _aiConfigCache.ts) < AI_CONFIG_TTL_MS) {
192
+ return _aiConfigCache.data;
193
+ }
187
194
  try {
188
195
  const data = readFileSync(AI_CONFIG_PATH, 'utf-8');
189
196
  const config = JSON.parse(data);
@@ -193,10 +200,14 @@ function loadAiConfig() {
193
200
  const proxy = typeof config.proxy === 'string' ? config.proxy.trim() : '';
194
201
  const minimaxApiKey = typeof config.minimaxApiKey === 'string' ? config.minimaxApiKey.trim() : '';
195
202
  const openrouterApiKey = typeof config.openrouterApiKey === 'string' ? config.openrouterApiKey.trim() : '';
196
- return { apiKey, baseUrl, model, proxy, minimaxApiKey, openrouterApiKey };
203
+ const result = { apiKey, baseUrl, model, proxy, minimaxApiKey, openrouterApiKey };
204
+ _aiConfigCache = { data: result, ts: now };
205
+ return result;
197
206
  }
198
207
  catch {
199
- return { apiKey: '', baseUrl: '', model: DEFAULT_AI_MODEL, proxy: '', minimaxApiKey: '', openrouterApiKey: '' };
208
+ const fallback = { apiKey: '', baseUrl: '', model: DEFAULT_AI_MODEL, proxy: '', minimaxApiKey: '', openrouterApiKey: '' };
209
+ _aiConfigCache = { data: fallback, ts: now };
210
+ return fallback;
200
211
  }
201
212
  }
202
213
  /** Get OpenRouter API key from ai-config.json (served by backend) */
@@ -240,7 +251,7 @@ function readProxyToggle() {
240
251
  /** Env for the Claude Code child: copy of host env with host ANTHROPIC_* stripped, then inject from provider or ai-config.
241
252
  * If a local model is provided, override baseUrl to point to the anthropic-proxy adapter.
242
253
  * If resolvedProvider is provided, use its credentials instead of ai-config defaults. */
243
- function envForClaudeCodeChild(localModel, resolvedProvider) {
254
+ function envForClaudeCodeChild(_localModel, resolvedProvider) {
244
255
  const env = { ...process.env };
245
256
  // Strip any host ANTHROPIC_* vars to prevent leaking credentials or wrong URLs
246
257
  delete env.ANTHROPIC_API_KEY;
@@ -322,6 +333,71 @@ export class AgentSessionManager {
322
333
  promptAbortControllers = new Map(); // Map promptId -> AbortController for cancellation
323
334
  emergencyStopInProgress = new Set(); // Track sessions being emergency stopped
324
335
  sessionHandlers = new Map(); // Track handlers for each session
336
+ socketRef = null; // Socket.IO reference for fetching session history from backend
337
+ /** Set the socket reference for backend communication (called from app-child.ts) */
338
+ setSocketRef(socket) {
339
+ this.socketRef = socket;
340
+ }
341
+ /**
342
+ * Fetch conversation history from the backend DB for a session.
343
+ * Returns array of { role, content } pairs (user prompts + assistant responses).
344
+ */
345
+ async fetchSessionHistory(sessionId) {
346
+ return new Promise((resolve) => {
347
+ if (!this.socketRef?.connected) {
348
+ console.log(`[AgentSessionManager] Cannot fetch history: socket not connected`);
349
+ resolve([]);
350
+ return;
351
+ }
352
+ const timeoutId = setTimeout(() => {
353
+ console.warn(`[AgentSessionManager] fetchSessionHistory timed out for ${sessionId}`);
354
+ resolve([]);
355
+ }, 5000);
356
+ this.socketRef.emit('session:history', { sessionId }, (response) => {
357
+ clearTimeout(timeoutId);
358
+ if (response?.history && Array.isArray(response.history)) {
359
+ console.log(`[AgentSessionManager] Fetched ${response.history.length} history entries for session ${sessionId}`);
360
+ resolve(response.history);
361
+ }
362
+ else {
363
+ resolve([]);
364
+ }
365
+ });
366
+ });
367
+ }
368
+ /**
369
+ * Format conversation history into a text block for injection into a prompt.
370
+ * Takes the last N exchanges to avoid token overflow.
371
+ */
372
+ formatHistoryForPrompt(history) {
373
+ if (!history.length)
374
+ return '';
375
+ // Take last N entries to stay within reasonable token limits
376
+ const maxEntries = 40;
377
+ const trimmed = history.slice(-maxEntries);
378
+ const lines = trimmed.map(m => {
379
+ const content = typeof m.content === 'string' ? m.content : JSON.stringify(m.content);
380
+ // Truncate very long individual messages
381
+ const maxLen = 2000;
382
+ const truncated = content.length > maxLen
383
+ ? content.slice(0, maxLen) + '...[truncated]'
384
+ : content;
385
+ return `<${m.role}>\n${truncated}\n</${m.role}>`;
386
+ });
387
+ return [
388
+ '[Previous Conversation Context]',
389
+ 'The following is conversation history from this session that was lost due to a session reset.',
390
+ 'Use it as context for the current request.',
391
+ '',
392
+ '<conversation>',
393
+ ...lines,
394
+ '</conversation>',
395
+ '',
396
+ '[End of Previous Context]',
397
+ '',
398
+ '',
399
+ ].join('\n');
400
+ }
325
401
  async createSession(handler) {
326
402
  const { sessionId, projectPath, model } = handler;
327
403
  const sessionModel = model || CLAUDE_CONFIG.model;
@@ -445,8 +521,10 @@ export class AgentSessionManager {
445
521
  while (session.promptQueue.length > 0 && !this.emergencyStopInProgress.has(sessionId)) {
446
522
  const queuedPrompt = session.promptQueue.shift();
447
523
  const { enhancers, handler, promptId: queuedPromptId, abortController: queuedAbortController } = queuedPrompt;
448
- const { projectPath, promptId, onOutput, onError, onComplete, onStatusUpdate } = handler;
449
- const promptStartTime = Date.now();
524
+ const { projectPath, promptId, onOutput: _onOutput, onError: _onError, onComplete: _onComplete, onStatusUpdate } = handler;
525
+ const onOutput = _onOutput;
526
+ const onError = _onError;
527
+ const onComplete = _onComplete;
450
528
  // Write attachments to temp dir and inject paths into prompt
451
529
  let effectivePrompt = queuedPrompt.prompt;
452
530
  let attachmentDir;
@@ -492,7 +570,9 @@ export class AgentSessionManager {
492
570
  try {
493
571
  for await (const _ of session.activeQueryStream) { }
494
572
  }
495
- catch { }
573
+ catch (err) {
574
+ console.error(`[AgentSession] Error draining active query stream:`, err);
575
+ }
496
576
  session.activeQueryStream = undefined;
497
577
  }
498
578
  session.activeQueryStream = undefined;
@@ -504,6 +584,25 @@ export class AgentSessionManager {
504
584
  finalPrompt = `${skillContent}\n\n${effectivePrompt}`;
505
585
  }
506
586
  }
587
+ // Inject DB history if context was lost in a previous prompt (resume failed)
588
+ if (session.contextLost) {
589
+ console.log(`[agentSession] Context was lost previously, fetching DB history for session ${sessionId}`);
590
+ try {
591
+ const history = await this.fetchSessionHistory(sessionId);
592
+ if (history.length > 0) {
593
+ const historyPrefix = this.formatHistoryForPrompt(history);
594
+ finalPrompt = historyPrefix + finalPrompt;
595
+ console.log(`[agentSession] Injected ${history.length} history entries into prompt for session ${sessionId}`);
596
+ }
597
+ else {
598
+ console.log(`[agentSession] No DB history available for session ${sessionId}`);
599
+ }
600
+ }
601
+ catch (err) {
602
+ console.error(`[agentSession] Failed to fetch/format history:`, err);
603
+ }
604
+ session.contextLost = false; // Reset after injection attempt
605
+ }
507
606
  // Add user message to history
508
607
  session.messages.push({
509
608
  role: 'user',
@@ -566,7 +665,9 @@ export class AgentSessionManager {
566
665
  mkdirSync(cwd2, { recursive: true });
567
666
  }
568
667
  }
569
- catch { }
668
+ catch (err) {
669
+ console.error(`[AgentSession] Failed to create working directory ${cwd2}:`, err);
670
+ }
570
671
  const isWin = process.platform === 'win32';
571
672
  // Ensure PATH includes common node locations, especially in containers
572
673
  const defaultPath = isWin
@@ -767,15 +868,24 @@ export class AgentSessionManager {
767
868
  // Capture Claude SDK session ID from system init message
768
869
  if (message.type === 'system' && message.subtype === 'init') {
769
870
  const systemMsg = message;
770
- if (systemMsg.session_id && !session.claudeSessionId) {
871
+ if (systemMsg.session_id) {
872
+ // Detect context loss: session_id changed unexpectedly (resume failed)
873
+ if (session.claudeSessionId && session.claudeSessionId !== systemMsg.session_id) {
874
+ session.contextLost = true;
875
+ console.warn(`[AgentSessionManager] Context lost! Session ID changed: ${session.claudeSessionId} → ${systemMsg.session_id}`);
876
+ }
771
877
  session.claudeSessionId = systemMsg.session_id;
772
878
  saveSessionState(sessionId, { claudeSessionId: systemMsg.session_id, model: session.model, provider: resolveProvider(session.model).provider, updatedAt: Date.now() });
773
879
  }
774
880
  }
775
881
  if (message.type === 'assistant') {
776
882
  const msg = message;
777
- // Capture Claude session ID from assistant message if not already set
778
- if (msg.session_id && !session.claudeSessionId) {
883
+ // Capture Claude session ID from assistant message (always update to track session changes)
884
+ if (msg.session_id) {
885
+ if (session.claudeSessionId && session.claudeSessionId !== msg.session_id) {
886
+ session.contextLost = true;
887
+ console.warn(`[AgentSessionManager] Context lost! Session ID changed in assistant msg: ${session.claudeSessionId} → ${msg.session_id}`);
888
+ }
779
889
  session.claudeSessionId = msg.session_id;
780
890
  saveSessionState(sessionId, { claudeSessionId: msg.session_id, model: session.model, provider: resolveProvider(session.model).provider, updatedAt: Date.now() });
781
891
  }
@@ -1082,6 +1192,7 @@ export class AgentSessionManager {
1082
1192
  session.totalInputTokens = 0;
1083
1193
  session.totalOutputTokens = 0;
1084
1194
  session.totalCostUsd = 0;
1195
+ session.contextLost = false;
1085
1196
  // Clear Claude session ID to start fresh
1086
1197
  session.claudeSessionId = undefined;
1087
1198
  session.lastUsage = undefined;