@slock-ai/daemon 0.35.0 → 0.36.1-alpha.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.
@@ -1,12 +1,205 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- buildFetchDispatcher
4
- } from "./chunk-GX2DVINN.js";
3
+ buildFetchDispatcher,
4
+ logger
5
+ } from "./chunk-E6OOH3IC.js";
5
6
 
6
7
  // src/chat-bridge.ts
7
8
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
10
  import { z } from "zod";
11
+
12
+ // src/historyFormatting.ts
13
+ function toLocalHistoryTime(iso) {
14
+ const d = new Date(iso);
15
+ if (Number.isNaN(d.getTime())) return iso;
16
+ const pad = (n) => String(n).padStart(2, "0");
17
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
18
+ }
19
+ function formatHistorySenderHandle(message) {
20
+ return message.senderDescription ? `@${message.senderName} \u2014 ${message.senderDescription}` : `@${message.senderName}`;
21
+ }
22
+ function formatHistoryMessageLine(message) {
23
+ const headerParts = [
24
+ `seq=${message.seq}`,
25
+ `msg=${message.id || "-"}`,
26
+ `time=${message.createdAt ? toLocalHistoryTime(message.createdAt) : "-"}`
27
+ ];
28
+ if (message.senderType) {
29
+ headerParts.push(`type=${message.senderType}`);
30
+ }
31
+ if (message.threadId) {
32
+ headerParts.push(`threadId=${message.threadId}`);
33
+ }
34
+ if ((message.replyCount ?? 0) > 0) {
35
+ headerParts.push(`replyCount=${message.replyCount}`);
36
+ }
37
+ const attachSuffix = message.attachments?.length ? ` [${message.attachments.length} attachment${message.attachments.length > 1 ? "s" : ""}: ${message.attachments.map((attachment) => `${attachment.filename} (id:${attachment.id})`).join(", ")} \u2014 use view_file to download]` : "";
38
+ const taskSuffix = message.taskStatus ? ` [task #${message.taskNumber} status=${message.taskStatus}${message.taskAssigneeId ? ` assignee=${message.taskAssigneeType}:${message.taskAssigneeId}` : ""}]` : "";
39
+ return `[${headerParts.join(" ")}] ${formatHistorySenderHandle(message)}: ${message.content}${attachSuffix}${taskSuffix}`;
40
+ }
41
+
42
+ // src/chatBridgeRequest.ts
43
+ var DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS = Number.parseInt(
44
+ process.env.SLOCK_CHAT_BRIDGE_TOOL_TIMEOUT_MS || "",
45
+ 10
46
+ ) || 6e4;
47
+ var ChatBridgeToolTimeoutError = class extends Error {
48
+ toolName;
49
+ target;
50
+ timeoutMs;
51
+ durationMs;
52
+ constructor(toolName, target, timeoutMs, durationMs) {
53
+ super(`${toolName} timed out after ${timeoutMs}ms${target ? ` (target: ${target})` : ""}`);
54
+ this.name = "ChatBridgeToolTimeoutError";
55
+ this.toolName = toolName;
56
+ this.target = target;
57
+ this.timeoutMs = timeoutMs;
58
+ this.durationMs = durationMs;
59
+ }
60
+ };
61
+ function describeError(err) {
62
+ if (err instanceof Error) return `${err.name}: ${err.message}`;
63
+ return String(err);
64
+ }
65
+ async function executeJsonRequest(url, init, {
66
+ toolName,
67
+ target = null,
68
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
69
+ fetchImpl,
70
+ now = () => Date.now(),
71
+ warn = (message) => logger.warn(message)
72
+ }) {
73
+ const startedAt = now();
74
+ const timeoutController = new AbortController();
75
+ const signals = [timeoutController.signal];
76
+ if (init.signal) signals.push(init.signal);
77
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
78
+ const timeout = setTimeout(() => {
79
+ timeoutController.abort();
80
+ }, timeoutMs);
81
+ timeout.unref?.();
82
+ try {
83
+ const response = await fetchImpl(url, { ...init, signal });
84
+ const data = await response.json();
85
+ return { response, data, durationMs: now() - startedAt };
86
+ } catch (err) {
87
+ const durationMs = now() - startedAt;
88
+ if (timeoutController.signal.aborted && !init.signal?.aborted) {
89
+ warn(
90
+ `[ChatBridgeTimeout] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} timeout_ms=${timeoutMs} outcome=timeout`
91
+ );
92
+ throw new ChatBridgeToolTimeoutError(toolName, target, timeoutMs, durationMs);
93
+ }
94
+ warn(
95
+ `[ChatBridgeError] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} outcome=error error=${describeError(err)}`
96
+ );
97
+ throw err;
98
+ } finally {
99
+ clearTimeout(timeout);
100
+ }
101
+ }
102
+ async function executeResponseRequest(url, init, {
103
+ toolName,
104
+ target = null,
105
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
106
+ fetchImpl,
107
+ now = () => Date.now(),
108
+ warn = (message) => logger.warn(message)
109
+ }) {
110
+ const startedAt = now();
111
+ const timeoutController = new AbortController();
112
+ const signals = [timeoutController.signal];
113
+ if (init.signal) signals.push(init.signal);
114
+ const signal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
115
+ const timeout = setTimeout(() => {
116
+ timeoutController.abort();
117
+ }, timeoutMs);
118
+ timeout.unref?.();
119
+ try {
120
+ const response = await fetchImpl(url, { ...init, signal });
121
+ return { response, durationMs: now() - startedAt };
122
+ } catch (err) {
123
+ const durationMs = now() - startedAt;
124
+ if (timeoutController.signal.aborted && !init.signal?.aborted) {
125
+ warn(
126
+ `[ChatBridgeTimeout] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} timeout_ms=${timeoutMs} outcome=timeout`
127
+ );
128
+ throw new ChatBridgeToolTimeoutError(toolName, target, timeoutMs, durationMs);
129
+ }
130
+ warn(
131
+ `[ChatBridgeError] tool=${toolName} target=${target ?? "-"} duration_ms=${durationMs} outcome=error error=${describeError(err)}`
132
+ );
133
+ throw err;
134
+ } finally {
135
+ clearTimeout(timeout);
136
+ }
137
+ }
138
+
139
+ // src/chatBridgeSendRequest.ts
140
+ import { randomUUID } from "crypto";
141
+ async function executeRetrySafeSendRequest(url, buildInit, {
142
+ fetchImpl,
143
+ target,
144
+ timeoutMs = DEFAULT_CHAT_BRIDGE_TOOL_TIMEOUT_MS,
145
+ now = () => Date.now(),
146
+ warn = (message) => logger.warn(message),
147
+ idempotencyKey = randomUUID()
148
+ }) {
149
+ let lastError;
150
+ for (let attempt = 1; attempt <= 2; attempt += 1) {
151
+ try {
152
+ const result = await executeJsonRequest(
153
+ url,
154
+ buildInit(idempotencyKey),
155
+ {
156
+ toolName: "send_message",
157
+ target,
158
+ timeoutMs,
159
+ fetchImpl,
160
+ now,
161
+ warn
162
+ }
163
+ );
164
+ return {
165
+ ...result,
166
+ idempotencyKey,
167
+ attempts: attempt
168
+ };
169
+ } catch (error) {
170
+ lastError = error;
171
+ if (attempt === 2) break;
172
+ warn(
173
+ `[ChatBridgeRetry] tool=send_message target=${target} attempt=${attempt + 1} reason=${describeRetryReason(error)}`
174
+ );
175
+ }
176
+ }
177
+ throw lastError;
178
+ }
179
+ function describeRetryReason(error) {
180
+ if (error && typeof error === "object" && "name" in error && error.name === "ChatBridgeToolTimeoutError") {
181
+ return error.message;
182
+ }
183
+ return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
184
+ }
185
+
186
+ // src/perfAttribution.ts
187
+ var PERF_CALLER_CONTEXT_HEADER = "X-Perf-Caller-Context";
188
+ var AGENT_ORIGINATED_CALLER_CONTEXT = "agent_originated";
189
+ function buildChatBridgeCommonHeaders(authToken2, { includeContentType = true } = {}) {
190
+ const headers = {
191
+ [PERF_CALLER_CONTEXT_HEADER]: AGENT_ORIGINATED_CALLER_CONTEXT
192
+ };
193
+ if (includeContentType) {
194
+ headers["Content-Type"] = "application/json";
195
+ }
196
+ if (authToken2) {
197
+ headers.Authorization = `Bearer ${authToken2}`;
198
+ }
199
+ return headers;
200
+ }
201
+
202
+ // src/chat-bridge.ts
10
203
  function toLocalTime(iso) {
11
204
  const d = new Date(iso);
12
205
  if (isNaN(d.getTime())) return iso;
@@ -26,15 +219,61 @@ if (!agentId) {
26
219
  console.error("Missing --agent-id");
27
220
  process.exit(1);
28
221
  }
29
- var commonHeaders = { "Content-Type": "application/json" };
30
- if (authToken) {
31
- commonHeaders["Authorization"] = `Bearer ${authToken}`;
32
- }
222
+ var commonHeaders = buildChatBridgeCommonHeaders(authToken);
33
223
  function bridgeFetch(url, init = {}) {
34
224
  const dispatcher = buildFetchDispatcher(url, process.env);
35
225
  const requestInit = dispatcher ? { ...init, dispatcher } : init;
36
226
  return fetch(url, requestInit);
37
227
  }
228
+ var RECENT_DELIVERY_CACHE_LIMIT = 5e3;
229
+ var deliveredMessageKeys = /* @__PURE__ */ new Set();
230
+ var deliveredMessageOrder = [];
231
+ function messageDeliveryKey(message) {
232
+ if (message.seq) return `seq:${message.seq}`;
233
+ if (message.message_id) return `msg:${message.message_id}`;
234
+ return null;
235
+ }
236
+ function rememberDeliveredMessages(messages) {
237
+ const unseen = [];
238
+ for (const message of messages) {
239
+ const key = messageDeliveryKey(message);
240
+ if (!key) {
241
+ unseen.push(message);
242
+ continue;
243
+ }
244
+ if (deliveredMessageKeys.has(key)) {
245
+ continue;
246
+ }
247
+ deliveredMessageKeys.add(key);
248
+ deliveredMessageOrder.push(key);
249
+ unseen.push(message);
250
+ }
251
+ while (deliveredMessageOrder.length > RECENT_DELIVERY_CACHE_LIMIT) {
252
+ const evicted = deliveredMessageOrder.shift();
253
+ if (evicted) deliveredMessageKeys.delete(evicted);
254
+ }
255
+ return unseen;
256
+ }
257
+ async function acknowledgeReceivedMessages(messages) {
258
+ const seqs = [...new Set(
259
+ messages.map((message) => message.seq).filter((seq) => typeof seq === "number" && Number.isInteger(seq) && seq > 0)
260
+ )];
261
+ if (seqs.length === 0) return;
262
+ try {
263
+ const res = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/receive-ack`, {
264
+ method: "POST",
265
+ headers: commonHeaders,
266
+ body: JSON.stringify({ seqs })
267
+ });
268
+ if (!res.ok) {
269
+ console.warn(`[chat-bridge] receive-ack failed (${res.status}) for agent ${agentId}; delivery will replay on the next poll`);
270
+ }
271
+ } catch (err) {
272
+ console.warn(
273
+ `[chat-bridge] receive-ack errored for agent ${agentId}; delivery will replay on the next poll: ${err instanceof Error ? err.message : String(err)}`
274
+ );
275
+ }
276
+ }
38
277
  function formatAttachmentSuffix(attachments) {
39
278
  if (!attachments?.length) return "";
40
279
  return ` [${attachments.length} attachment${attachments.length > 1 ? "s" : ""}: ${attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use view_file to download]`;
@@ -144,12 +383,18 @@ server.tool(
144
383
  },
145
384
  async ({ target, content, attachment_ids }) => {
146
385
  try {
147
- const res = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/send`, {
148
- method: "POST",
149
- headers: commonHeaders,
150
- body: JSON.stringify({ target, content, attachmentIds: attachment_ids })
151
- });
152
- const data = await res.json();
386
+ const { response: res, data } = await executeRetrySafeSendRequest(
387
+ `${serverUrl}/internal/agent/${agentId}/send`,
388
+ (idempotencyKey) => ({
389
+ method: "POST",
390
+ headers: commonHeaders,
391
+ body: JSON.stringify({ target, content, attachmentIds: attachment_ids, idempotencyKey })
392
+ }),
393
+ {
394
+ target,
395
+ fetchImpl: bridgeFetch
396
+ }
397
+ );
153
398
  if (!res.ok) {
154
399
  return {
155
400
  content: [
@@ -161,10 +406,14 @@ server.tool(
161
406
  const replyHint = shortId ? ` (to reply in this message's thread, use target "${target.includes(":") ? target : target + ":" + shortId}")` : "";
162
407
  let unreadSection = "";
163
408
  if (data.recentUnread && data.recentUnread.length > 0) {
164
- unreadSection = `
409
+ await acknowledgeReceivedMessages(data.recentUnread);
410
+ const unreadToShow = rememberDeliveredMessages(data.recentUnread);
411
+ if (unreadToShow.length > 0) {
412
+ unreadSection = `
165
413
 
166
414
  --- New messages you may have missed ---
167
- ${formatMessages(data.recentUnread)}`;
415
+ ${formatMessages(unreadToShow)}`;
416
+ }
168
417
  }
169
418
  return {
170
419
  content: [
@@ -184,7 +433,7 @@ ${formatMessages(data.recentUnread)}`;
184
433
  );
185
434
  server.tool(
186
435
  "upload_file",
187
- "Upload a file to attach to a message. Returns an attachment ID that you can pass to send_message's attachment_ids parameter. Images keep preview behavior; other files are sent as downloadable attachments. Max size: 5MB.",
436
+ "Upload a file to attach to a message. Returns an attachment ID that you can pass to send_message's attachment_ids parameter. Images keep preview behavior; other files are sent as downloadable attachments. Max size: 10MB.",
188
437
  {
189
438
  file_path: z.string().describe("Absolute path to the file on your local filesystem"),
190
439
  channel: z.string().describe("The channel target where this file will be used (e.g. '#general', 'dm:@richard')")
@@ -200,27 +449,32 @@ server.tool(
200
449
  };
201
450
  }
202
451
  const stat = fs.statSync(file_path);
203
- if (stat.size > 5 * 1024 * 1024) {
452
+ if (stat.size > 10 * 1024 * 1024) {
204
453
  return {
205
454
  isError: true,
206
- content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 5MB per file.` }]
455
+ content: [{ type: "text", text: `Error: File too large (${(stat.size / 1024 / 1024).toFixed(1)}MB). Max 10MB per file.` }]
207
456
  };
208
457
  }
209
- const listRes = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/resolve-channel`, {
210
- method: "POST",
211
- headers: commonHeaders,
212
- body: JSON.stringify({ target: channel })
213
- });
214
- let channelId;
215
- if (listRes.ok) {
216
- const listData = await listRes.json();
217
- channelId = listData.channelId;
218
- } else {
458
+ const { response: listRes, data: listData } = await executeJsonRequest(
459
+ `${serverUrl}/internal/agent/${agentId}/resolve-channel`,
460
+ {
461
+ method: "POST",
462
+ headers: commonHeaders,
463
+ body: JSON.stringify({ target: channel })
464
+ },
465
+ {
466
+ toolName: "upload_file.resolve_channel",
467
+ target: channel,
468
+ fetchImpl: bridgeFetch
469
+ }
470
+ );
471
+ if (!listRes.ok || !listData.channelId) {
219
472
  return {
220
473
  isError: true,
221
- content: [{ type: "text", text: `Error: Could not resolve channel: ${channel}` }]
474
+ content: [{ type: "text", text: `Error: ${listData.error || `Could not resolve channel: ${channel}`}` }]
222
475
  };
223
476
  }
477
+ const channelId = listData.channelId;
224
478
  const fileBuffer = fs.readFileSync(file_path);
225
479
  const filename = path.basename(file_path);
226
480
  const mimeType = guessMimeTypeFromFilename(filename);
@@ -228,16 +482,20 @@ server.tool(
228
482
  const formData = new FormData();
229
483
  formData.append("file", blob, filename);
230
484
  formData.append("channelId", channelId);
231
- const uploadHeaders = {};
232
- if (authToken) {
233
- uploadHeaders["Authorization"] = `Bearer ${authToken}`;
234
- }
235
- const res = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/upload`, {
236
- method: "POST",
237
- headers: uploadHeaders,
238
- body: formData
239
- });
240
- const data = await res.json();
485
+ const uploadHeaders = buildChatBridgeCommonHeaders(authToken, { includeContentType: false });
486
+ const { response: res, data } = await executeJsonRequest(
487
+ `${serverUrl}/internal/agent/${agentId}/upload`,
488
+ {
489
+ method: "POST",
490
+ headers: uploadHeaders,
491
+ body: formData
492
+ },
493
+ {
494
+ toolName: "upload_file",
495
+ target: channel,
496
+ fetchImpl: bridgeFetch
497
+ }
498
+ );
241
499
  if (!res.ok) {
242
500
  return {
243
501
  isError: true,
@@ -283,14 +541,19 @@ server.tool(
283
541
  content: [{ type: "text", text: `File already cached at: ${cachedPath}` }]
284
542
  };
285
543
  }
286
- const downloadHeaders = {};
287
- if (authToken) {
288
- downloadHeaders["Authorization"] = `Bearer ${authToken}`;
289
- }
290
- const res = await bridgeFetch(`${serverUrl}/api/attachments/${attachment_id}`, {
291
- headers: downloadHeaders,
292
- redirect: "follow"
293
- });
544
+ const downloadHeaders = buildChatBridgeCommonHeaders(authToken, { includeContentType: false });
545
+ const { response: res } = await executeResponseRequest(
546
+ `${serverUrl}/api/attachments/${attachment_id}`,
547
+ {
548
+ headers: downloadHeaders,
549
+ redirect: "follow"
550
+ },
551
+ {
552
+ toolName: "view_file",
553
+ target: attachment_id,
554
+ fetchImpl: bridgeFetch
555
+ }
556
+ );
294
557
  if (!res.ok) {
295
558
  return {
296
559
  isError: true,
@@ -320,17 +583,25 @@ server.tool(
320
583
  {},
321
584
  async () => {
322
585
  try {
323
- const res = await bridgeFetch(
586
+ const { response: res, data } = await executeJsonRequest(
324
587
  `${serverUrl}/internal/agent/${agentId}/receive`,
325
- { method: "GET", headers: commonHeaders }
588
+ { method: "GET", headers: commonHeaders },
589
+ {
590
+ toolName: "check_messages",
591
+ timeoutMs: 1e4,
592
+ fetchImpl: bridgeFetch
593
+ }
326
594
  );
327
595
  if (!res.ok) {
328
- const errData = await res.json().catch(() => ({}));
329
- return { isError: true, content: [{ type: "text", text: `Error: ${errData.error || res.statusText}` }] };
596
+ return { isError: true, content: [{ type: "text", text: `Error: ${data.error || res.statusText}` }] };
330
597
  }
331
- const data = await res.json();
332
- if (data.messages?.length > 0) {
333
- return { content: [{ type: "text", text: formatMessages(data.messages) }] };
598
+ const messages = data.messages ?? [];
599
+ if (messages.length > 0) {
600
+ await acknowledgeReceivedMessages(messages);
601
+ const messagesToShow = rememberDeliveredMessages(messages);
602
+ if (messagesToShow.length > 0) {
603
+ return { content: [{ type: "text", text: formatMessages(messagesToShow) }] };
604
+ }
334
605
  }
335
606
  return {
336
607
  content: [{ type: "text", text: "No new messages." }]
@@ -361,16 +632,22 @@ server.tool(
361
632
  {},
362
633
  async () => {
363
634
  try {
364
- const res = await bridgeFetch(
635
+ const { response: res, data } = await executeJsonRequest(
365
636
  `${serverUrl}/internal/agent/${agentId}/server`,
366
- { method: "GET", headers: commonHeaders }
637
+ { method: "GET", headers: commonHeaders },
638
+ {
639
+ toolName: "list_server",
640
+ fetchImpl: bridgeFetch
641
+ }
367
642
  );
368
- const data = await res.json();
369
643
  let text = "## Server\n\n";
644
+ const channels = data.channels ?? [];
645
+ const agents = data.agents ?? [];
646
+ const humans = data.humans ?? [];
370
647
  text += "### Channels\n";
371
648
  text += "Use `#channel-name` with send_message to post in a channel. `joined` means you currently belong to that channel.\n";
372
- if (data.channels?.length > 0) {
373
- for (const t of data.channels) {
649
+ if (channels.length > 0) {
650
+ for (const t of channels) {
374
651
  const status = t.joined ? "joined" : "not joined";
375
652
  text += t.description ? ` - #${t.name} [${status}] \u2014 ${t.description}
376
653
  ` : ` - #${t.name} [${status}]
@@ -381,8 +658,8 @@ server.tool(
381
658
  }
382
659
  text += "\n### Agents\n";
383
660
  text += "Other AI agents in this server.\n";
384
- if (data.agents?.length > 0) {
385
- for (const a of data.agents) {
661
+ if (agents.length > 0) {
662
+ for (const a of agents) {
386
663
  text += a.description ? ` - @${a.name} (${a.status}) \u2014 ${a.description}
387
664
  ` : ` - @${a.name} (${a.status})
388
665
  `;
@@ -392,8 +669,8 @@ server.tool(
392
669
  }
393
670
  text += "\n### Humans\n";
394
671
  text += 'To start a new DM: send_message(target="dm:@name"). To reply in an existing DM: reuse the target from received messages.\n';
395
- if (data.humans?.length > 0) {
396
- for (const u of data.humans) {
672
+ if (humans.length > 0) {
673
+ for (const u of humans) {
397
674
  text += u.description ? ` - @${u.name} \u2014 ${u.description}
398
675
  ` : ` - @${u.name}
399
676
  `;
@@ -438,11 +715,15 @@ server.tool(
438
715
  if (sender_id) params.set("senderId", sender_id);
439
716
  if (after) params.set("after", after);
440
717
  if (before) params.set("before", before);
441
- const res = await bridgeFetch(
718
+ const { response: res, data } = await executeJsonRequest(
442
719
  `${serverUrl}/internal/agent/${agentId}/search?${params}`,
443
- { method: "GET", headers: commonHeaders }
720
+ { method: "GET", headers: commonHeaders },
721
+ {
722
+ toolName: "search_messages",
723
+ target: channel ?? null,
724
+ fetchImpl: bridgeFetch
725
+ }
444
726
  );
445
- const data = await res.json();
446
727
  if (!res.ok) {
447
728
  return {
448
729
  content: [{ type: "text", text: `Error: ${data.error}` }]
@@ -500,11 +781,15 @@ server.tool(
500
781
  if (around !== void 0) params.set("around", String(around));
501
782
  if (before) params.set("before", String(before));
502
783
  if (after) params.set("after", String(after));
503
- const res = await bridgeFetch(
784
+ const { response: res, data } = await executeJsonRequest(
504
785
  `${serverUrl}/internal/agent/${agentId}/history?${params}`,
505
- { method: "GET", headers: commonHeaders }
786
+ { method: "GET", headers: commonHeaders },
787
+ {
788
+ toolName: "read_history",
789
+ target: channel,
790
+ fetchImpl: bridgeFetch
791
+ }
506
792
  );
507
- const data = await res.json();
508
793
  if (!res.ok) {
509
794
  return {
510
795
  content: [
@@ -519,15 +804,11 @@ server.tool(
519
804
  ]
520
805
  };
521
806
  }
522
- const formatted = data.messages.map((m) => {
523
- const senderType = ` type=${m.senderType}`;
524
- const time = m.createdAt ? toLocalTime(m.createdAt) : "-";
525
- const msgId = m.id || "-";
526
- const renderedContent = m.content;
527
- const attachSuffix = formatAttachmentSuffix(m.attachments);
528
- const taskSuffix = m.taskStatus ? ` [task #${m.taskNumber} status=${m.taskStatus}${m.taskAssigneeId ? ` assignee=${m.taskAssigneeType}:${m.taskAssigneeId}` : ""}]` : "";
529
- return `[seq=${m.seq} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(m)}: ${renderedContent}${attachSuffix}${taskSuffix}`;
530
- }).join("\n");
807
+ const formatted = data.messages.map((m) => formatHistoryMessageLine({
808
+ ...m,
809
+ senderName: m.senderName ?? m.sender_name ?? "unknown",
810
+ senderDescription: m.senderDescription ?? m.sender_description ?? null
811
+ })).join("\n");
531
812
  let footer = "";
532
813
  if (data.historyLimited) {
533
814
  footer = `
@@ -553,7 +834,7 @@ server.tool(
553
834
  }
554
835
  }
555
836
  let header = `## Message History for ${channel}${around ? ` around ${around}` : ""} (${data.messages.length} messages)`;
556
- if (data.last_read_seq > 0 && !after && !before && !around) {
837
+ if ((data.last_read_seq ?? 0) > 0 && !after && !before && !around) {
557
838
  header += `
558
839
  Your last read position: seq ${data.last_read_seq}. Use read_history(channel="${channel}", after=${data.last_read_seq}) to see only unread messages.`;
559
840
  }
@@ -587,11 +868,15 @@ server.tool(
587
868
  const params = new URLSearchParams();
588
869
  params.set("channel", channel);
589
870
  if (status !== "all") params.set("status", status);
590
- const res = await bridgeFetch(
871
+ const { response: res, data } = await executeJsonRequest(
591
872
  `${serverUrl}/internal/agent/${agentId}/tasks?${params}`,
592
- { method: "GET", headers: commonHeaders }
873
+ { method: "GET", headers: commonHeaders },
874
+ {
875
+ toolName: "list_tasks",
876
+ target: channel,
877
+ fetchImpl: bridgeFetch
878
+ }
593
879
  );
594
- const data = await res.json();
595
880
  if (!res.ok) {
596
881
  return {
597
882
  isError: true,
@@ -641,12 +926,19 @@ server.tool(
641
926
  },
642
927
  async ({ channel, tasks }) => {
643
928
  try {
644
- const res = await bridgeFetch(`${serverUrl}/internal/agent/${agentId}/tasks`, {
645
- method: "POST",
646
- headers: commonHeaders,
647
- body: JSON.stringify({ channel, tasks })
648
- });
649
- const data = await res.json();
929
+ const { response: res, data } = await executeJsonRequest(
930
+ `${serverUrl}/internal/agent/${agentId}/tasks`,
931
+ {
932
+ method: "POST",
933
+ headers: commonHeaders,
934
+ body: JSON.stringify({ channel, tasks })
935
+ },
936
+ {
937
+ toolName: "create_tasks",
938
+ target: channel,
939
+ fetchImpl: bridgeFetch
940
+ }
941
+ );
650
942
  if (!res.ok) {
651
943
  return {
652
944
  isError: true,
@@ -697,15 +989,19 @@ Thread messages cannot be claimed or converted into tasks. If a task is in "todo
697
989
  const body = { channel };
698
990
  if (task_numbers && task_numbers.length > 0) body.task_numbers = task_numbers;
699
991
  if (message_ids && message_ids.length > 0) body.message_ids = message_ids;
700
- const res = await bridgeFetch(
992
+ const { response: res, data } = await executeJsonRequest(
701
993
  `${serverUrl}/internal/agent/${agentId}/tasks/claim`,
702
994
  {
703
995
  method: "POST",
704
996
  headers: commonHeaders,
705
997
  body: JSON.stringify(body)
998
+ },
999
+ {
1000
+ toolName: "claim_tasks",
1001
+ target: channel,
1002
+ fetchImpl: bridgeFetch
706
1003
  }
707
1004
  );
708
- const data = await res.json();
709
1005
  if (!res.ok) {
710
1006
  return {
711
1007
  isError: true,
@@ -755,15 +1051,19 @@ server.tool(
755
1051
  },
756
1052
  async ({ channel, task_number }) => {
757
1053
  try {
758
- const res = await bridgeFetch(
1054
+ const { response: res, data } = await executeJsonRequest(
759
1055
  `${serverUrl}/internal/agent/${agentId}/tasks/unclaim`,
760
1056
  {
761
1057
  method: "POST",
762
1058
  headers: commonHeaders,
763
1059
  body: JSON.stringify({ channel, task_number })
1060
+ },
1061
+ {
1062
+ toolName: "unclaim_task",
1063
+ target: channel,
1064
+ fetchImpl: bridgeFetch
764
1065
  }
765
1066
  );
766
- const data = await res.json();
767
1067
  if (!res.ok) {
768
1068
  return {
769
1069
  isError: true,
@@ -793,15 +1093,19 @@ server.tool(
793
1093
  },
794
1094
  async ({ channel, task_number, status }) => {
795
1095
  try {
796
- const res = await bridgeFetch(
1096
+ const { response: res, data } = await executeJsonRequest(
797
1097
  `${serverUrl}/internal/agent/${agentId}/tasks/update-status`,
798
1098
  {
799
1099
  method: "POST",
800
1100
  headers: commonHeaders,
801
1101
  body: JSON.stringify({ channel, task_number, status })
1102
+ },
1103
+ {
1104
+ toolName: "update_task_status",
1105
+ target: channel,
1106
+ fetchImpl: bridgeFetch
802
1107
  }
803
1108
  );
804
- const data = await res.json();
805
1109
  if (!res.ok) {
806
1110
  return {
807
1111
  isError: true,