twinclaw 1.3.0 → 1.3.1

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.
@@ -1,54 +1,41 @@
1
- var _a;
2
1
  import { logThought } from '../utils/logger.js';
3
2
  import { checkFirstInteraction, loadUserMemory, saveUserMemory, getDefaultUserMemory, } from '../services/user-memory.js';
3
+ const ONBOARDING_SYSTEM_PROMPT = `You are having a natural conversation to get to know the user better. Your goal is to learn about them in a friendly, casual way - like two people chatting over coffee.
4
+
5
+ Topics to naturally explore (one at a time, naturally):
6
+ - What they do for work/study
7
+ - What they're currently working on or interested in
8
+ - What tools/tech they use
9
+ - What they like to do outside of work
10
+ - How they prefer to communicate (brief vs detailed, technical vs simple)
11
+ - What their goals are
12
+
13
+ Guidelines:
14
+ - Be conversational and warm, not robotic
15
+ - Ask one question at a time, building on what they share
16
+ - Don't use numbered lists or formal questionnaires
17
+ - If they share multiple things, acknowledge them naturally before asking follow-ups
18
+ - When you feel you've learned enough (after 3-5 exchanges), ask if they'd like you to remember this info
19
+
20
+ Important: Extract key information from their responses and include it in your response as a JSON comment like:
21
+ <!-- INFO: {"role": "developer", "interests": "AI, music", "goals": "building automation"} -->
22
+
23
+ This JSON will be automatically parsed to save their profile. Use these keys:
24
+ - role (what they do)
25
+ - company (where they work)
26
+ - currentProject (what they're working on)
27
+ - tools (comma-separated list of tools they use)
28
+ - interests (what they like doing)
29
+ - communicationPreference (brief, detailed, or contextual)
30
+ - frustrations (what frustrates them about AI)
31
+ - goals (what they're trying to achieve)
32
+ - technicalLevel (beginner, intermediate, or expert)`;
4
33
  export class SelfSetupAgent {
5
34
  #userId;
6
- #phase = 'detecting';
7
- #discoveryAnswers = {};
8
- #discoveryQuestionIndex = 0;
9
- #messageCount = 0;
10
- static DISCOVERY_QUESTIONS = [
11
- {
12
- category: 'professional',
13
- question: "What do you do? (Role, company, industry)",
14
- field: 'role',
15
- },
16
- {
17
- category: 'professional',
18
- question: "What are you working on right now? (Current projects)",
19
- field: 'currentProject',
20
- },
21
- {
22
- category: 'professional',
23
- question: "What tools do you use daily? (Tech stack, platforms)",
24
- field: 'tools',
25
- },
26
- {
27
- category: 'personal',
28
- question: "What do you like to do outside of work?",
29
- field: 'interests',
30
- },
31
- {
32
- category: 'communication',
33
- question: "How do you like information presented? (Brief vs detailed, casual vs formal)",
34
- field: 'communicationPreference',
35
- },
36
- {
37
- category: 'communication',
38
- question: "What frustrates you about AI assistants?",
39
- field: 'frustrations',
40
- },
41
- {
42
- category: 'goals',
43
- question: "What are you trying to achieve? (Short and long term)",
44
- field: 'goals',
45
- },
46
- {
47
- category: 'technical',
48
- question: "How technical should I be with you?",
49
- field: 'technicalLevel',
50
- },
51
- ];
35
+ #phase = 'idle';
36
+ #conversationTurn = 0;
37
+ #extractedInfo = {};
38
+ #confirmed = false;
52
39
  constructor(userId) {
53
40
  this.#userId = userId;
54
41
  }
@@ -61,215 +48,147 @@ export class SelfSetupAgent {
61
48
  isComplete() {
62
49
  return this.#phase === 'completed';
63
50
  }
64
- processFirstMessage(userMessage) {
65
- this.#messageCount++;
66
- if (this.#phase === 'detecting') {
67
- return this.#startWarmIntro();
68
- }
69
- if (this.#phase === 'warm_intro') {
70
- return this.#handleWarmIntroResponse(userMessage);
71
- }
72
- if (this.#phase === 'discovery') {
73
- return this.#handleDiscoveryResponse(userMessage);
74
- }
75
- if (this.#phase === 'synthesis') {
76
- return this.#handleSynthesisResponse(userMessage);
77
- }
78
- if (this.#phase === 'completed') {
51
+ getSystemPrompt() {
52
+ return ONBOARDING_SYSTEM_PROMPT;
53
+ }
54
+ getConversationHistory() {
55
+ return [
56
+ {
57
+ role: 'system',
58
+ content: ONBOARDING_SYSTEM_PROMPT,
59
+ },
60
+ ];
61
+ }
62
+ processLLMResponse(llmText) {
63
+ this.#conversationTurn++;
64
+ const extracted = this.#extractInfo(llmText);
65
+ if (extracted) {
66
+ this.#extractedInfo = { ...this.#extractedInfo, ...extracted };
67
+ }
68
+ if (this.#conversationTurn >= 4 && !this.#confirmed) {
69
+ this.#phase = 'confirming';
79
70
  return {
80
- text: "Setup is complete! How can I help you today?",
71
+ text: llmText,
81
72
  phase: this.#phase,
82
- isComplete: true,
83
- needsMoreInfo: false,
73
+ isComplete: false,
74
+ needsMoreInfo: true,
75
+ extractedInfo: this.#extractedInfo,
84
76
  };
85
77
  }
86
- return {
87
- text: "I'm not sure what to do next. Let me start over.",
88
- phase: this.#phase,
89
- isComplete: false,
90
- needsMoreInfo: true,
91
- };
92
- }
93
- #startWarmIntro() {
94
- this.#phase = 'warm_intro';
95
- logThought(`[SelfSetupAgent] Starting warm intro for user: ${this.#userId}`);
96
- return {
97
- text: `Hey! I'm noticing this is our first conversation. Before we dive into anything, I'd love to take a few minutes to get to know you better so I can be actually helpful (not just generic AI helpful).
98
-
99
- Think of this as a quick coffee chat where I learn what makes you tick. Cool?
100
-
101
- To start - what do you do, and what kind of thing are you looking to build?`,
102
- phase: this.#phase,
103
- isComplete: false,
104
- needsMoreInfo: true,
105
- };
106
- }
107
- #handleWarmIntroResponse(userMessage) {
108
- this.#discoveryAnswers.role = userMessage;
109
- this.#phase = 'discovery';
110
- this.#discoveryQuestionIndex = 0;
111
- return this.#askNextDiscoveryQuestion();
112
- }
113
- #askNextDiscoveryQuestion() {
114
- const question = _a.DISCOVERY_QUESTIONS[this.#discoveryQuestionIndex];
115
- if (!question) {
116
- return this.#moveToSynthesis();
78
+ if (this.#confirmed) {
79
+ return this.#completeSetup();
117
80
  }
118
- const questionText = question.question;
81
+ this.#phase = 'learning';
119
82
  return {
120
- text: questionText,
83
+ text: llmText,
121
84
  phase: this.#phase,
122
85
  isComplete: false,
123
86
  needsMoreInfo: true,
87
+ extractedInfo: this.#extractedInfo,
124
88
  };
125
89
  }
126
- #handleDiscoveryResponse(userMessage) {
127
- const question = _a.DISCOVERY_QUESTIONS[this.#discoveryQuestionIndex];
128
- if (question) {
129
- const field = question.field;
130
- this.#discoveryAnswers[field] = userMessage;
131
- }
132
- this.#discoveryQuestionIndex++;
133
- if (this.#discoveryQuestionIndex >= _a.DISCOVERY_QUESTIONS.length) {
134
- return this.#moveToSynthesis();
135
- }
136
- return this.#askNextDiscoveryQuestion();
90
+ confirmAndSave() {
91
+ this.#confirmed = true;
92
+ return this.#completeSetup();
137
93
  }
138
- #moveToSynthesis() {
139
- this.#phase = 'synthesis';
140
- const answers = this.#discoveryAnswers;
141
- const synthesis = this.#buildSynthesis(answers);
94
+ skipOrDecline() {
95
+ this.#phase = 'completed';
142
96
  return {
143
- text: `${synthesis}
144
-
145
- Did I get that right? Anything I should adjust or add?`,
97
+ text: "No problem! Whenever you're ready to share more about yourself, just let me know. How can I help you today?",
146
98
  phase: this.#phase,
147
- isComplete: false,
148
- needsMoreInfo: true,
99
+ isComplete: true,
100
+ needsMoreInfo: false,
149
101
  };
150
102
  }
151
- #buildSynthesis(answers) {
152
- const insights = [];
153
- if (answers.role) {
154
- insights.push(`You're working as ${answers.role}`);
155
- }
156
- if (answers.currentProject) {
157
- insights.push(`currently focused on ${answers.currentProject}`);
158
- }
159
- if (answers.tools && answers.tools.length > 0) {
160
- insights.push(`using tools like ${answers.tools}`);
161
- }
162
- if (answers.interests) {
163
- insights.push(`into ${answers.interests} outside work`);
164
- }
165
- let preference = 'contextual';
166
- if (answers.communicationPreference) {
167
- if (answers.communicationPreference.includes('brief')) {
168
- preference = 'brief';
103
+ #completeSetup() {
104
+ if (Object.keys(this.#extractedInfo).length > 0) {
105
+ const memory = getDefaultUserMemory();
106
+ if (this.#extractedInfo.role) {
107
+ memory.profile.identity.role = String(this.#extractedInfo.role);
169
108
  }
170
- else if (answers.communicationPreference.includes('detail')) {
171
- preference = 'comprehensive';
109
+ if (this.#extractedInfo.company) {
110
+ memory.profile.identity.company = String(this.#extractedInfo.company);
172
111
  }
112
+ if (this.#extractedInfo.currentProject) {
113
+ memory.profile.professionalBackground.currentFocus = String(this.#extractedInfo.currentProject);
114
+ }
115
+ if (this.#extractedInfo.tools) {
116
+ const tools = String(this.#extractedInfo.tools).split(',').map(t => t.trim()).filter(Boolean);
117
+ memory.profile.professionalBackground.toolsAndPlatforms = tools;
118
+ }
119
+ if (this.#extractedInfo.interests) {
120
+ memory.profile.personalContext.interests = String(this.#extractedInfo.interests).split(',').map(t => t.trim()).filter(Boolean);
121
+ }
122
+ if (this.#extractedInfo.goals) {
123
+ memory.profile.goals.shortTerm = [String(this.#extractedInfo.goals)];
124
+ }
125
+ if (this.#extractedInfo.technicalLevel) {
126
+ const level = String(this.#extractedInfo.technicalLevel).toLowerCase();
127
+ if (level.includes('beginner')) {
128
+ memory.profile.identity.technicalLevel = 'beginner';
129
+ }
130
+ else if (level.includes('expert') || level.includes('advanced')) {
131
+ memory.profile.identity.technicalLevel = 'expert';
132
+ }
133
+ else {
134
+ memory.profile.identity.technicalLevel = 'intermediate';
135
+ }
136
+ }
137
+ if (this.#extractedInfo.communicationPreference) {
138
+ const pref = String(this.#extractedInfo.communicationPreference).toLowerCase();
139
+ if (pref.includes('brief')) {
140
+ memory.profile.communicationStyle.detailLevel = 'brief';
141
+ }
142
+ else if (pref.includes('detail')) {
143
+ memory.profile.communicationStyle.detailLevel = 'comprehensive';
144
+ }
145
+ else {
146
+ memory.profile.communicationStyle.detailLevel = 'contextual';
147
+ }
148
+ }
149
+ if (this.#extractedInfo.frustrations) {
150
+ memory.profile.communicationStyle.frustrations = [String(this.#extractedInfo.frustrations)];
151
+ }
152
+ saveUserMemory(this.#userId, memory);
153
+ logThought(`[SelfSetupAgent] Saved user profile for: ${this.#userId}`);
173
154
  }
174
- insights.push(`prefer ${preference} communication`);
175
- if (answers.goals) {
176
- insights.push(`your goals include ${answers.goals}`);
177
- }
178
- const synthesis = insights.join('. ');
179
- return `Alright, here's what I'm picking up:\n- ${insights.join('\n- ')}`;
180
- }
181
- #handleSynthesisResponse(userMessage) {
182
- const lowerResponse = userMessage.toLowerCase();
183
- if (lowerResponse.includes('yes') || lowerResponse.includes('right') || lowerResponse.includes('correct') || lowerResponse.includes('yep')) {
184
- return this.#completeSetup();
185
- }
186
- return this.#completeSetup();
187
- }
188
- #completeSetup() {
189
- this.#phase = 'file_creation';
190
- const memory = this.#buildUserMemory();
191
- saveUserMemory(this.#userId, memory);
192
155
  this.#phase = 'completed';
193
- logThought(`[SelfSetupAgent] Completed setup for user: ${this.#userId}`);
194
156
  return {
195
- text: `Cool, I've saved all this so I don't forget. I created a few quick reference files:
196
- - Your profile & preferences
197
- - How you like me to communicate
198
- - What you're currently working on
199
- - Quick facts I can reference
157
+ text: `Got it! I've saved what you shared about yourself. I'll use this to tailor my responses to you.
200
158
 
201
- These will help me be consistent and actually useful. I'll update them as I learn more about you.
202
-
203
- We're all set! From now on, I'll:
204
- ✓ Remember our conversations and what matters to you
205
- ✓ Adapt my style to what works for you
159
+ From now on, I'll:
160
+ ✓ Remember what matters to you
161
+ Adapt my communication style to your preferences
206
162
  ✓ Keep track of your projects and goals
207
- ✓ Update my notes as things change
208
163
 
209
- If I ever seem off-base or forget something important, just call me out. Ready to jump in?`,
164
+ If anything changes or you want to update your info, just let me know. Ready to dive in!`,
210
165
  phase: this.#phase,
211
166
  isComplete: true,
212
167
  needsMoreInfo: false,
168
+ extractedInfo: this.#extractedInfo,
213
169
  };
214
170
  }
215
- #buildUserMemory() {
216
- const defaultMemory = getDefaultUserMemory();
217
- const answers = this.#discoveryAnswers;
218
- defaultMemory.profile.identity.role = answers.role;
219
- defaultMemory.profile.identity.technicalLevel = this.#mapTechnicalLevel(answers.technicalLevel);
220
- defaultMemory.profile.professionalBackground.currentFocus = answers.currentProject;
221
- if (answers.tools) {
222
- defaultMemory.profile.professionalBackground.toolsAndPlatforms = Array.isArray(answers.tools)
223
- ? answers.tools
224
- : [answers.tools];
225
- }
226
- defaultMemory.profile.personalContext.interests = answers.interests ? [answers.interests] : [];
227
- if (answers.communicationPreference) {
228
- defaultMemory.profile.communicationStyle.detailLevel = this.#mapDetailLevel(answers.communicationPreference);
171
+ #extractInfo(text) {
172
+ const infoMatch = text.match(/<!--\s*INFO:\s*(\{[^}]+\})\s*-->/);
173
+ if (!infoMatch) {
174
+ return null;
229
175
  }
230
- if (answers.frustrations) {
231
- defaultMemory.profile.communicationStyle.frustrations = Array.isArray(answers.frustrations)
232
- ? answers.frustrations
233
- : [answers.frustrations];
176
+ try {
177
+ return JSON.parse(infoMatch[1]);
234
178
  }
235
- if (answers.goals) {
236
- defaultMemory.profile.goals.shortTerm = [answers.goals];
179
+ catch {
180
+ logThought(`[SelfSetupAgent] Failed to parse extracted info: ${infoMatch[1]}`);
181
+ return null;
237
182
  }
238
- return defaultMemory;
239
- }
240
- #mapTechnicalLevel(level) {
241
- if (!level)
242
- return undefined;
243
- const lower = level.toLowerCase();
244
- if (lower.includes('expert') || lower.includes('advanced'))
245
- return 'expert';
246
- if (lower.includes('beginner') || lower.includes('new'))
247
- return 'beginner';
248
- return 'intermediate';
249
- }
250
- #mapDetailLevel(pref) {
251
- if (!pref)
252
- return undefined;
253
- const lower = pref.toLowerCase();
254
- if (lower.includes('brief') || lower.includes('short'))
255
- return 'brief';
256
- if (lower.includes('detail') || lower.includes('comprehensive'))
257
- return 'comprehensive';
258
- return 'contextual';
259
- }
260
- cancel() {
261
- this.#phase = 'cancelled';
262
- logThought(`[SelfSetupAgent] Setup cancelled for user: ${this.#userId}`);
263
183
  }
264
184
  getState() {
265
185
  return {
266
186
  phase: this.#phase,
267
- messageCount: this.#messageCount,
268
- answersCount: Object.keys(this.#discoveryAnswers).length,
187
+ turn: this.#conversationTurn,
188
+ extractedCount: Object.keys(this.#extractedInfo).length,
269
189
  };
270
190
  }
271
191
  }
272
- _a = SelfSetupAgent;
273
192
  export function createSelfSetupAgent(userId) {
274
193
  if (!checkFirstInteraction(userId)) {
275
194
  return null;
@@ -279,3 +198,10 @@ export function createSelfSetupAgent(userId) {
279
198
  export function getExistingUserMemory(userId) {
280
199
  return loadUserMemory(userId);
281
200
  }
201
+ export function getOrCreateAgent(userId, existing) {
202
+ if (existing && existing.phase !== 'completed') {
203
+ return existing;
204
+ }
205
+ const newAgent = createSelfSetupAgent(userId);
206
+ return newAgent ?? existing;
207
+ }
@@ -170,24 +170,44 @@ export class Gateway {
170
170
  const sessionId = `${message.platform}:${message.senderId}`;
171
171
  const userId = message.senderId;
172
172
  const existingAgent = this.#selfSetupAgents.get(userId);
173
- if (existingAgent) {
174
- const response = existingAgent.processFirstMessage(normalizedText);
175
- if (response.isComplete) {
176
- this.#selfSetupAgents.delete(userId);
177
- if (response.isComplete && !response.needsMoreInfo) {
178
- return response.text;
179
- }
180
- }
181
- return response.text;
173
+ if (existingAgent && !existingAgent.isComplete()) {
174
+ return this.#runOnboardingConversation(existingAgent, normalizedText, sessionId);
182
175
  }
183
176
  const newAgent = createSelfSetupAgent(userId);
184
177
  if (newAgent) {
185
178
  this.#selfSetupAgents.set(userId, newAgent);
186
- const response = newAgent.processFirstMessage(normalizedText);
187
- return response.text;
179
+ return this.#runOnboardingConversation(newAgent, normalizedText, sessionId);
188
180
  }
189
181
  return this.processText(sessionId, normalizedText);
190
182
  }
183
+ async #runOnboardingConversation(agent, userMessage, sessionId) {
184
+ const messages = [
185
+ { role: 'system', content: agent.getSystemPrompt() },
186
+ ...agent.getConversationHistory(),
187
+ { role: 'user', content: userMessage },
188
+ ];
189
+ try {
190
+ const response = await this.#router.createChatCompletion(messages, undefined, { sessionId });
191
+ if (!response.content) {
192
+ return "I'm having trouble thinking right now. Let's try again.";
193
+ }
194
+ const result = agent.processLLMResponse(response.content);
195
+ if (result.isComplete && !result.needsMoreInfo) {
196
+ this.#selfSetupAgents.delete(agent.userId);
197
+ return result.text;
198
+ }
199
+ if (result.phase === 'confirming') {
200
+ const confirmMessage = result.text + "\n\nWould you like me to remember this? (yes/no or just tell me more)";
201
+ return confirmMessage;
202
+ }
203
+ return result.text;
204
+ }
205
+ catch (err) {
206
+ const errorMsg = err instanceof Error ? err.message : String(err);
207
+ logThought(`[SelfSetupAgent] LLM error: ${errorMsg}`);
208
+ return "I'm having trouble continuing our conversation. Let's try again - what would you like to talk about?";
209
+ }
210
+ }
191
211
  async processText(sessionId, text) {
192
212
  const normalizedText = text.trim();
193
213
  if (!normalizedText) {
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { MODEL_SLOT_IDS, } from '../types/model-routing.js';
3
- import { scrubSensitiveText } from '../utils/logger.js';
3
+ import { scrubSensitiveText, logThought } from '../utils/logger.js';
4
4
  import { getModelRoutingSetting, listModelRoutingEvents, saveModelRoutingEvent, saveModelRoutingSetting, } from './db-model-routing.js';
5
5
  import { getSecretVaultService } from './secret-vault.js';
6
6
  import { getGitHubCopilotService } from './github-copilot-service.js';
@@ -434,11 +434,18 @@ export class ModelRouter {
434
434
  }
435
435
  async getApiKey(config) {
436
436
  const envName = config.apiKeyEnvName;
437
+ const providerId = this.resolveProviderId(config);
437
438
  const key = getSecretVaultService().readSecret(envName);
438
439
  if (!key) {
439
- console.warn(`Warning: API key ${envName} is not set in environment.`);
440
+ const configKey = getConfigValue(envName);
441
+ if (configKey) {
442
+ logThought(`[Router] Using API key from config for ${providerId}: ${envName.slice(0, 8)}...`);
443
+ return configKey;
444
+ }
445
+ console.warn(`[Router] Warning: API key ${envName} is not set in environment or config (provider: ${providerId}).`);
440
446
  return '';
441
447
  }
448
+ logThought(`[Router] Using API key from vault for ${providerId}: ${envName.slice(0, 8)}...`);
442
449
  if (envName !== 'GITHUB_TOKEN') {
443
450
  return key;
444
451
  }
@@ -1009,19 +1016,26 @@ export class ModelRouter {
1009
1016
  }
1010
1017
  loadModelsFromConfig() {
1011
1018
  const configModels = [];
1019
+ const definitionsJson = getConfigValue('MODEL_DEFINITIONS');
1020
+ const primaryModel = getConfigValue('PRIMARY_MODEL');
1021
+ logThought(`[Router] loadModelsFromConfig: PRIMARY_MODEL=${primaryModel}, MODEL_DEFINITIONS present=${!!definitionsJson}`);
1012
1022
  // First, check if config.models.definitions has any models (from model picker)
1013
1023
  // If so, use those instead of hardcoded fallbacks
1014
- const definitionsJson = getConfigValue('MODEL_DEFINITIONS');
1015
1024
  if (definitionsJson) {
1016
1025
  try {
1017
1026
  const definitions = JSON.parse(definitionsJson);
1018
1027
  if (definitions && definitions.length > 0) {
1019
1028
  const primaryModel = getConfigValue('PRIMARY_MODEL') ?? '';
1029
+ // Normalize model ID for comparison (modal/zai-glm-5-fp8 -> modal-zai-glm-5-fp8)
1030
+ const normalizeId = (id) => id.replace('/', '-').toLowerCase();
1031
+ const normalizedPrimary = normalizeId(primaryModel);
1020
1032
  // Sort models so primary is first, then the rest
1021
1033
  const sortedDefinitions = [...definitions].sort((a, b) => {
1022
- if (a.id === primaryModel)
1034
+ const aNorm = normalizeId(a.id);
1035
+ const bNorm = normalizeId(b.id);
1036
+ if (aNorm === normalizedPrimary)
1023
1037
  return -1;
1024
- if (b.id === primaryModel)
1038
+ if (bNorm === normalizedPrimary)
1025
1039
  return 1;
1026
1040
  return 0;
1027
1041
  });
@@ -1033,13 +1047,36 @@ export class ModelRouter {
1033
1047
  apiKeyEnvName: def.apiKeyEnvName,
1034
1048
  });
1035
1049
  }
1036
- // If we have models from definitions, return them
1037
- if (configModels.length > 0) {
1038
- return configModels;
1050
+ // Even with definitions, add fallback providers in case primary fails
1051
+ // This ensures we have working alternatives if the user's primary model fails
1052
+ const groqApiKey = getConfigValue('GROQ_API_KEY');
1053
+ const openRouterApiKey = getConfigValue('OPENROUTER_API_KEY');
1054
+ const existingIds = new Set(configModels.map(m => m.id));
1055
+ // Add Groq as fallback if not already in definitions
1056
+ if (groqApiKey && !existingIds.has('groq-qwen3-32b') && !existingIds.has(MODEL_SLOT_IDS.FALLBACK_1)) {
1057
+ configModels.push({
1058
+ id: MODEL_SLOT_IDS.FALLBACK_1,
1059
+ model: 'qwen/qwen3-32b',
1060
+ baseURL: 'https://api.groq.com/openai/v1/chat/completions',
1061
+ apiKeyEnvName: 'GROQ_API_KEY',
1062
+ });
1063
+ }
1064
+ // Add OpenRouter free model as secondary fallback
1065
+ if (openRouterApiKey && !existingIds.has('openrouter-arcee-ai-trinity-mini-free') && !existingIds.has(MODEL_SLOT_IDS.FALLBACK_2)) {
1066
+ configModels.push({
1067
+ id: MODEL_SLOT_IDS.FALLBACK_2,
1068
+ model: 'arcee-ai/trinity-mini:free',
1069
+ baseURL: 'https://openrouter.ai/api/v1/chat/completions',
1070
+ apiKeyEnvName: 'OPENROUTER_API_KEY',
1071
+ });
1039
1072
  }
1073
+ logThought(`[Router] Loaded ${configModels.length} models from definitions. PRIMARY: ${primaryModel}`);
1074
+ // Return all models including fallbacks
1075
+ return configModels;
1040
1076
  }
1041
1077
  }
1042
- catch {
1078
+ catch (e) {
1079
+ logThought(`[Router] Failed to load definitions: ${e}`);
1043
1080
  // JSON parse failed, fall through to legacy logic
1044
1081
  }
1045
1082
  }
@@ -1158,6 +1195,7 @@ export class ModelRouter {
1158
1195
  apiKeyEnvName: 'MODAL_API_KEY',
1159
1196
  });
1160
1197
  }
1198
+ logThought(`[Router] Final loaded models (${configModels.length}): ${configModels.map(m => `${m.id}->${m.model.split('/').pop()}`).join(', ')}`);
1161
1199
  return configModels;
1162
1200
  }
1163
1201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twinclaw",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Eagle-eyed agentic AI gateway with multi-modal hooks and proactive memory.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {