@dotsetlabs/dotclaw 2.1.0 → 2.2.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 (75) hide show
  1. package/.env.example +12 -0
  2. package/README.md +5 -2
  3. package/config-examples/runtime.json +46 -5
  4. package/config-examples/tool-budgets.json +1 -1
  5. package/config-examples/tool-policy.json +1 -1
  6. package/container/Dockerfile +5 -1
  7. package/container/agent-runner/package.json +1 -1
  8. package/container/agent-runner/src/agent-config.ts +65 -15
  9. package/container/agent-runner/src/container-protocol.ts +6 -0
  10. package/container/agent-runner/src/daemon.ts +18 -5
  11. package/container/agent-runner/src/index.ts +416 -240
  12. package/container/agent-runner/src/ipc.ts +76 -1
  13. package/container/agent-runner/src/mcp-registry.ts +11 -0
  14. package/container/agent-runner/src/memory.ts +139 -1
  15. package/container/agent-runner/src/process-registry.ts +257 -0
  16. package/container/agent-runner/src/system-prompt.ts +311 -0
  17. package/container/agent-runner/src/tools.ts +382 -29
  18. package/container/agent-runner/src/tts.ts +42 -0
  19. package/dist/agent-context.d.ts +1 -0
  20. package/dist/agent-context.d.ts.map +1 -1
  21. package/dist/agent-context.js +6 -3
  22. package/dist/agent-context.js.map +1 -1
  23. package/dist/agent-execution.d.ts +1 -0
  24. package/dist/agent-execution.d.ts.map +1 -1
  25. package/dist/agent-execution.js +11 -4
  26. package/dist/agent-execution.js.map +1 -1
  27. package/dist/container-protocol.d.ts +8 -0
  28. package/dist/container-protocol.d.ts.map +1 -1
  29. package/dist/container-runner.d.ts.map +1 -1
  30. package/dist/container-runner.js +44 -8
  31. package/dist/container-runner.js.map +1 -1
  32. package/dist/index.js +53 -6
  33. package/dist/index.js.map +1 -1
  34. package/dist/ipc-dispatcher.d.ts.map +1 -1
  35. package/dist/ipc-dispatcher.js +336 -6
  36. package/dist/ipc-dispatcher.js.map +1 -1
  37. package/dist/memory-recall.d.ts +1 -0
  38. package/dist/memory-recall.d.ts.map +1 -1
  39. package/dist/memory-recall.js +3 -0
  40. package/dist/memory-recall.js.map +1 -1
  41. package/dist/memory-store.d.ts.map +1 -1
  42. package/dist/memory-store.js +5 -3
  43. package/dist/memory-store.js.map +1 -1
  44. package/dist/message-pipeline.d.ts.map +1 -1
  45. package/dist/message-pipeline.js +30 -9
  46. package/dist/message-pipeline.js.map +1 -1
  47. package/dist/model-registry.d.ts +15 -0
  48. package/dist/model-registry.d.ts.map +1 -1
  49. package/dist/model-registry.js +56 -12
  50. package/dist/model-registry.js.map +1 -1
  51. package/dist/providers/telegram/telegram-provider.d.ts +1 -0
  52. package/dist/providers/telegram/telegram-provider.d.ts.map +1 -1
  53. package/dist/providers/telegram/telegram-provider.js +14 -0
  54. package/dist/providers/telegram/telegram-provider.js.map +1 -1
  55. package/dist/request-router.d.ts +0 -1
  56. package/dist/request-router.d.ts.map +1 -1
  57. package/dist/request-router.js +18 -6
  58. package/dist/request-router.js.map +1 -1
  59. package/dist/runtime-config.d.ts +14 -0
  60. package/dist/runtime-config.d.ts.map +1 -1
  61. package/dist/runtime-config.js +64 -16
  62. package/dist/runtime-config.js.map +1 -1
  63. package/dist/task-scheduler.d.ts.map +1 -1
  64. package/dist/task-scheduler.js +3 -5
  65. package/dist/task-scheduler.js.map +1 -1
  66. package/dist/tool-budgets.js +1 -1
  67. package/dist/tool-budgets.js.map +1 -1
  68. package/dist/tool-policy.d.ts.map +1 -1
  69. package/dist/tool-policy.js +13 -3
  70. package/dist/tool-policy.js.map +1 -1
  71. package/dist/webhook.d.ts +14 -0
  72. package/dist/webhook.d.ts.map +1 -0
  73. package/dist/webhook.js +169 -0
  74. package/dist/webhook.js.map +1 -0
  75. package/package.json +3 -2
package/.env.example CHANGED
@@ -27,6 +27,12 @@ TZ=America/New_York
27
27
  # GitHub Personal Access Token (enables gh CLI in containers)
28
28
  # GH_TOKEN=ghp_your_token_here
29
29
 
30
+ # OpenAI API key (enables OpenAI TTS provider; falls back to OPENROUTER_API_KEY)
31
+ # OPENAI_API_KEY=sk-replace-with-openai-key
32
+
33
+ # Override vision model for the AnalyzeImage tool (defaults to openai/gpt-4o)
34
+ # DOTCLAW_VISION_MODEL=openai/gpt-4o
35
+
30
36
  # --- Optional: System (set in shell before starting) ---
31
37
  # Override DotClaw home directory (defaults to ~/.dotclaw)
32
38
  # DOTCLAW_HOME=~/.dotclaw
@@ -39,12 +45,18 @@ TZ=America/New_York
39
45
  # Required for non-interactive bootstrap
40
46
  # DOTCLAW_BOOTSTRAP_CHAT_ID=123456789
41
47
 
48
+ # Explicit provider selection (telegram or discord)
49
+ # DOTCLAW_BOOTSTRAP_PROVIDER=telegram
50
+
42
51
  # Optional bootstrap defaults
43
52
  # DOTCLAW_BOOTSTRAP_GROUP_NAME=main
44
53
  # DOTCLAW_BOOTSTRAP_GROUP_FOLDER=main
45
54
  # DOTCLAW_BOOTSTRAP_BUILD=true
46
55
  # DOTCLAW_BOOTSTRAP_SELF_CHECK=true
47
56
 
57
+ # Required for non-interactive configure
58
+ # DOTCLAW_CONFIGURE_CHAT_ID=123456789
59
+
48
60
  # --- Optional: autotune (advanced) ---
49
61
  # Autotune uses dotenv, so these can live in .env if you run `npm run autotune`.
50
62
  # AUTOTUNE_TRACE_DIR=~/.dotclaw/traces
package/README.md CHANGED
@@ -120,10 +120,13 @@ Or see:
120
120
 
121
121
  ```bash
122
122
  npm run dev # Run with hot reload
123
- npm run build # Compile TypeScript
123
+ npm run dev:up # Full dev cycle: rebuild container + kill stale daemons + start dev
124
+ npm run dev:down # Remove all running dotclaw agent containers
125
+ npm run build # Compile TypeScript (host)
126
+ npm run build:all # Build both host and container
124
127
  npm run lint # Run ESLint
125
128
  npm test # Run tests
126
- ./container/build.sh # Rebuild agent container
129
+ dotclaw build # Rebuild agent container (or: ./container/build.sh)
127
130
  ```
128
131
 
129
132
  ## License
@@ -15,6 +15,9 @@
15
15
  "port": 3002
16
16
  },
17
17
  "memory": {
18
+ "recall": {
19
+ "minScore": 0.35
20
+ },
18
21
  "embeddings": {
19
22
  "enabled": true
20
23
  }
@@ -22,32 +25,70 @@
22
25
  "routing": {
23
26
  "model": "moonshotai/kimi-k2.5",
24
27
  "fallbacks": ["anthropic/claude-sonnet-4-5", "openai/gpt-4.1"],
25
- "maxOutputTokens": 4096,
26
- "maxToolSteps": 50,
27
- "temperature": 0.2,
28
+ "allowedModels": [],
29
+ "maxOutputTokens": 0,
30
+ "maxToolSteps": 200,
31
+ "temperature": 0.6,
28
32
  "recallMaxResults": 8,
29
33
  "recallMaxTokens": 1500
30
34
  },
35
+ "webhook": {
36
+ "enabled": false,
37
+ "port": 3003,
38
+ "token": ""
39
+ },
31
40
  "streaming": {
32
41
  "enabled": true,
33
42
  "chunkFlushIntervalMs": 200,
34
43
  "editIntervalMs": 400,
35
44
  "maxEditLength": 3800
36
45
  },
46
+ "telegram": {
47
+ "enabled": true
48
+ },
49
+ "discord": {
50
+ "enabled": false
51
+ },
37
52
  "toolBudgets": {
38
53
  "enabled": false
39
54
  }
40
55
  },
41
56
  "agent": {
42
57
  "assistantName": "Rain",
43
- "reasoning": { "effort": "low" },
58
+ "reasoning": { "effort": "medium" },
59
+ "context": {
60
+ "maxHistoryTurns": 40,
61
+ "contextPruning": {
62
+ "softTrimMaxChars": 4000,
63
+ "softTrimHeadChars": 1500,
64
+ "softTrimTailChars": 1500,
65
+ "keepLastAssistant": 3
66
+ }
67
+ },
44
68
  "promptPacks": {
45
69
  "enabled": true
46
70
  },
47
71
  "tools": {
48
72
  "enableBash": true,
49
73
  "enableWebSearch": true,
50
- "enableWebFetch": true
74
+ "enableWebFetch": true,
75
+ "bash": {
76
+ "timeoutMs": 600000
77
+ },
78
+ "process": {
79
+ "maxSessions": 16,
80
+ "maxOutputBytes": 1048576,
81
+ "defaultTimeoutMs": 1800000
82
+ }
83
+ },
84
+ "tts": {
85
+ "provider": "edge-tts",
86
+ "openaiModel": "tts-1",
87
+ "openaiVoice": "alloy"
88
+ },
89
+ "mcp": {
90
+ "enabled": true,
91
+ "servers": []
51
92
  }
52
93
  }
53
94
  }
@@ -6,7 +6,7 @@
6
6
  "Bash": 2000,
7
7
  "Python": 1500,
8
8
  "GitClone": 400,
9
- "NpmInstall": 300
9
+ "PackageInstall": 300
10
10
  }
11
11
  },
12
12
  "groups": {
@@ -7,7 +7,7 @@
7
7
  "Glob",
8
8
  "Grep",
9
9
  "GitClone",
10
- "NpmInstall",
10
+ "PackageInstall",
11
11
  "WebSearch",
12
12
  "WebFetch",
13
13
  "Bash",
@@ -37,6 +37,7 @@ RUN apt-get update && apt-get install -y \
37
37
  tree \
38
38
  ripgrep \
39
39
  graphviz \
40
+ poppler-utils \
40
41
  && rm -rf /var/lib/apt/lists/*
41
42
 
42
43
  # Install GitHub CLI (not in standard Debian repos)
@@ -51,7 +52,7 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \
51
52
  RUN echo 'node ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/node && chmod 0440 /etc/sudoers.d/node
52
53
 
53
54
  # Install common Python packages
54
- RUN pip3 install --break-system-packages pandas numpy requests beautifulsoup4 matplotlib Pillow openpyxl pyyaml tabulate chardet
55
+ RUN pip3 install --break-system-packages pandas numpy requests beautifulsoup4 matplotlib Pillow openpyxl pyyaml tabulate chardet pdfminer.six httpx lxml cssselect python-docx python-pptx
55
56
 
56
57
  # Set Chromium path for agent-browser
57
58
  ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium
@@ -60,6 +61,9 @@ ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
60
61
  # Install agent-browser globally
61
62
  RUN npm install -g agent-browser
62
63
 
64
+ # Enable pnpm via corepack (cross-platform lockfiles)
65
+ RUN corepack enable && corepack prepare pnpm@latest --activate
66
+
63
67
  # Create app directory
64
68
  WORKDIR /app
65
69
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotclaw-agent-runner",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "description": "Container-side agent runner for DotClaw",
6
6
  "main": "dist/index.js",
@@ -27,13 +27,19 @@ export type AgentRuntimeConfig = {
27
27
  summaryMaxOutputTokens: number;
28
28
  temperature: number;
29
29
  maxContextMessageTokens: number;
30
+ maxHistoryTurns: number;
31
+ contextPruning: {
32
+ softTrimMaxChars: number;
33
+ softTrimHeadChars: number;
34
+ softTrimTailChars: number;
35
+ keepLastAssistant: number;
36
+ };
30
37
  };
31
38
  memory: {
32
39
  maxResults: number;
33
40
  maxTokens: number;
34
41
  extraction: {
35
42
  enabled: boolean;
36
- async: boolean;
37
43
  maxMessages: number;
38
44
  maxOutputTokens: number;
39
45
  };
@@ -98,6 +104,9 @@ export type AgentRuntimeConfig = {
98
104
  model: string;
99
105
  baseUrl: string;
100
106
  defaultVoice: string;
107
+ provider: 'edge-tts' | 'openai';
108
+ openaiModel: string;
109
+ openaiVoice: string;
101
110
  };
102
111
  browser: {
103
112
  enabled: boolean;
@@ -124,6 +133,11 @@ export type AgentRuntimeConfig = {
124
133
  maxSkills: number;
125
134
  maxSummaryChars: number;
126
135
  };
136
+ process: {
137
+ maxSessions: number;
138
+ maxOutputBytes: number;
139
+ defaultTimeoutMs: number;
140
+ };
127
141
  };
128
142
  };
129
143
 
@@ -147,21 +161,27 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
147
161
  canaryRate: 0.1
148
162
  },
149
163
  context: {
150
- maxContextTokens: 24_000,
151
- compactionTriggerTokens: 20_000,
164
+ maxContextTokens: 128_000,
165
+ compactionTriggerTokens: 120_000,
152
166
  recentContextTokens: 8000,
153
167
  summaryUpdateEveryMessages: 20,
154
- maxOutputTokens: 4096,
168
+ maxOutputTokens: 8192,
155
169
  summaryMaxOutputTokens: 2048,
156
- temperature: 0.2,
157
- maxContextMessageTokens: 3000
170
+ temperature: 0.6,
171
+ maxContextMessageTokens: 4000,
172
+ maxHistoryTurns: 40,
173
+ contextPruning: {
174
+ softTrimMaxChars: 4_000,
175
+ softTrimHeadChars: 1_500,
176
+ softTrimTailChars: 1_500,
177
+ keepLastAssistant: 3
178
+ }
158
179
  },
159
180
  memory: {
160
181
  maxResults: 6,
161
182
  maxTokens: 2000,
162
183
  extraction: {
163
184
  enabled: true,
164
- async: true,
165
185
  maxMessages: 4,
166
186
  maxOutputTokens: 1024
167
187
  },
@@ -173,7 +193,7 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
173
193
  memory: 'deepseek/deepseek-v3.2'
174
194
  },
175
195
  tools: {
176
- maxToolSteps: 50,
196
+ maxToolSteps: 200,
177
197
  outputLimitBytes: 400_000,
178
198
  enableBash: true,
179
199
  enableWebSearch: true,
@@ -189,7 +209,7 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
189
209
  timeoutMs: 20_000
190
210
  },
191
211
  bash: {
192
- timeoutMs: 120_000,
212
+ timeoutMs: 600_000,
193
213
  outputLimitBytes: 200_000
194
214
  },
195
215
  grepMaxFileBytes: 1_000_000,
@@ -225,7 +245,10 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
225
245
  enabled: true,
226
246
  model: 'edge-tts',
227
247
  baseUrl: '',
228
- defaultVoice: 'en-US-AriaNeural'
248
+ defaultVoice: 'en-US-AriaNeural',
249
+ provider: 'edge-tts',
250
+ openaiModel: 'tts-1',
251
+ openaiVoice: 'alloy'
229
252
  },
230
253
  browser: {
231
254
  enabled: true,
@@ -233,21 +256,27 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
233
256
  screenshotQuality: 80
234
257
  },
235
258
  mcp: {
236
- enabled: false,
259
+ enabled: true,
237
260
  servers: [],
238
261
  connectionTimeoutMs: 10_000
239
262
  },
240
263
  reasoning: {
241
- effort: 'low',
264
+ effort: 'medium',
242
265
  },
243
266
  skills: {
244
267
  enabled: true,
245
268
  maxSkills: 32,
246
269
  maxSummaryChars: 4000,
270
+ },
271
+ process: {
272
+ maxSessions: 16,
273
+ maxOutputBytes: 1_048_576,
274
+ defaultTimeoutMs: 1_800_000,
247
275
  }
248
276
  };
249
277
 
250
278
  let cachedConfig: AgentRuntimeConfig | null = null;
279
+ let cachedMtime: number | null = null;
251
280
 
252
281
  function cloneConfig<T>(value: T): T {
253
282
  return JSON.parse(JSON.stringify(value)) as T;
@@ -257,20 +286,25 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
257
286
  return typeof value === 'object' && value !== null && !Array.isArray(value);
258
287
  }
259
288
 
260
- function mergeDefaults<T>(base: T, overrides: unknown): T {
289
+ function mergeDefaults<T>(base: T, overrides: unknown, pathPrefix = ''): T {
261
290
  if (!isPlainObject(overrides)) return cloneConfig(base);
262
291
  const result = cloneConfig(base) as Record<string, unknown>;
263
292
  const baseObj = base as Record<string, unknown>;
264
293
  for (const [key, value] of Object.entries(overrides)) {
265
294
  const current = baseObj[key];
295
+ const fullPath = pathPrefix ? `${pathPrefix}.${key}` : key;
266
296
  if (isPlainObject(current) && isPlainObject(value)) {
267
- result[key] = mergeDefaults(current, value);
297
+ result[key] = mergeDefaults(current, value, fullPath);
268
298
  continue;
269
299
  }
270
300
  if (Array.isArray(current) && Array.isArray(value)) {
271
301
  result[key] = value;
272
302
  continue;
273
303
  }
304
+ if (current !== undefined && typeof value !== typeof current) {
305
+ console.error(`[agent-config] ${fullPath}: expected ${typeof current}, got ${typeof value}. Using default.`);
306
+ continue;
307
+ }
274
308
  if (typeof value === typeof current) {
275
309
  result[key] = value as unknown;
276
310
  }
@@ -291,7 +325,17 @@ function readJson(filePath: string): unknown {
291
325
  }
292
326
 
293
327
  export function loadAgentConfig(): AgentRuntimeConfig {
294
- if (cachedConfig) return cachedConfig;
328
+ if (cachedConfig) {
329
+ // Check if file has been modified since last load
330
+ try {
331
+ const stat = fs.statSync(CONFIG_PATH);
332
+ if (cachedMtime !== null && stat.mtimeMs === cachedMtime) {
333
+ return cachedConfig;
334
+ }
335
+ } catch {
336
+ return cachedConfig;
337
+ }
338
+ }
295
339
  const raw = readJson(CONFIG_PATH);
296
340
 
297
341
  let defaultModel = DEFAULT_DEFAULT_MODEL;
@@ -326,5 +370,11 @@ export function loadAgentConfig(): AgentRuntimeConfig {
326
370
  daemonHeartbeatIntervalMs,
327
371
  agent: mergeDefaults(DEFAULT_AGENT_CONFIG, agentOverrides)
328
372
  };
373
+ try {
374
+ const stat = fs.statSync(CONFIG_PATH);
375
+ cachedMtime = stat.mtimeMs;
376
+ } catch {
377
+ cachedMtime = null;
378
+ }
329
379
  return cachedConfig;
330
380
  }
@@ -36,6 +36,10 @@ export interface ContainerInput {
36
36
  modelOverride?: string;
37
37
  modelFallbacks?: string[];
38
38
  reasoningEffort?: 'off' | 'low' | 'medium' | 'high';
39
+ modelCapabilities?: {
40
+ context_length: number;
41
+ max_completion_tokens?: number;
42
+ };
39
43
  modelContextTokens?: number;
40
44
  modelMaxOutputTokens?: number;
41
45
  modelTemperature?: number;
@@ -84,6 +88,8 @@ export interface ContainerOutput {
84
88
  output_truncated?: boolean;
85
89
  }>;
86
90
  latency_ms?: number;
91
+ /** Reply-to message ID parsed from agent output [[reply_to:<id>]] tags */
92
+ replyToId?: string;
87
93
  /** Set by the host container-runner when stdout was truncated before parsing */
88
94
  stdoutTruncated?: boolean;
89
95
  }
@@ -204,11 +204,8 @@ async function processRequests(): Promise<void> {
204
204
  if (!output) {
205
205
  throw new Error('Agent worker returned no output');
206
206
  }
207
- if (fs.existsSync(cancelFile)) {
208
- try { fs.unlinkSync(cancelFile); } catch { /* already removed */ }
209
- try { fs.unlinkSync(filePath); } catch { /* request file already removed */ }
210
- continue;
211
- }
207
+ // Response completed successfully — always write it.
208
+ // Cancel during execution is already handled by runRequestWithCancellation().
212
209
  const responsePath = path.join(RESPONSES_DIR, `${requestId}.json`);
213
210
  const tmpPath = responsePath + '.tmp';
214
211
  fs.writeFileSync(tmpPath, JSON.stringify(output));
@@ -275,6 +272,21 @@ process.on('uncaughtException', (err) => {
275
272
  process.exit(1);
276
273
  });
277
274
 
275
+ // --- MCP hot-reload ---
276
+
277
+ const MCP_RELOAD_FILE = '/workspace/ipc/mcp_reload';
278
+
279
+ async function checkMcpReload(): Promise<void> {
280
+ try {
281
+ if (fs.existsSync(MCP_RELOAD_FILE)) {
282
+ fs.unlinkSync(MCP_RELOAD_FILE);
283
+ log('MCP reload signal detected — agent will pick up new config on next run');
284
+ }
285
+ } catch {
286
+ // ignore
287
+ }
288
+ }
289
+
278
290
  // --- Main loop ---
279
291
 
280
292
  async function loop(): Promise<void> {
@@ -284,6 +296,7 @@ async function loop(): Promise<void> {
284
296
 
285
297
  while (!shuttingDown) {
286
298
  try {
299
+ await checkMcpReload();
287
300
  await processRequests();
288
301
  } catch (err) {
289
302
  log(`Daemon loop error: ${err instanceof Error ? err.message : String(err)}`);