@dotsetlabs/dotclaw 1.8.0 → 2.0.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.
Files changed (175) hide show
  1. package/.env.example +6 -0
  2. package/README.md +14 -7
  3. package/config-examples/groups/global/CLAUDE.md +6 -14
  4. package/config-examples/groups/main/CLAUDE.md +8 -39
  5. package/config-examples/runtime.json +4 -4
  6. package/container/Dockerfile +20 -2
  7. package/container/agent-runner/package-lock.json +260 -2
  8. package/container/agent-runner/package.json +2 -1
  9. package/container/agent-runner/src/agent-config.ts +57 -8
  10. package/container/agent-runner/src/browser.ts +180 -0
  11. package/container/agent-runner/src/container-protocol.ts +5 -0
  12. package/container/agent-runner/src/id.ts +3 -2
  13. package/container/agent-runner/src/index.ts +184 -390
  14. package/container/agent-runner/src/ipc.ts +33 -1
  15. package/container/agent-runner/src/mcp-client.ts +222 -0
  16. package/container/agent-runner/src/mcp-registry.ts +163 -0
  17. package/container/agent-runner/src/skill-loader.ts +375 -0
  18. package/container/agent-runner/src/tools.ts +260 -32
  19. package/container/agent-runner/src/tts.ts +61 -0
  20. package/container/agent-runner/tsconfig.json +1 -0
  21. package/dist/admin-commands.d.ts.map +1 -1
  22. package/dist/admin-commands.js +12 -0
  23. package/dist/admin-commands.js.map +1 -1
  24. package/dist/agent-execution.d.ts +3 -1
  25. package/dist/agent-execution.d.ts.map +1 -1
  26. package/dist/agent-execution.js +21 -0
  27. package/dist/agent-execution.js.map +1 -1
  28. package/dist/background-job-classifier.d.ts +1 -1
  29. package/dist/background-job-classifier.d.ts.map +1 -1
  30. package/dist/background-jobs.d.ts.map +1 -1
  31. package/dist/background-jobs.js +30 -10
  32. package/dist/background-jobs.js.map +1 -1
  33. package/dist/cli.js +61 -16
  34. package/dist/cli.js.map +1 -1
  35. package/dist/config.d.ts +0 -2
  36. package/dist/config.d.ts.map +1 -1
  37. package/dist/config.js +1 -3
  38. package/dist/config.js.map +1 -1
  39. package/dist/container-protocol.d.ts +5 -0
  40. package/dist/container-protocol.d.ts.map +1 -1
  41. package/dist/container-runner.d.ts.map +1 -1
  42. package/dist/container-runner.js +33 -14
  43. package/dist/container-runner.js.map +1 -1
  44. package/dist/dashboard.d.ts +5 -0
  45. package/dist/dashboard.d.ts.map +1 -1
  46. package/dist/dashboard.js +11 -5
  47. package/dist/dashboard.js.map +1 -1
  48. package/dist/db.d.ts +0 -13
  49. package/dist/db.d.ts.map +1 -1
  50. package/dist/db.js +41 -70
  51. package/dist/db.js.map +1 -1
  52. package/dist/error-messages.d.ts.map +1 -1
  53. package/dist/error-messages.js +13 -5
  54. package/dist/error-messages.js.map +1 -1
  55. package/dist/hooks.d.ts +7 -0
  56. package/dist/hooks.d.ts.map +1 -0
  57. package/dist/hooks.js +93 -0
  58. package/dist/hooks.js.map +1 -0
  59. package/dist/id.d.ts.map +1 -1
  60. package/dist/id.js +2 -1
  61. package/dist/id.js.map +1 -1
  62. package/dist/index.js +742 -2793
  63. package/dist/index.js.map +1 -1
  64. package/dist/ipc-dispatcher.d.ts +26 -0
  65. package/dist/ipc-dispatcher.d.ts.map +1 -0
  66. package/dist/ipc-dispatcher.js +1044 -0
  67. package/dist/ipc-dispatcher.js.map +1 -0
  68. package/dist/local-embeddings.d.ts +7 -0
  69. package/dist/local-embeddings.d.ts.map +1 -0
  70. package/dist/local-embeddings.js +60 -0
  71. package/dist/local-embeddings.js.map +1 -0
  72. package/dist/maintenance.d.ts.map +1 -1
  73. package/dist/maintenance.js +7 -1
  74. package/dist/maintenance.js.map +1 -1
  75. package/dist/memory-embeddings.d.ts +1 -1
  76. package/dist/memory-embeddings.d.ts.map +1 -1
  77. package/dist/memory-embeddings.js +59 -31
  78. package/dist/memory-embeddings.js.map +1 -1
  79. package/dist/memory-store.d.ts +0 -10
  80. package/dist/memory-store.d.ts.map +1 -1
  81. package/dist/memory-store.js +12 -28
  82. package/dist/memory-store.js.map +1 -1
  83. package/dist/message-pipeline.d.ts +47 -0
  84. package/dist/message-pipeline.d.ts.map +1 -0
  85. package/dist/message-pipeline.js +876 -0
  86. package/dist/message-pipeline.js.map +1 -0
  87. package/dist/metrics.d.ts +8 -8
  88. package/dist/metrics.d.ts.map +1 -1
  89. package/dist/metrics.js.map +1 -1
  90. package/dist/model-registry.d.ts +0 -14
  91. package/dist/model-registry.d.ts.map +1 -1
  92. package/dist/model-registry.js +0 -36
  93. package/dist/model-registry.js.map +1 -1
  94. package/dist/orchestration.d.ts +39 -0
  95. package/dist/orchestration.d.ts.map +1 -0
  96. package/dist/orchestration.js +136 -0
  97. package/dist/orchestration.js.map +1 -0
  98. package/dist/paths.d.ts.map +1 -1
  99. package/dist/paths.js +2 -0
  100. package/dist/paths.js.map +1 -1
  101. package/dist/providers/discord/discord-format.d.ts +16 -0
  102. package/dist/providers/discord/discord-format.d.ts.map +1 -0
  103. package/dist/providers/discord/discord-format.js +153 -0
  104. package/dist/providers/discord/discord-format.js.map +1 -0
  105. package/dist/providers/discord/discord-provider.d.ts +50 -0
  106. package/dist/providers/discord/discord-provider.d.ts.map +1 -0
  107. package/dist/providers/discord/discord-provider.js +604 -0
  108. package/dist/providers/discord/discord-provider.js.map +1 -0
  109. package/dist/providers/discord/index.d.ts +4 -0
  110. package/dist/providers/discord/index.d.ts.map +1 -0
  111. package/dist/providers/discord/index.js +3 -0
  112. package/dist/providers/discord/index.js.map +1 -0
  113. package/dist/providers/registry.d.ts +14 -0
  114. package/dist/providers/registry.d.ts.map +1 -0
  115. package/dist/providers/registry.js +49 -0
  116. package/dist/providers/registry.js.map +1 -0
  117. package/dist/providers/telegram/index.d.ts +4 -0
  118. package/dist/providers/telegram/index.d.ts.map +1 -0
  119. package/dist/providers/telegram/index.js +3 -0
  120. package/dist/providers/telegram/index.js.map +1 -0
  121. package/dist/providers/telegram/telegram-format.d.ts +3 -0
  122. package/dist/providers/telegram/telegram-format.d.ts.map +1 -0
  123. package/dist/providers/telegram/telegram-format.js +215 -0
  124. package/dist/providers/telegram/telegram-format.js.map +1 -0
  125. package/dist/providers/telegram/telegram-provider.d.ts +51 -0
  126. package/dist/providers/telegram/telegram-provider.d.ts.map +1 -0
  127. package/dist/providers/telegram/telegram-provider.js +824 -0
  128. package/dist/providers/telegram/telegram-provider.js.map +1 -0
  129. package/dist/providers/types.d.ts +107 -0
  130. package/dist/providers/types.d.ts.map +1 -0
  131. package/dist/providers/types.js +2 -0
  132. package/dist/providers/types.js.map +1 -0
  133. package/dist/request-router.d.ts.map +1 -1
  134. package/dist/request-router.js +12 -26
  135. package/dist/request-router.js.map +1 -1
  136. package/dist/runtime-config.d.ts +70 -6
  137. package/dist/runtime-config.d.ts.map +1 -1
  138. package/dist/runtime-config.js +119 -65
  139. package/dist/runtime-config.js.map +1 -1
  140. package/dist/skill-manager.d.ts +39 -0
  141. package/dist/skill-manager.d.ts.map +1 -0
  142. package/dist/skill-manager.js +286 -0
  143. package/dist/skill-manager.js.map +1 -0
  144. package/dist/task-scheduler.d.ts.map +1 -1
  145. package/dist/task-scheduler.js +56 -11
  146. package/dist/task-scheduler.js.map +1 -1
  147. package/dist/telegram-format.d.ts.map +1 -1
  148. package/dist/telegram-format.js +16 -1
  149. package/dist/telegram-format.js.map +1 -1
  150. package/dist/tool-intent-probe.d.ts +11 -0
  151. package/dist/tool-intent-probe.d.ts.map +1 -0
  152. package/dist/tool-intent-probe.js +63 -0
  153. package/dist/tool-intent-probe.js.map +1 -0
  154. package/dist/tool-policy.d.ts.map +1 -1
  155. package/dist/tool-policy.js +18 -0
  156. package/dist/tool-policy.js.map +1 -1
  157. package/dist/transcription.d.ts +8 -0
  158. package/dist/transcription.d.ts.map +1 -0
  159. package/dist/transcription.js +174 -0
  160. package/dist/transcription.js.map +1 -0
  161. package/dist/types.d.ts +2 -9
  162. package/dist/types.d.ts.map +1 -1
  163. package/dist/workflow-engine.d.ts +51 -0
  164. package/dist/workflow-engine.d.ts.map +1 -0
  165. package/dist/workflow-engine.js +281 -0
  166. package/dist/workflow-engine.js.map +1 -0
  167. package/dist/workflow-store.d.ts +39 -0
  168. package/dist/workflow-store.d.ts.map +1 -0
  169. package/dist/workflow-store.js +173 -0
  170. package/dist/workflow-store.js.map +1 -0
  171. package/package.json +15 -3
  172. package/scripts/bootstrap.js +40 -4
  173. package/scripts/configure.js +48 -7
  174. package/scripts/doctor.js +30 -4
  175. package/scripts/init.js +13 -6
@@ -7,7 +7,7 @@ import fs from 'fs';
7
7
  import path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { OpenRouter, stepCountIs } from '@openrouter/sdk';
10
- import { createTools, ToolCallRecord } from './tools.js';
10
+ import { createTools, discoverMcpTools, ToolCallRecord } from './tools.js';
11
11
  import { createIpcHandlers } from './ipc.js';
12
12
  import { loadAgentConfig } from './agent-config.js';
13
13
  import { OUTPUT_START_MARKER, OUTPUT_END_MARKER, type ContainerInput, type ContainerOutput } from './container-protocol.js';
@@ -27,6 +27,7 @@ import {
27
27
  Message
28
28
  } from './memory.js';
29
29
  import { loadPromptPackWithCanary, formatPromptPack, PromptPack } from './prompt-packs.js';
30
+ import { buildSkillCatalog, formatSkillCatalog, type SkillCatalog } from './skill-loader.js';
30
31
 
31
32
  type OpenRouterResult = ReturnType<OpenRouter['callModel']>;
32
33
 
@@ -40,9 +41,6 @@ const AVAILABLE_GROUPS_PATH = '/workspace/ipc/available_groups.json';
40
41
  const GROUP_CLAUDE_PATH = path.join(GROUP_DIR, 'CLAUDE.md');
41
42
  const GLOBAL_CLAUDE_PATH = path.join(GLOBAL_DIR, 'CLAUDE.md');
42
43
  const CLAUDE_NOTES_MAX_CHARS = 4000;
43
- const SKILL_NOTES_MAX_FILES = 16;
44
- const SKILL_NOTES_MAX_CHARS = 3000;
45
- const SKILL_NOTES_TOTAL_MAX_CHARS = 18_000;
46
44
 
47
45
  const agentConfig = loadAgentConfig();
48
46
  const agent = agentConfig.agent;
@@ -74,167 +72,17 @@ function log(message: string): void {
74
72
  console.error(`[agent-runner] ${message}`);
75
73
  }
76
74
 
77
- // ── Response extraction pipeline ─────────────────────────────────────
78
- // OpenRouter SDK v0.3.x returns raw response IDs (gen-*, resp-*, etc.) instead
79
- // of text for fast reasoning models (GPT-5-mini/nano). Reasoning tokens consume
80
- // the output budget, leaving nothing for actual text. This multi-layer pipeline
81
- // works around that:
82
- // 1. isLikelyResponseId — detect leaked IDs so we never surface them
83
- // 2. extractTextFromRawResponse — walk raw response fields ourselves
84
- // 3. getTextWithFallback — try SDK getText(), fall back to raw extraction
85
- // 4. chatCompletionsFallback — retry via /chat/completions when all else fails
86
- // Remove this pipeline once the SDK reliably returns text for reasoning models.
87
-
88
- const RESPONSE_ID_PREFIXES = ['gen-', 'resp-', 'resp_', 'chatcmpl-', 'msg_'];
89
-
90
- function isLikelyResponseId(value: string): boolean {
91
- const trimmed = value.trim();
92
- if (!trimmed || trimmed.includes(' ') || trimmed.includes('\n')) return false;
93
- return RESPONSE_ID_PREFIXES.some(prefix => trimmed.startsWith(prefix));
94
- }
95
-
96
- function isValidText(value: unknown): value is string {
97
- return typeof value === 'string' && value.trim().length > 0 && !isLikelyResponseId(value);
98
- }
99
-
100
- function extractTextFromRawResponse(response: unknown): string {
101
- if (!response || typeof response !== 'object') return '';
102
- const record = response as Record<string, unknown>;
103
-
104
- // 1. SDK-parsed camelCase field
105
- if (isValidText(record.outputText)) return record.outputText;
106
-
107
- // 2. Raw API snake_case field
108
- if (isValidText(record.output_text)) return record.output_text;
109
-
110
- // 3. Walk response.output[] for message/output_text items
111
- if (Array.isArray(record.output)) {
112
- const parts: string[] = [];
113
- for (const item of record.output) {
114
- if (!item || typeof item !== 'object') continue;
115
- const typed = item as { type?: string; content?: unknown; text?: string };
116
- if (typed.type === 'message' && Array.isArray(typed.content)) {
117
- for (const part of typed.content as Array<{ type?: string; text?: string }>) {
118
- if (part?.type === 'output_text' && isValidText(part.text)) {
119
- parts.push(part.text);
120
- }
121
- }
122
- } else if (typed.type === 'output_text' && isValidText(typed.text)) {
123
- parts.push(typed.text);
124
- }
125
- }
126
- const joined = parts.join('');
127
- if (joined.trim()) return joined;
128
- }
75
+ // ── Response text extraction ─────────────────────────────────────────
129
76
 
130
- // 4. OpenAI chat completions compat
131
- if (Array.isArray(record.choices) && record.choices.length > 0) {
132
- const choice = record.choices[0] as { message?: { content?: unknown } } | null | undefined;
133
- if (choice?.message && isValidText(choice.message.content)) {
134
- return choice.message.content;
135
- }
136
- }
137
-
138
- return '';
139
- }
140
-
141
- async function getTextWithFallback(result: OpenRouterResult, context: string): Promise<string> {
142
- // 1. Try the SDK's proper getText() first — this handles tool execution and
143
- // extracts text from the final response via the SDK's own logic.
77
+ async function getResponseText(result: OpenRouterResult, context: string): Promise<string> {
144
78
  try {
145
79
  const text = await result.getText();
146
- if (isValidText(text)) {
80
+ if (typeof text === 'string' && text.trim()) {
147
81
  return text;
148
82
  }
149
- if (text && isLikelyResponseId(text)) {
150
- log(`Ignored response id from getText (${context}): ${String(text).slice(0, 60)}`);
151
- }
152
83
  } catch (err) {
153
84
  log(`getText failed (${context}): ${err instanceof Error ? err.message : String(err)}`);
154
85
  }
155
-
156
- // 2. Fall back to raw response extraction — walk known fields ourselves
157
- try {
158
- const response = await result.getResponse();
159
- const fallbackText = extractTextFromRawResponse(response);
160
- if (fallbackText) {
161
- log(`Recovered text from raw response (${context})`);
162
- return fallbackText;
163
- }
164
- const r = response as Record<string, unknown>;
165
- const outputLen = Array.isArray(r.output) ? (r.output as unknown[]).length : 0;
166
- log(`No text in raw response (${context}): id=${String(r.id ?? 'none').slice(0, 40)} status=${String(r.status ?? '?')} outputs=${outputLen}`);
167
- } catch (err) {
168
- log(`Raw response extraction failed (${context}): ${err instanceof Error ? err.message : String(err)}`);
169
- }
170
-
171
- // 3. Never return a response ID
172
- return '';
173
- }
174
-
175
- /**
176
- * Direct Chat Completions API fallback.
177
- * When the Responses API returns a gen-ID instead of text (common with fast
178
- * models like gpt-5-nano/mini via OpenRouter), retry using the standard
179
- * /chat/completions endpoint which reliably returns text content.
180
- */
181
- async function chatCompletionsFallback(params: {
182
- model: string;
183
- instructions: string;
184
- messages: Array<{ role: string; content: string }>;
185
- maxOutputTokens: number;
186
- temperature: number;
187
- }): Promise<string> {
188
- const apiKey = process.env.OPENROUTER_API_KEY;
189
- if (!apiKey) return '';
190
-
191
- const headers: Record<string, string> = {
192
- 'Authorization': `Bearer ${apiKey}`,
193
- 'Content-Type': 'application/json'
194
- };
195
- if (agent.openrouter.siteUrl) {
196
- headers['HTTP-Referer'] = agent.openrouter.siteUrl;
197
- }
198
- if (agent.openrouter.siteName) {
199
- headers['X-Title'] = agent.openrouter.siteName;
200
- }
201
-
202
- const chatMessages = [
203
- { role: 'system', content: params.instructions },
204
- ...params.messages
205
- ];
206
-
207
- log(`Chat Completions fallback: model=${params.model}, messages=${chatMessages.length}`);
208
- const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
209
- method: 'POST',
210
- headers,
211
- body: JSON.stringify({
212
- model: params.model,
213
- messages: chatMessages,
214
- max_completion_tokens: params.maxOutputTokens,
215
- temperature: params.temperature,
216
- reasoning_effort: 'low'
217
- }),
218
- signal: AbortSignal.timeout(agent.openrouter.timeoutMs)
219
- });
220
-
221
- const bodyText = await response.text();
222
- if (!response.ok) {
223
- log(`Chat Completions fallback HTTP ${response.status}: ${bodyText.slice(0, 300)}`);
224
- return '';
225
- }
226
-
227
- try {
228
- const data = JSON.parse(bodyText);
229
- const content = data?.choices?.[0]?.message?.content;
230
- if (isValidText(content)) {
231
- log(`Chat Completions fallback recovered text (${String(content).length} chars)`);
232
- return content;
233
- }
234
- log(`Chat Completions fallback returned no text: ${JSON.stringify(data).slice(0, 300)}`);
235
- } catch (err) {
236
- log(`Chat Completions fallback parse error: ${err instanceof Error ? err.message : String(err)}`);
237
- }
238
86
  return '';
239
87
  }
240
88
 
@@ -469,11 +317,23 @@ function estimateMessagesTokens(messages: Message[], tokensPerChar: number, toke
469
317
  return total;
470
318
  }
471
319
 
320
+ const MEMORY_SUMMARY_MAX_CHARS = 2000;
321
+
322
+ const compactToolsDoc = [
323
+ 'Tools: Bash, Read, Write, Edit, Glob, Grep, WebSearch, WebFetch, GitClone, NpmInstall,',
324
+ 'mcp__dotclaw__send_message, send_file, send_photo, send_voice, send_audio, send_location,',
325
+ 'send_contact, send_poll, send_buttons, edit_message, delete_message, download_url,',
326
+ 'schedule_task, run_task, list_tasks, pause_task, resume_task, cancel_task, update_task,',
327
+ 'spawn_job, job_status, list_jobs, cancel_job, job_update, register_group, remove_group,',
328
+ 'list_groups, set_model, memory_upsert, memory_search, memory_list, memory_forget, memory_stats.',
329
+ 'plugin__* (plugins), mcp_ext__* (external MCP).'
330
+ ].join('\n');
331
+
472
332
  function buildSystemInstructions(params: {
473
333
  assistantName: string;
474
334
  groupNotes?: string | null;
475
335
  globalNotes?: string | null;
476
- skillNotes?: SkillNote[];
336
+ skillCatalog?: SkillCatalog | null;
477
337
  memorySummary: string;
478
338
  memoryFacts: string[];
479
339
  sessionRecall: string[];
@@ -489,7 +349,10 @@ function buildSystemInstructions(params: {
489
349
  isBackgroundJob: boolean;
490
350
  jobId?: string;
491
351
  timezone?: string;
352
+ hostPlatform?: string;
353
+ messagingPlatform?: string;
492
354
  planBlock?: string;
355
+ profile?: 'fast' | 'standard' | 'deep' | 'background';
493
356
  taskExtractionPack?: PromptPack | null;
494
357
  responseQualityPack?: PromptPack | null;
495
358
  toolCallingPack?: PromptPack | null;
@@ -497,122 +360,107 @@ function buildSystemInstructions(params: {
497
360
  memoryPolicyPack?: PromptPack | null;
498
361
  memoryRecallPack?: PromptPack | null;
499
362
  }): string {
500
- const toolsDoc = [
501
- 'Tools available (use with care):',
502
- '- `Bash`: run shell commands in `/workspace/group`.',
503
- '- `Read`, `Write`, `Edit`, `Glob`, `Grep`: filesystem operations within mounted paths.',
504
- '- `WebSearch`: Brave Search API (requires `BRAVE_SEARCH_API_KEY`).',
505
- '- `WebFetch`: fetch URLs (limit payload sizes).',
506
- '- `GitClone`: clone git repositories into the workspace.',
507
- '- `NpmInstall`: install npm dependencies in the workspace.',
508
- '- `mcp__dotclaw__send_message`: send Telegram messages.',
509
- '- `mcp__dotclaw__send_file`: send a file/document.',
510
- '- `mcp__dotclaw__send_photo`: send a photo with compression.',
511
- '- `mcp__dotclaw__send_voice`: send a voice message (.ogg format).',
512
- '- `mcp__dotclaw__send_audio`: send an audio file (mp3, m4a, etc.).',
513
- '- `mcp__dotclaw__send_location`: send a map pin (latitude/longitude).',
514
- '- `mcp__dotclaw__send_contact`: send a contact card (phone + name).',
515
- '- `mcp__dotclaw__send_poll`: create a Telegram poll.',
516
- '- `mcp__dotclaw__send_buttons`: send a message with inline keyboard buttons.',
517
- '- `mcp__dotclaw__edit_message`: edit a previously sent message.',
518
- '- `mcp__dotclaw__delete_message`: delete a message.',
519
- '- `mcp__dotclaw__download_url`: download a URL to the workspace as a file.',
520
- '- Users may send photos, documents, voice messages, and videos. These are downloaded to `/workspace/group/inbox/` and referenced as `<attachment>` tags in messages. Process them with Read/Bash/Python tools.',
521
- '- `mcp__dotclaw__schedule_task`: schedule tasks (set `timezone` for locale-specific schedules).',
522
- '- `mcp__dotclaw__run_task`: run a scheduled task immediately.',
523
- '- `mcp__dotclaw__list_tasks`, `mcp__dotclaw__pause_task`, `mcp__dotclaw__resume_task`, `mcp__dotclaw__cancel_task`.',
524
- '- `mcp__dotclaw__update_task`: update a task (state, prompt, schedule, status).',
525
- '- `mcp__dotclaw__spawn_job`: start a background job.',
526
- '- `mcp__dotclaw__job_status`, `mcp__dotclaw__list_jobs`, `mcp__dotclaw__cancel_job`.',
527
- '- `mcp__dotclaw__job_update`: log job progress or notify the user.',
528
- 'Rule: If the task is likely to take more than ~2 minutes or needs multi-step research/coding, you MUST call `mcp__dotclaw__spawn_job` immediately and tell the user you queued it. Do not run long tasks in the foreground.',
529
- '- `mcp__dotclaw__register_group`: main group only.',
530
- '- `mcp__dotclaw__remove_group`, `mcp__dotclaw__list_groups`: main group only.',
531
- '- `mcp__dotclaw__set_model`: main group only.',
532
- '- `mcp__dotclaw__memory_upsert`: store durable memories.',
533
- '- `mcp__dotclaw__memory_search`, `mcp__dotclaw__memory_list`, `mcp__dotclaw__memory_forget`, `mcp__dotclaw__memory_stats`.',
534
- '- `plugin__*`: dynamically loaded plugin tools (if present and allowed by policy).'
535
- ].join('\n');
536
- const browserAutomation = [
537
- 'Browser automation (via Bash):',
538
- '- Use `agent-browser open <url>` then `agent-browser snapshot -i`.',
539
- '- Interact with refs using `agent-browser click @e1`, `fill @e2 "text"`.',
540
- '- Capture evidence with `agent-browser screenshot`.'
363
+ const profile = params.profile || 'standard';
364
+ const isFast = profile === 'fast';
365
+
366
+ // Tool descriptions are already in each tool's schema only add essential guidance here
367
+ const toolGuidance = isFast ? '' : [
368
+ 'Key tool rules:',
369
+ '- User attachments arrive in /workspace/group/inbox/ (see <attachment> tags). Process with Read/Bash/Python.',
370
+ '- To send media from the web: download_url → send_photo/send_file/send_audio. Always foreground.',
371
+ '- Charts/plots: matplotlib savefig → send_photo. Graphviz → dot -Tpng → send_photo.',
372
+ '- Voice messages are auto-transcribed (<transcript> in <attachment>). Reply with normal text — the host auto-converts to voice.',
373
+ '- GitHub CLI (`gh`) is available if GH_TOKEN is set.',
374
+ '- Use spawn_job ONLY for tasks >2 minutes (deep research, large projects). Everything else: foreground.',
375
+ '- When you spawn a job, reply minimally (e.g. "Working on it"). No job IDs, bullet plans, or status offers.',
376
+ '- plugin__* and mcp_ext__* tools may be available if configured.'
541
377
  ].join('\n');
542
378
 
543
- const memorySummary = params.memorySummary ? params.memorySummary : 'None yet.';
379
+ const toolsSection = isFast ? compactToolsDoc : toolGuidance;
380
+
381
+ const browserAutomation = !isFast && agentConfig.agent.browser.enabled ? [
382
+ 'Browser Tool: actions: navigate, snapshot, click, fill, screenshot, extract, evaluate, close.',
383
+ 'Use snapshot with interactive=true for clickable refs (@e1, @e2). Screenshots → /workspace/group/screenshots/.'
384
+ ].join('\n') : '';
385
+
386
+ // Memory section: skip for fast profile, consolidate empty placeholders for other profiles
387
+ const includeMemory = !isFast;
388
+ const hasAnyMemory = params.memorySummary || params.memoryFacts.length > 0 ||
389
+ params.longTermRecall.length > 0 || params.userProfile;
390
+
391
+ const memorySummary = params.memorySummary
392
+ ? params.memorySummary.slice(0, MEMORY_SUMMARY_MAX_CHARS)
393
+ : '';
544
394
  const memoryFacts = params.memoryFacts.length > 0
545
395
  ? params.memoryFacts.map(fact => `- ${fact}`).join('\n')
546
- : 'None yet.';
396
+ : '';
547
397
  const sessionRecall = params.sessionRecall.length > 0
548
398
  ? params.sessionRecall.map(item => `- ${item}`).join('\n')
549
- : 'None.';
550
-
399
+ : '';
551
400
  const longTermRecall = params.longTermRecall.length > 0
552
401
  ? params.longTermRecall.map(item => `- ${item}`).join('\n')
553
- : 'None.';
554
-
555
- const userProfile = params.userProfile
556
- ? params.userProfile
557
- : 'None.';
558
-
402
+ : '';
403
+ const userProfile = params.userProfile || '';
559
404
  const memoryStats = params.memoryStats
560
405
  ? `Total: ${params.memoryStats.total}, User: ${params.memoryStats.user}, Group: ${params.memoryStats.group}, Global: ${params.memoryStats.global}`
561
- : 'Unknown.';
406
+ : '';
562
407
 
563
408
  const availableGroups = params.availableGroups && params.availableGroups.length > 0
564
409
  ? params.availableGroups
565
410
  .map(group => `- ${group.name} (chat ${group.jid}, last: ${group.lastActivity})`)
566
411
  .join('\n')
567
- : 'None.';
412
+ : '';
568
413
 
569
414
  const groupNotes = params.groupNotes ? `Group notes:\n${params.groupNotes}` : '';
570
415
  const globalNotes = params.globalNotes ? `Global notes:\n${params.globalNotes}` : '';
571
- const skillNotes = formatSkillNotes(params.skillNotes || []);
416
+ const skillNotes = params.skillCatalog ? formatSkillCatalog(params.skillCatalog) : '';
572
417
 
573
- const toolReliability = params.toolReliability && params.toolReliability.length > 0
418
+ const toolReliability = !isFast && params.toolReliability && params.toolReliability.length > 0
574
419
  ? params.toolReliability
575
- .sort((a, b) => b.success_rate - a.success_rate)
420
+ .sort((a, b) => a.success_rate - b.success_rate)
421
+ .slice(0, 20)
576
422
  .map(tool => {
577
423
  const pct = `${Math.round(tool.success_rate * 100)}%`;
578
424
  const avg = Number.isFinite(tool.avg_duration_ms) ? `${Math.round(tool.avg_duration_ms!)}ms` : 'n/a';
579
425
  return `- ${tool.name}: success ${pct} over ${tool.count} calls (avg ${avg})`;
580
426
  })
581
427
  .join('\n')
582
- : 'No recent tool reliability data.';
428
+ : '';
583
429
 
584
430
  const behaviorNotes: string[] = [];
585
431
  const responseStyle = typeof params.behaviorConfig?.response_style === 'string'
586
432
  ? String(params.behaviorConfig.response_style)
587
433
  : '';
588
434
  if (responseStyle === 'concise') {
589
- behaviorNotes.push('Response style: concise and action-oriented.');
435
+ behaviorNotes.push('Keep responses short and to the point.');
590
436
  } else if (responseStyle === 'detailed') {
591
- behaviorNotes.push('Response style: detailed and step-by-step where helpful.');
437
+ behaviorNotes.push('Give detailed, step-by-step responses when helpful.');
592
438
  }
593
439
  const toolBias = typeof params.behaviorConfig?.tool_calling_bias === 'number'
594
440
  ? Number(params.behaviorConfig.tool_calling_bias)
595
441
  : null;
596
442
  if (toolBias !== null && toolBias < 0.4) {
597
- behaviorNotes.push('Tool usage: be conservative, ask clarifying questions before calling tools.');
443
+ behaviorNotes.push('Ask before using tools unless the intent is obvious.');
598
444
  } else if (toolBias !== null && toolBias > 0.6) {
599
- behaviorNotes.push('Tool usage: be proactive when tools add accuracy or save time.');
445
+ behaviorNotes.push('Use tools proactively when they add accuracy or save time.');
600
446
  }
601
447
  const cautionBias = typeof params.behaviorConfig?.caution_bias === 'number'
602
448
  ? Number(params.behaviorConfig.caution_bias)
603
449
  : null;
604
450
  if (cautionBias !== null && cautionBias > 0.6) {
605
- behaviorNotes.push('Caution: verify uncertain facts and flag limitations.');
451
+ behaviorNotes.push('Double-check uncertain facts and flag limitations.');
606
452
  }
607
453
 
608
- const behaviorConfig = params.behaviorConfig
609
- ? `Behavior overrides:\n${JSON.stringify(params.behaviorConfig, null, 2)}`
610
- : '';
611
-
612
454
  const timezoneNote = params.timezone
613
455
  ? `Timezone: ${params.timezone}. Use this timezone when interpreting or presenting timestamps unless the user specifies another.`
614
456
  : '';
615
457
 
458
+ const hostPlatformNote = params.hostPlatform
459
+ ? (params.hostPlatform.startsWith('linux')
460
+ ? `Host platform: ${params.hostPlatform} (matches container).`
461
+ : `You are running inside a Linux container, but the user's host machine is ${params.hostPlatform}. Packages with platform-specific native binaries (e.g. esbuild, swc, sharp) installed here won't work on the host. When you create projects with dependencies, delete node_modules before finishing and tell the user to run the install command on their machine.`)
462
+ : '';
463
+
616
464
  const scheduledNote = params.isScheduledTask
617
465
  ? `You are running as a scheduled task${params.taskId ? ` (task id: ${params.taskId})` : ''}. If you need to communicate, use \`mcp__dotclaw__send_message\`.`
618
466
  : '';
@@ -620,7 +468,7 @@ function buildSystemInstructions(params: {
620
468
  ? 'You are running in the background for a user request. Focus on completing the task and return a complete response without asking follow-up questions unless strictly necessary.'
621
469
  : '';
622
470
  const jobNote = params.isBackgroundJob
623
- ? `You are running as a background job${params.jobId ? ` (job id: ${params.jobId})` : ''}. Return a complete result. Do not send routine progress chatter. Only call \`mcp__dotclaw__job_update\` when the user must be notified about a blocker, a required decision, or a critical failure; otherwise rely on the final completion message. Prefer writing large outputs to the job artifacts directory.`
471
+ ? `You are running as a background job${params.jobId ? ` (job id: ${params.jobId})` : ''}. Complete the task silently and return the result. Do NOT call \`mcp__dotclaw__job_update\` for routine progress only for critical blockers or required user decisions. Do NOT send messages to the chat about your progress. Just do the work and return the final result. The system will deliver your result to the user automatically.`
624
472
  : '';
625
473
  const jobArtifactsNote = params.isBackgroundJob && params.jobId
626
474
  ? `Job artifacts directory: /workspace/group/jobs/${params.jobId}`
@@ -629,20 +477,71 @@ function buildSystemInstructions(params: {
629
477
  const fmtPack = (label: string, pack: PromptPack | null | undefined) =>
630
478
  pack ? formatPromptPack({ label, pack, maxDemos: PROMPT_PACKS_MAX_DEMOS, maxChars: PROMPT_PACKS_MAX_CHARS }) : '';
631
479
 
632
- const taskExtractionBlock = fmtPack('Task Extraction Guidelines', params.taskExtractionPack);
633
- const responseQualityBlock = fmtPack('Response Quality Guidelines', params.responseQualityPack);
634
- const toolCallingBlock = fmtPack('Tool Calling Guidelines', params.toolCallingPack);
635
- const toolOutcomeBlock = fmtPack('Tool Outcome Guidelines', params.toolOutcomePack);
636
- const memoryPolicyBlock = fmtPack('Memory Policy Guidelines', params.memoryPolicyPack);
637
- const memoryRecallBlock = fmtPack('Memory Recall Guidelines', params.memoryRecallPack);
480
+ // Skip prompt packs for fast profile; enforce aggregate budget for others
481
+ const PROMPT_PACKS_TOTAL_BUDGET = PROMPT_PACKS_MAX_CHARS * 3; // Aggregate cap across all packs
482
+ const allPackBlocks: string[] = [];
483
+ if (!isFast) {
484
+ const packEntries: Array<[string, PromptPack | null | undefined]> = [
485
+ ['Tool Calling Guidelines', params.toolCallingPack],
486
+ ['Tool Outcome Guidelines', params.toolOutcomePack],
487
+ ['Task Extraction Guidelines', params.taskExtractionPack],
488
+ ['Response Quality Guidelines', params.responseQualityPack],
489
+ ['Memory Policy Guidelines', params.memoryPolicyPack],
490
+ ['Memory Recall Guidelines', params.memoryRecallPack],
491
+ ];
492
+ let totalChars = 0;
493
+ for (const [label, pack] of packEntries) {
494
+ const block = fmtPack(label, pack);
495
+ if (!block) continue;
496
+ if (totalChars + block.length > PROMPT_PACKS_TOTAL_BUDGET) break;
497
+ allPackBlocks.push(block);
498
+ totalChars += block.length;
499
+ }
500
+ }
501
+ const taskExtractionBlock = allPackBlocks.find(b => b.includes('Task Extraction')) || '';
502
+ const responseQualityBlock = allPackBlocks.find(b => b.includes('Response Quality')) || '';
503
+ const toolCallingBlock = allPackBlocks.find(b => b.includes('Tool Calling')) || '';
504
+ const toolOutcomeBlock = allPackBlocks.find(b => b.includes('Tool Outcome')) || '';
505
+ const memoryPolicyBlock = allPackBlocks.find(b => b.includes('Memory Policy')) || '';
506
+ const memoryRecallBlock = allPackBlocks.find(b => b.includes('Memory Recall')) || '';
507
+
508
+ // Build memory sections — omit entirely for fast, consolidate empty placeholders for others
509
+ const memorySections: string[] = [];
510
+ if (includeMemory) {
511
+ if (hasAnyMemory) {
512
+ if (memorySummary) {
513
+ memorySections.push('Long-term memory summary:', memorySummary);
514
+ }
515
+ if (memoryFacts) {
516
+ memorySections.push('Long-term facts:', memoryFacts);
517
+ }
518
+ if (userProfile) {
519
+ memorySections.push('User profile (if available):', userProfile);
520
+ }
521
+ if (longTermRecall) {
522
+ memorySections.push('What you remember about the user (long-term):', longTermRecall);
523
+ }
524
+ if (memoryStats) {
525
+ memorySections.push('Memory stats:', memoryStats);
526
+ }
527
+ } else {
528
+ memorySections.push('No long-term memory available yet.');
529
+ }
530
+ }
531
+
532
+ // Session recall is always included (local context from current conversation)
533
+ if (sessionRecall) {
534
+ memorySections.push('Recent conversation context:', sessionRecall);
535
+ }
638
536
 
639
537
  return [
640
- `You are ${params.assistantName}, a personal assistant running inside DotClaw.`,
538
+ `You are ${params.assistantName}, a personal assistant running inside DotClaw.${params.messagingPlatform ? ` You are currently connected via ${params.messagingPlatform}.` : ''}`,
539
+ hostPlatformNote,
641
540
  scheduledNote,
642
541
  backgroundNote,
643
542
  jobNote,
644
543
  jobArtifactsNote,
645
- toolsDoc,
544
+ toolsSection,
646
545
  browserAutomation,
647
546
  groupNotes,
648
547
  globalNotes,
@@ -655,25 +554,11 @@ function buildSystemInstructions(params: {
655
554
  responseQualityBlock,
656
555
  memoryPolicyBlock,
657
556
  memoryRecallBlock,
658
- 'Long-term memory summary:',
659
- memorySummary,
660
- 'Long-term facts:',
661
- memoryFacts,
662
- 'User profile (if available):',
663
- userProfile,
664
- 'Long-term memory recall (durable facts/preferences):',
665
- longTermRecall,
666
- 'Session recall (recent/older conversation snippets):',
667
- sessionRecall,
668
- 'Memory stats:',
669
- memoryStats,
670
- 'Available groups (main group only):',
671
- availableGroups,
672
- 'Tool reliability (recent):',
673
- toolReliability,
557
+ ...memorySections,
558
+ availableGroups ? `Available groups (main group only):\n${availableGroups}` : '',
559
+ toolReliability ? `Tool reliability (recent):\n${toolReliability}` : '',
674
560
  behaviorNotes.length > 0 ? `Behavior notes:\n${behaviorNotes.join('\n')}` : '',
675
- behaviorConfig,
676
- 'Respond succinctly and helpfully. If you perform tool actions, summarize the results.'
561
+ 'Be concise and helpful. When you use tools, summarize what happened rather than dumping raw output.'
677
562
  ].filter(Boolean).join('\n\n');
678
563
  }
679
564
 
@@ -708,122 +593,6 @@ function loadClaudeNotes(): { group: string | null; global: string | null } {
708
593
  };
709
594
  }
710
595
 
711
- export type SkillNote = {
712
- scope: 'group' | 'global';
713
- path: string;
714
- content: string;
715
- };
716
-
717
- function collectSkillFiles(rootDir: string, maxFiles: number): string[] {
718
- const files: string[] = [];
719
- const seen = new Set<string>();
720
- const addFile = (filePath: string) => {
721
- const normalized = path.resolve(filePath);
722
- if (seen.has(normalized)) return;
723
- if (!fs.existsSync(normalized)) return;
724
- let stat: fs.Stats;
725
- try {
726
- stat = fs.statSync(normalized);
727
- } catch {
728
- return;
729
- }
730
- if (!stat.isFile()) return;
731
- if (!normalized.toLowerCase().endsWith('.md')) return;
732
- seen.add(normalized);
733
- files.push(normalized);
734
- };
735
-
736
- addFile(path.join(rootDir, 'SKILL.md'));
737
-
738
- const skillsDir = path.join(rootDir, 'skills');
739
- if (fs.existsSync(skillsDir)) {
740
- const stack = [skillsDir];
741
- while (stack.length > 0 && files.length < maxFiles) {
742
- const current = stack.pop();
743
- if (!current) continue;
744
- let entries: fs.Dirent[];
745
- try {
746
- entries = fs.readdirSync(current, { withFileTypes: true });
747
- } catch {
748
- continue;
749
- }
750
- entries.sort((a, b) => a.name.localeCompare(b.name));
751
- for (const entry of entries) {
752
- const nextPath = path.join(current, entry.name);
753
- if (entry.isSymbolicLink()) continue;
754
- if (entry.isDirectory()) {
755
- stack.push(nextPath);
756
- continue;
757
- }
758
- if (entry.isFile()) {
759
- addFile(nextPath);
760
- }
761
- if (files.length >= maxFiles) break;
762
- }
763
- }
764
- }
765
-
766
- files.sort((a, b) => a.localeCompare(b));
767
- return files.slice(0, maxFiles);
768
- }
769
-
770
- export function loadSkillNotesFromRoots(params: {
771
- groupDir: string;
772
- globalDir: string;
773
- maxFiles?: number;
774
- maxCharsPerFile?: number;
775
- maxTotalChars?: number;
776
- }): SkillNote[] {
777
- const maxFiles = Number.isFinite(params.maxFiles) ? Math.max(1, Math.floor(params.maxFiles!)) : SKILL_NOTES_MAX_FILES;
778
- const maxCharsPerFile = Number.isFinite(params.maxCharsPerFile)
779
- ? Math.max(200, Math.floor(params.maxCharsPerFile!))
780
- : SKILL_NOTES_MAX_CHARS;
781
- const maxTotalChars = Number.isFinite(params.maxTotalChars)
782
- ? Math.max(maxCharsPerFile, Math.floor(params.maxTotalChars!))
783
- : SKILL_NOTES_TOTAL_MAX_CHARS;
784
-
785
- const notes: SkillNote[] = [];
786
- let consumedChars = 0;
787
-
788
- const appendScopeNotes = (scope: 'group' | 'global', rootDir: string) => {
789
- const skillFiles = collectSkillFiles(rootDir, maxFiles);
790
- for (const filePath of skillFiles) {
791
- if (notes.length >= maxFiles) break;
792
- if (consumedChars >= maxTotalChars) break;
793
- const content = readTextFileLimited(filePath, maxCharsPerFile);
794
- if (!content) continue;
795
- const remaining = maxTotalChars - consumedChars;
796
- const truncated = content.length > remaining
797
- ? `${content.slice(0, remaining)}\n\n[Truncated for total skill budget]`
798
- : content;
799
- const relativePath = path.relative(rootDir, filePath).split(path.sep).join('/');
800
- notes.push({
801
- scope,
802
- path: relativePath || path.basename(filePath),
803
- content: truncated
804
- });
805
- consumedChars += truncated.length;
806
- if (consumedChars >= maxTotalChars) break;
807
- }
808
- };
809
-
810
- appendScopeNotes('group', params.groupDir);
811
- appendScopeNotes('global', params.globalDir);
812
- return notes;
813
- }
814
-
815
- function formatSkillNotes(notes: SkillNote[]): string {
816
- if (!notes || notes.length === 0) return '';
817
- const lines: string[] = [
818
- 'Skill instructions (loaded from SKILL.md / skills/*.md):',
819
- 'When a task matches a skill, follow that skill workflow first and keep output concise.'
820
- ];
821
- for (const note of notes) {
822
- lines.push(`[${note.scope}] ${note.path}`);
823
- lines.push(note.content);
824
- }
825
- return lines.join('\n\n');
826
- }
827
596
 
828
597
  function extractQueryFromPrompt(prompt: string): string {
829
598
  if (!prompt) return '';
@@ -886,7 +655,7 @@ async function updateMemorySummary(params: {
886
655
  temperature: 0.1,
887
656
  reasoning: { effort: 'low' as const }
888
657
  });
889
- const text = await getTextWithFallback(result, 'summary');
658
+ const text = await getResponseText(result, 'summary');
890
659
  return parseSummaryResponse(text);
891
660
  }
892
661
 
@@ -1029,7 +798,7 @@ async function validateResponseQuality(params: {
1029
798
  temperature: params.temperature,
1030
799
  reasoning: { effort: 'low' as const }
1031
800
  });
1032
- const text = await getTextWithFallback(result, 'response_validation');
801
+ const text = await getResponseText(result, 'response_validation');
1033
802
  return parseResponseValidation(text);
1034
803
  }
1035
804
 
@@ -1138,9 +907,10 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1138
907
  const tokenEstimate = resolveTokenEstimate(input, agentConfig);
1139
908
  const availableGroups = loadAvailableGroups();
1140
909
  const claudeNotes = loadClaudeNotes();
1141
- const skillNotes = loadSkillNotesFromRoots({
910
+ const skillCatalog = buildSkillCatalog({
1142
911
  groupDir: GROUP_DIR,
1143
- globalDir: GLOBAL_DIR
912
+ globalDir: GLOBAL_DIR,
913
+ maxSkills: agent.skills.maxSkills
1144
914
  });
1145
915
 
1146
916
  const { ctx: sessionCtx, isNew } = createSessionContext(SESSION_ROOT, input.sessionId);
@@ -1168,6 +938,36 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1168
938
  }
1169
939
  });
1170
940
 
941
+ // Discover MCP external tools if enabled
942
+ let mcpCleanup: (() => Promise<void>) | null = null;
943
+ if (agent.mcp.enabled && agent.mcp.servers.length > 0) {
944
+ try {
945
+ // Build a minimal wrapExecute for MCP tools (policy + logging handled by createTools wrapExecute pattern)
946
+ const wrapMcp = <TInput, TOutput>(name: string, execute: (args: TInput) => Promise<TOutput>) => {
947
+ return async (args: TInput): Promise<TOutput> => {
948
+ const start = Date.now();
949
+ try {
950
+ const result = await execute(args);
951
+ toolCalls.push({ name, ok: true, duration_ms: Date.now() - start });
952
+ return result;
953
+ } catch (err) {
954
+ const error = err instanceof Error ? err.message : String(err);
955
+ toolCalls.push({ name, ok: false, duration_ms: Date.now() - start, error });
956
+ throw err;
957
+ }
958
+ };
959
+ };
960
+ const mcp = await discoverMcpTools(agent, wrapMcp);
961
+ tools.push(...mcp.tools);
962
+ mcpCleanup = mcp.cleanup;
963
+ if (mcp.tools.length > 0) {
964
+ log(`MCP: discovered ${mcp.tools.length} external tools`);
965
+ }
966
+ } catch (err) {
967
+ log(`MCP discovery failed: ${err instanceof Error ? err.message : String(err)}`);
968
+ }
969
+ }
970
+
1171
971
  if (process.env.DOTCLAW_SELF_CHECK === '1') {
1172
972
  try {
1173
973
  const details = await runSelfCheck({ model });
@@ -1332,7 +1132,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1332
1132
  assistantName,
1333
1133
  groupNotes: claudeNotes.group,
1334
1134
  globalNotes: claudeNotes.global,
1335
- skillNotes,
1135
+ skillCatalog,
1336
1136
  memorySummary: sessionCtx.state.summary,
1337
1137
  memoryFacts: sessionCtx.state.facts,
1338
1138
  sessionRecall,
@@ -1348,7 +1148,10 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1348
1148
  isBackgroundJob: !!input.isBackgroundJob,
1349
1149
  jobId: input.jobId,
1350
1150
  timezone: typeof input.timezone === 'string' ? input.timezone : undefined,
1151
+ hostPlatform: typeof input.hostPlatform === 'string' ? input.hostPlatform : undefined,
1152
+ messagingPlatform: input.chatJid?.includes(':') ? input.chatJid.split(':')[0] : undefined,
1351
1153
  planBlock: planBlockValue,
1154
+ profile: input.profile,
1352
1155
  taskExtractionPack: taskPackResult?.pack || null,
1353
1156
  responseQualityPack: responseQualityResult?.pack || null,
1354
1157
  toolCallingPack: toolCallingResult?.pack || null,
@@ -1384,7 +1187,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1384
1187
  temperature: plannerTemperature,
1385
1188
  reasoning: { effort: 'low' as const }
1386
1189
  });
1387
- const plannerText = await getTextWithFallback(plannerResult, 'planner');
1190
+ const plannerText = await getResponseText(plannerResult, 'planner');
1388
1191
  const plan = parsePlannerResponse(plannerText);
1389
1192
  if (plan) {
1390
1193
  planBlock = formatPlanBlock(plan);
@@ -1465,7 +1268,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1465
1268
  const localLatencyMs = Date.now() - startedAt;
1466
1269
 
1467
1270
  // Get the complete response text via the SDK's proper getText() path
1468
- let localResponseText = await getTextWithFallback(result, 'completion');
1271
+ let localResponseText = await getResponseText(result, 'completion');
1469
1272
 
1470
1273
  const toolCallsFromModel = await result.getToolCalls();
1471
1274
  if (toolCallsFromModel.length > 0) {
@@ -1476,21 +1279,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1476
1279
  if (toolCallsFromModel.length > 0) {
1477
1280
  localResponseText = 'I started running tool calls but did not get a final response. If you want me to continue, please ask a narrower subtask or say "continue".';
1478
1281
  } else {
1479
- // Responses API likely returned a gen-ID; retry with Chat Completions API
1480
- try {
1481
- localResponseText = await chatCompletionsFallback({
1482
- model,
1483
- instructions: resolvedInstructions,
1484
- messages: messagesToOpenRouter(contextMessages),
1485
- maxOutputTokens: config.maxOutputTokens,
1486
- temperature: config.temperature
1487
- });
1488
- } catch (err) {
1489
- log(`Chat Completions fallback error: ${err instanceof Error ? err.message : String(err)}`);
1490
- }
1491
- }
1492
- if (!localResponseText || !localResponseText.trim()) {
1493
- log(`Warning: Model returned empty/whitespace response after all fallbacks. tool calls: ${toolCallsFromModel.length}`);
1282
+ log(`Warning: Model returned empty/whitespace response. tool calls: ${toolCallsFromModel.length}`);
1494
1283
  }
1495
1284
  } else {
1496
1285
  log(`Model returned text response (${localResponseText.length} chars)`);
@@ -1625,7 +1414,7 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1625
1414
  temperature: 0.1,
1626
1415
  reasoning: { effort: 'low' as const }
1627
1416
  });
1628
- const extractionText = await getTextWithFallback(extractionResult, 'memory_extraction');
1417
+ const extractionText = await getResponseText(extractionResult, 'memory_extraction');
1629
1418
  const extractedItems = parseMemoryExtraction(extractionText);
1630
1419
  if (extractedItems.length === 0) return;
1631
1420
 
@@ -1691,6 +1480,11 @@ export async function runAgentOnce(input: ContainerInput): Promise<ContainerOutp
1691
1480
  }
1692
1481
  }
1693
1482
 
1483
+ // Cleanup MCP connections
1484
+ if (mcpCleanup) {
1485
+ try { await mcpCleanup(); } catch { /* ignore cleanup errors */ }
1486
+ }
1487
+
1694
1488
  return {
1695
1489
  status: 'success',
1696
1490
  result: finalResult,