@swarmclawai/swarmclaw 0.6.2 → 0.6.3

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.
@@ -6,6 +6,17 @@ import { UPLOAD_DIR } from '../storage'
6
6
  import type { ToolBuildContext } from './context'
7
7
  import { safePath, truncate, listDirRecursive, MAX_OUTPUT, MAX_FILE } from './context'
8
8
 
9
+ const SEND_FILE_DEDUPE_TTL_MS = 30_000
10
+ const recentSendFileResults = new Map<string, { at: number; output: string; uploadPath: string }>()
11
+
12
+ function pruneRecentSendFileCache(now: number): void {
13
+ for (const [key, entry] of recentSendFileResults.entries()) {
14
+ if (now - entry.at > SEND_FILE_DEDUPE_TTL_MS || !fs.existsSync(entry.uploadPath)) {
15
+ recentSendFileResults.delete(key)
16
+ }
17
+ }
18
+ }
19
+
9
20
  export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[] {
10
21
  const tools: StructuredToolInterface[] = []
11
22
 
@@ -197,6 +208,8 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
197
208
  tool(
198
209
  async ({ filePath: rawPath }) => {
199
210
  try {
211
+ const now = Date.now()
212
+ pruneRecentSendFileCache(now)
200
213
  // Resolve relative to cwd, but also allow absolute paths
201
214
  const resolved = path.isAbsolute(rawPath) ? rawPath : path.resolve(bctx.cwd, rawPath)
202
215
  if (!fs.existsSync(resolved)) return `Error: file not found: ${rawPath}`
@@ -204,6 +217,13 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
204
217
  if (stat.isDirectory()) return `Error: cannot send a directory. Send individual files instead.`
205
218
  if (stat.size > 100 * 1024 * 1024) return `Error: file too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 100MB.`
206
219
 
220
+ const sessionId = bctx.ctx?.sessionId || 'no-session'
221
+ const dedupeKey = `${sessionId}|${resolved}`
222
+ const cached = recentSendFileResults.get(dedupeKey)
223
+ if (cached && now - cached.at <= SEND_FILE_DEDUPE_TTL_MS && fs.existsSync(cached.uploadPath)) {
224
+ return cached.output
225
+ }
226
+
207
227
  const ext = path.extname(resolved).slice(1).toLowerCase()
208
228
  const basename = path.basename(resolved)
209
229
  const filename = `${Date.now()}-${basename}`
@@ -212,14 +232,13 @@ export function buildFileTools(bctx: ToolBuildContext): StructuredToolInterface[
212
232
 
213
233
  const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico']
214
234
  const VIDEO_EXTS = ['mp4', 'webm', 'mov', 'avi', 'mkv']
235
+ const AUDIO_EXTS = ['mp3', 'ogg', 'wav', 'aac', 'm4a', 'opus']
215
236
 
216
- if (IMAGE_EXTS.includes(ext)) {
217
- return `![${basename}](/api/uploads/${filename})`
218
- } else if (VIDEO_EXTS.includes(ext)) {
219
- return `![${basename}](/api/uploads/${filename})`
220
- } else {
221
- return `[Download ${basename}](/api/uploads/${filename})`
222
- }
237
+ const output = (IMAGE_EXTS.includes(ext) || VIDEO_EXTS.includes(ext) || AUDIO_EXTS.includes(ext))
238
+ ? `![${basename}](/api/uploads/${filename})`
239
+ : `[Download ${basename}](/api/uploads/${filename})`
240
+ recentSendFileResults.set(dedupeKey, { at: now, output, uploadPath: dest })
241
+ return output
223
242
  } catch (err: unknown) {
224
243
  return `Error sending file: ${err instanceof Error ? err.message : String(err)}`
225
244
  }
@@ -278,10 +278,9 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
278
278
 
279
279
  if (Array.isArray(content)) {
280
280
  const parts: string[] = []
281
- let hasBinaryImage = false
281
+ const contentHasBinaryImage = content.some((c) => c.type === 'image' && !!c.data)
282
282
  for (const c of content) {
283
283
  if (c.type === 'image' && c.data) {
284
- hasBinaryImage = true
285
284
  const imageBuffer = Buffer.from(c.data, 'base64')
286
285
  const filename = `screenshot-${Date.now()}.png`
287
286
  const filepath = path.join(UPLOAD_DIR, filename)
@@ -306,8 +305,8 @@ export function buildWebTools(bctx: ToolBuildContext): StructuredToolInterface[]
306
305
  if (fs.existsSync(srcPath)) {
307
306
  const ext = path.extname(srcPath).slice(1).toLowerCase()
308
307
  const IMAGE_EXTS = ['png', 'jpg', 'jpeg', 'gif', 'webp']
309
- // Skip file-path images if we already have a binary image (avoids duplicates)
310
- if (IMAGE_EXTS.includes(ext) && hasBinaryImage) {
308
+ // Skip file-path images whenever MCP already returned image binary payloads.
309
+ if (IMAGE_EXTS.includes(ext) && contentHasBinaryImage) {
311
310
  parts.push(isError ? text : cleanPlaywrightOutput(text))
312
311
  } else {
313
312
  const filename = `browser-${Date.now()}.${ext}`
@@ -127,6 +127,12 @@ function buildAgenticExecutionPolicy(opts: {
127
127
  opts.enabledTools.includes('manage_connectors')
128
128
  ? 'If the user wants proactive outreach (e.g., WhatsApp updates), configure connectors and pair with schedules/tasks to deliver status updates.'
129
129
  : '',
130
+ opts.enabledTools.includes('manage_connectors')
131
+ ? 'Autonomous outreach is allowed for significant events (completed/failed tasks, blockers, deadlines, meaningful reminders from memory). Avoid casual or repetitive check-ins.'
132
+ : '',
133
+ opts.enabledTools.includes('manage_connectors')
134
+ ? 'When you proactively message through connectors, keep it concise and purposeful, and avoid sending duplicate updates about the same event.'
135
+ : '',
130
136
  opts.enabledTools.includes('manage_sessions')
131
137
  ? 'When coordinating platform work, inspect existing sessions and avoid duplicating active efforts.'
132
138
  : '',
@@ -164,6 +170,7 @@ function buildAgenticExecutionPolicy(opts: {
164
170
  'The test: if you saw this message from a friend, would you feel compelled to type something back? If not, NO_MESSAGE.',
165
171
  'Ask for confirmation only for high-risk or irreversible actions. For normal low-risk research/build steps, proceed autonomously.',
166
172
  'Default behavior is execution, not interrogation: do not ask exploratory clarification questions when a safe next action exists.',
173
+ 'Do not end every response with a question. Use declarative completion statements by default, and only ask a question when a concrete missing detail blocks the next action.',
167
174
  'Do not pause for a "continue" confirmation after the user has already asked you to execute a goal. Keep moving until blocked by permissions, missing credentials, or hard tool failures.',
168
175
  'Never repeat one-time side effects that are already complete (for example creating the same schedule/task again). Verify state first, then either continue execution or reply HEARTBEAT_OK.',
169
176
  'For main-loop tick messages that begin with "SWARM_MAIN_MISSION_TICK" or "SWARM_MAIN_AUTO_FOLLOWUP", follow that response contract exactly and include one valid [MAIN_LOOP_META] JSON line when you are not returning HEARTBEAT_OK.',