@dotsetlabs/dotclaw 2.4.0 → 2.6.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 (118) hide show
  1. package/.env.example +9 -10
  2. package/README.md +8 -4
  3. package/config-examples/runtime.json +34 -8
  4. package/config-examples/tool-policy.json +12 -2
  5. package/container/agent-runner/package-lock.json +2 -2
  6. package/container/agent-runner/package.json +1 -1
  7. package/container/agent-runner/src/agent-config.ts +19 -3
  8. package/container/agent-runner/src/container-protocol.ts +11 -0
  9. package/container/agent-runner/src/context-overflow-recovery.ts +39 -0
  10. package/container/agent-runner/src/index.ts +603 -165
  11. package/container/agent-runner/src/openrouter-input.ts +159 -0
  12. package/container/agent-runner/src/system-prompt.ts +13 -3
  13. package/container/agent-runner/src/tool-loop-policy.ts +741 -0
  14. package/container/agent-runner/src/tools.ts +211 -8
  15. package/dist/agent-context.d.ts +1 -0
  16. package/dist/agent-context.d.ts.map +1 -1
  17. package/dist/agent-context.js +21 -9
  18. package/dist/agent-context.js.map +1 -1
  19. package/dist/agent-execution.d.ts +2 -0
  20. package/dist/agent-execution.d.ts.map +1 -1
  21. package/dist/agent-execution.js +164 -15
  22. package/dist/agent-execution.js.map +1 -1
  23. package/dist/agent-semaphore.d.ts +24 -1
  24. package/dist/agent-semaphore.d.ts.map +1 -1
  25. package/dist/agent-semaphore.js +109 -20
  26. package/dist/agent-semaphore.js.map +1 -1
  27. package/dist/cli.js +3 -11
  28. package/dist/cli.js.map +1 -1
  29. package/dist/config.d.ts +2 -0
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +2 -0
  32. package/dist/config.js.map +1 -1
  33. package/dist/container-protocol.d.ts +22 -0
  34. package/dist/container-protocol.d.ts.map +1 -1
  35. package/dist/container-protocol.js.map +1 -1
  36. package/dist/container-runner.d.ts +7 -0
  37. package/dist/container-runner.d.ts.map +1 -1
  38. package/dist/container-runner.js +417 -143
  39. package/dist/container-runner.js.map +1 -1
  40. package/dist/db.d.ts.map +1 -1
  41. package/dist/db.js +46 -12
  42. package/dist/db.js.map +1 -1
  43. package/dist/error-messages.d.ts.map +1 -1
  44. package/dist/error-messages.js +18 -4
  45. package/dist/error-messages.js.map +1 -1
  46. package/dist/failover-policy.d.ts +41 -0
  47. package/dist/failover-policy.d.ts.map +1 -0
  48. package/dist/failover-policy.js +261 -0
  49. package/dist/failover-policy.js.map +1 -0
  50. package/dist/index.js +1 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/ipc-dispatcher.d.ts.map +1 -1
  53. package/dist/ipc-dispatcher.js +27 -43
  54. package/dist/ipc-dispatcher.js.map +1 -1
  55. package/dist/mcp-config.d.ts +22 -0
  56. package/dist/mcp-config.d.ts.map +1 -0
  57. package/dist/mcp-config.js +94 -0
  58. package/dist/mcp-config.js.map +1 -0
  59. package/dist/memory-backend.d.ts +27 -0
  60. package/dist/memory-backend.d.ts.map +1 -0
  61. package/dist/memory-backend.js +112 -0
  62. package/dist/memory-backend.js.map +1 -0
  63. package/dist/memory-recall.d.ts.map +1 -1
  64. package/dist/memory-recall.js +135 -22
  65. package/dist/memory-recall.js.map +1 -1
  66. package/dist/memory-store.d.ts +1 -0
  67. package/dist/memory-store.d.ts.map +1 -1
  68. package/dist/memory-store.js +55 -7
  69. package/dist/memory-store.js.map +1 -1
  70. package/dist/message-pipeline.d.ts +24 -0
  71. package/dist/message-pipeline.d.ts.map +1 -1
  72. package/dist/message-pipeline.js +131 -27
  73. package/dist/message-pipeline.js.map +1 -1
  74. package/dist/metrics.d.ts +1 -0
  75. package/dist/metrics.d.ts.map +1 -1
  76. package/dist/metrics.js +9 -0
  77. package/dist/metrics.js.map +1 -1
  78. package/dist/providers/discord/discord-provider.d.ts.map +1 -1
  79. package/dist/providers/discord/discord-provider.js +72 -4
  80. package/dist/providers/discord/discord-provider.js.map +1 -1
  81. package/dist/providers/telegram/telegram-provider.d.ts.map +1 -1
  82. package/dist/providers/telegram/telegram-provider.js +65 -3
  83. package/dist/providers/telegram/telegram-provider.js.map +1 -1
  84. package/dist/recall-policy.d.ts +12 -0
  85. package/dist/recall-policy.d.ts.map +1 -0
  86. package/dist/recall-policy.js +89 -0
  87. package/dist/recall-policy.js.map +1 -0
  88. package/dist/runtime-config.d.ts +33 -0
  89. package/dist/runtime-config.d.ts.map +1 -1
  90. package/dist/runtime-config.js +109 -9
  91. package/dist/runtime-config.js.map +1 -1
  92. package/dist/streaming.d.ts.map +1 -1
  93. package/dist/streaming.js +125 -33
  94. package/dist/streaming.js.map +1 -1
  95. package/dist/task-scheduler.d.ts.map +1 -1
  96. package/dist/task-scheduler.js +4 -2
  97. package/dist/task-scheduler.js.map +1 -1
  98. package/dist/tool-policy.d.ts.map +1 -1
  99. package/dist/tool-policy.js +26 -4
  100. package/dist/tool-policy.js.map +1 -1
  101. package/dist/trace-writer.d.ts +12 -0
  102. package/dist/trace-writer.d.ts.map +1 -1
  103. package/dist/trace-writer.js.map +1 -1
  104. package/dist/turn-hygiene.d.ts +14 -0
  105. package/dist/turn-hygiene.d.ts.map +1 -0
  106. package/dist/turn-hygiene.js +214 -0
  107. package/dist/turn-hygiene.js.map +1 -0
  108. package/dist/webhook.d.ts.map +1 -1
  109. package/dist/webhook.js +1 -0
  110. package/dist/webhook.js.map +1 -1
  111. package/package.json +15 -1
  112. package/scripts/benchmark-baseline.js +365 -0
  113. package/scripts/benchmark-harness.js +1413 -0
  114. package/scripts/benchmark-scenarios.js +301 -0
  115. package/scripts/canary-suite.js +123 -0
  116. package/scripts/generate-controlled-traces.js +230 -0
  117. package/scripts/release-slo-check.js +214 -0
  118. package/scripts/run-live-canary.js +339 -0
package/.env.example CHANGED
@@ -8,16 +8,18 @@
8
8
  # (they are NOT read from .env unless the script explicitly loads dotenv).
9
9
 
10
10
  # --- Required (app runtime) ---
11
+ # Set at least one provider token (Telegram or Discord).
12
+
11
13
  # Telegram bot token from @BotFather
12
- TELEGRAM_BOT_TOKEN=123456789:replace-with-real-token
14
+ # TELEGRAM_BOT_TOKEN=123456789:replace-with-real-token
15
+
16
+ # Discord bot token (from Discord Developer Portal)
17
+ # DISCORD_BOT_TOKEN=replace-with-discord-token
13
18
 
14
19
  # OpenRouter API key
15
20
  OPENROUTER_API_KEY=sk-or-replace-with-real-key
16
21
 
17
22
  # --- Optional (app runtime) ---
18
- # Discord bot token (enables Discord provider)
19
- # DISCORD_BOT_TOKEN=replace-with-discord-token
20
-
21
23
  # Brave Search API key (enables WebSearch tool in the agent)
22
24
  BRAVE_SEARCH_API_KEY=replace-with-brave-key
23
25
 
@@ -27,11 +29,8 @@ TZ=America/New_York
27
29
  # GitHub Personal Access Token (enables gh CLI in containers)
28
30
  # GH_TOKEN=ghp_your_token_here
29
31
 
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
32
+ # Container-only overrides (for example OPENAI_API_KEY or DOTCLAW_VISION_MODEL)
33
+ # should be set per group in ~/.dotclaw/data/registered_groups.json under containerConfig.env.
35
34
 
36
35
  # --- Optional: System (set in shell before starting) ---
37
36
  # Override DotClaw home directory (defaults to ~/.dotclaw)
@@ -63,7 +62,7 @@ TZ=America/New_York
63
62
  # AUTOTUNE_OUTPUT_DIR=~/.dotclaw/prompts
64
63
  # AUTOTUNE_CANARY_FRACTION=0.1
65
64
  # AUTOTUNE_BEHAVIOR_CONFIG_PATH=~/.dotclaw/config/behavior.json
66
- # AUTOTUNE_BEHAVIOR_REPORT_DIR=~/.dotclaw/config
65
+ # AUTOTUNE_BEHAVIOR_REPORT_DIR=~/.dotclaw/data
67
66
  # AUTOTUNE_BEHAVIOR_ENABLED=1
68
67
 
69
68
  # --- Internal (do not set) ---
package/README.md CHANGED
@@ -23,9 +23,10 @@ Personal OpenRouter-based assistant for Telegram and Discord. Each request runs
23
23
 
24
24
  - Node.js 20+
25
25
  - Docker (running)
26
- - Telegram bot token (from @BotFather)
26
+ - At least one provider token:
27
+ - Telegram bot token (from @BotFather), or
28
+ - Discord bot token
27
29
  - OpenRouter API key
28
- - Discord bot token (optional — for Discord provider)
29
30
 
30
31
  ## Quick Start
31
32
 
@@ -64,7 +65,10 @@ dotclaw groups # List registered chats
64
65
  dotclaw build # Build the Docker container image
65
66
  dotclaw add-instance # Create and start an isolated instance
66
67
  dotclaw instances # List discovered instances
68
+ dotclaw install-service # Install launchd/systemd service
69
+ dotclaw uninstall-service # Remove launchd/systemd service
67
70
  dotclaw version # Show installed version
71
+ dotclaw help # Show help
68
72
  ```
69
73
 
70
74
  Instance flags:
@@ -80,7 +84,7 @@ All configuration and data is stored in `~/.dotclaw/`:
80
84
 
81
85
  ```
82
86
  ~/.dotclaw/
83
- .env # Secrets (Telegram, OpenRouter keys)
87
+ .env # Secrets (provider tokens, OpenRouter key, optional Brave/GH keys)
84
88
  config/
85
89
  runtime.json # Runtime overrides
86
90
  model.json # Model selection
@@ -119,7 +123,7 @@ Or see:
119
123
  ## Development
120
124
 
121
125
  ```bash
122
- npm run dev # Run with hot reload
126
+ npm run dev # Run from source (tsx)
123
127
  npm run dev:up # Full dev cycle: rebuild container + kill stale daemons + start dev
124
128
  npm run dev:down # Remove all running dotclaw agent containers
125
129
  npm run build # Compile TypeScript (host)
@@ -4,8 +4,17 @@
4
4
  "container": {
5
5
  "mode": "daemon",
6
6
  "privileged": true,
7
+ "daemonPollMs": 100,
7
8
  "instanceId": ""
8
9
  },
10
+ "messageQueue": {
11
+ "promptMaxChars": 24000
12
+ },
13
+ "concurrency": {
14
+ "maxAgents": 4,
15
+ "laneStarvationMs": 15000,
16
+ "maxConsecutiveInteractive": 3
17
+ },
9
18
  "metrics": {
10
19
  "port": 3001,
11
20
  "enabled": true
@@ -20,6 +29,10 @@
20
29
  },
21
30
  "embeddings": {
22
31
  "enabled": true
32
+ },
33
+ "backend": {
34
+ "strategy": "builtin",
35
+ "modulePath": ""
23
36
  }
24
37
  },
25
38
  "routing": {
@@ -30,7 +43,11 @@
30
43
  "maxToolSteps": 200,
31
44
  "temperature": 0.6,
32
45
  "recallMaxResults": 8,
33
- "recallMaxTokens": 1500
46
+ "recallMaxTokens": 1500,
47
+ "hostFailover": {
48
+ "enabled": true,
49
+ "maxRetries": 1
50
+ }
34
51
  },
35
52
  "webhook": {
36
53
  "enabled": false,
@@ -39,7 +56,7 @@
39
56
  },
40
57
  "streaming": {
41
58
  "enabled": true,
42
- "chunkFlushIntervalMs": 200,
59
+ "chunkFlushIntervalMs": 120,
43
60
  "editIntervalMs": 400,
44
61
  "maxEditLength": 3800
45
62
  },
@@ -57,6 +74,7 @@
57
74
  "assistantName": "Rain",
58
75
  "reasoning": { "effort": "medium" },
59
76
  "context": {
77
+ "recentContextTokens": 0,
60
78
  "maxHistoryTurns": 40,
61
79
  "contextPruning": {
62
80
  "softTrimMaxChars": 4000,
@@ -72,22 +90,30 @@
72
90
  "enableBash": true,
73
91
  "enableWebSearch": true,
74
92
  "enableWebFetch": true,
93
+ "completionGuard": {
94
+ "idempotentRetryAttempts": 2,
95
+ "idempotentRetryBackoffMs": 500,
96
+ "repeatedSignatureThreshold": 3,
97
+ "repeatedRoundThreshold": 2,
98
+ "nonRetryableFailureThreshold": 3,
99
+ "forceSynthesisAfterTools": true
100
+ },
75
101
  "bash": {
76
102
  "timeoutMs": 600000
77
- },
78
- "process": {
79
- "maxSessions": 16,
80
- "maxOutputBytes": 1048576,
81
- "defaultTimeoutMs": 1800000
82
103
  }
83
104
  },
105
+ "process": {
106
+ "maxSessions": 16,
107
+ "maxOutputBytes": 1048576,
108
+ "defaultTimeoutMs": 1800000
109
+ },
84
110
  "tts": {
85
111
  "provider": "edge-tts",
86
112
  "openaiModel": "tts-1",
87
113
  "openaiVoice": "alloy"
88
114
  },
89
115
  "mcp": {
90
- "enabled": true,
116
+ "enabled": false,
91
117
  "servers": []
92
118
  }
93
119
  }
@@ -13,6 +13,8 @@
13
13
  "Bash",
14
14
  "Python",
15
15
  "Browser",
16
+ "Process",
17
+ "AnalyzeImage",
16
18
  "mcp__dotclaw__send_message",
17
19
  "mcp__dotclaw__send_file",
18
20
  "mcp__dotclaw__send_photo",
@@ -41,14 +43,22 @@
41
43
  "mcp__dotclaw__memory_forget",
42
44
  "mcp__dotclaw__memory_list",
43
45
  "mcp__dotclaw__memory_search",
44
- "mcp__dotclaw__memory_stats"
46
+ "mcp__dotclaw__memory_stats",
47
+ "mcp__dotclaw__get_config",
48
+ "mcp__dotclaw__set_tool_policy",
49
+ "mcp__dotclaw__set_behavior",
50
+ "mcp__dotclaw__set_mcp_config",
51
+ "mcp__dotclaw__subagent"
45
52
  ],
46
53
  "deny": [],
47
54
  "max_per_run": {
48
55
  "Bash": 128,
49
56
  "Python": 64,
50
57
  "WebSearch": 40,
51
- "WebFetch": 60
58
+ "WebFetch": 60,
59
+ "Process": 128,
60
+ "AnalyzeImage": 16,
61
+ "mcp__dotclaw__subagent": 8
52
62
  },
53
63
  "default_max_per_run": 256
54
64
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "dotclaw-agent-runner",
3
- "version": "1.9.0",
3
+ "version": "2.3.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "dotclaw-agent-runner",
9
- "version": "1.9.0",
9
+ "version": "2.3.0",
10
10
  "dependencies": {
11
11
  "@openrouter/sdk": "^0.3.0",
12
12
  "cron-parser": "^5.0.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dotclaw-agent-runner",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "description": "Container-side agent runner for DotClaw",
6
6
  "main": "dist/index.js",
@@ -56,6 +56,14 @@ export type AgentRuntimeConfig = {
56
56
  enableBash: boolean;
57
57
  enableWebSearch: boolean;
58
58
  enableWebFetch: boolean;
59
+ completionGuard: {
60
+ idempotentRetryAttempts: number;
61
+ idempotentRetryBackoffMs: number;
62
+ repeatedSignatureThreshold: number;
63
+ repeatedRoundThreshold: number;
64
+ nonRetryableFailureThreshold: number;
65
+ forceSynthesisAfterTools: boolean;
66
+ };
59
67
  webfetch: {
60
68
  blockPrivate: boolean;
61
69
  allowlist: string[];
@@ -143,7 +151,7 @@ export type AgentRuntimeConfig = {
143
151
 
144
152
  const CONFIG_PATH = '/workspace/config/runtime.json';
145
153
  const DEFAULT_DEFAULT_MODEL = 'moonshotai/kimi-k2.5';
146
- const DEFAULT_DAEMON_POLL_MS = 200;
154
+ const DEFAULT_DAEMON_POLL_MS = 100;
147
155
  const DEFAULT_DAEMON_HEARTBEAT_INTERVAL_MS = 1_000;
148
156
 
149
157
  const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
@@ -163,7 +171,7 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
163
171
  context: {
164
172
  maxContextTokens: 128_000,
165
173
  compactionTriggerTokens: 120_000,
166
- recentContextTokens: 0, // 0 = auto: 50% of model context window
174
+ recentContextTokens: 0, // 0 = auto: 35% of model context window (capped at 24K)
167
175
  summaryUpdateEveryMessages: 20,
168
176
  maxOutputTokens: 8192,
169
177
  summaryMaxOutputTokens: 2048,
@@ -198,6 +206,14 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
198
206
  enableBash: true,
199
207
  enableWebSearch: true,
200
208
  enableWebFetch: true,
209
+ completionGuard: {
210
+ idempotentRetryAttempts: 2,
211
+ idempotentRetryBackoffMs: 500,
212
+ repeatedSignatureThreshold: 3,
213
+ repeatedRoundThreshold: 2,
214
+ nonRetryableFailureThreshold: 3,
215
+ forceSynthesisAfterTools: true
216
+ },
201
217
  webfetch: {
202
218
  blockPrivate: true,
203
219
  allowlist: [],
@@ -256,7 +272,7 @@ const DEFAULT_AGENT_CONFIG: AgentRuntimeConfig['agent'] = {
256
272
  screenshotQuality: 80
257
273
  },
258
274
  mcp: {
259
- enabled: true,
275
+ enabled: false,
260
276
  servers: [],
261
277
  connectionTimeoutMs: 10_000
262
278
  },
@@ -13,6 +13,7 @@ export interface ContainerInput {
13
13
  userName?: string;
14
14
  maxToolSteps?: number;
15
15
  memoryRecall?: string[];
16
+ memoryRecallAttempted?: boolean;
16
17
  userProfile?: string | null;
17
18
  memoryStats?: {
18
19
  total: number;
@@ -92,4 +93,14 @@ export interface ContainerOutput {
92
93
  replyToId?: string;
93
94
  /** Set by the host container-runner when stdout was truncated before parsing */
94
95
  stdoutTruncated?: boolean;
96
+ /** Number of in-loop idempotent tool retries attempted */
97
+ tool_retry_attempts?: number;
98
+ /** True when a forced final synthesis pass was used after tool execution */
99
+ tool_outcome_verification_forced?: boolean;
100
+ /** True when repeated tool signatures triggered loop-break protection */
101
+ tool_loop_breaker_triggered?: boolean;
102
+ /** Reason emitted when the tool loop breaker triggered */
103
+ tool_loop_breaker_reason?: string;
104
+ /** Error from fire-and-forget memory extraction in daemon mode */
105
+ memory_extraction_error?: string;
95
106
  }
@@ -0,0 +1,39 @@
1
+ export type ContextMessageLike = {
2
+ role: 'user' | 'assistant';
3
+ content: string;
4
+ };
5
+
6
+ export type ContextOverflowRecoveryPlan = {
7
+ toCompact: ContextMessageLike[];
8
+ toKeep: ContextMessageLike[];
9
+ retryInput: Array<{ role: 'user' | 'assistant'; content: string }>;
10
+ };
11
+
12
+ export function buildContextOverflowRecoveryPlan(params: {
13
+ contextMessages: ContextMessageLike[];
14
+ emergencySummary?: string | null;
15
+ keepRecentCount?: number;
16
+ }): ContextOverflowRecoveryPlan {
17
+ const keepRecentCount = Number.isFinite(params.keepRecentCount)
18
+ ? Math.max(1, Math.floor(Number(params.keepRecentCount)))
19
+ : 4;
20
+ const contextMessages = Array.isArray(params.contextMessages)
21
+ ? params.contextMessages
22
+ : [];
23
+
24
+ const keepStart = Math.max(0, contextMessages.length - keepRecentCount);
25
+ const toCompact = contextMessages.slice(0, keepStart);
26
+ const toKeep = contextMessages.slice(keepStart);
27
+ const emergencySummary = (params.emergencySummary || '').trim();
28
+
29
+ const retryInput = emergencySummary
30
+ ? [{ role: 'user' as const, content: `[Previous conversation summary: ${emergencySummary}]` },
31
+ ...toKeep.map(msg => ({ role: msg.role, content: msg.content }))]
32
+ : toKeep.map(msg => ({ role: msg.role, content: msg.content }));
33
+
34
+ return {
35
+ toCompact,
36
+ toKeep,
37
+ retryInput
38
+ };
39
+ }