@botcord/daemon 0.2.60 → 0.2.61

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.
@@ -23,6 +23,13 @@ export interface BotCordChannelClient {
23
23
  hub_msg_id?: string;
24
24
  message_id?: string;
25
25
  } & Record<string, unknown>>;
26
+ sendTypedMessage?(to: string, type: "result" | "error", text: string, options?: {
27
+ replyTo?: string;
28
+ topic?: string;
29
+ }): Promise<{
30
+ hub_msg_id?: string;
31
+ message_id?: string;
32
+ } & Record<string, unknown>>;
26
33
  getHubUrl(): string;
27
34
  onTokenRefresh?: (token: string, expiresAt: number) => void;
28
35
  }
@@ -674,7 +674,9 @@ export function createBotCordChannel(options) {
674
674
  options.replyTo = message.replyTo;
675
675
  if (message.threadId)
676
676
  options.topic = message.threadId;
677
- const resp = await client.sendMessage(message.conversationId, message.text, options);
677
+ const resp = message.type === "error" && client.sendTypedMessage
678
+ ? await client.sendTypedMessage(message.conversationId, "error", message.text, options)
679
+ : await client.sendMessage(message.conversationId, message.text, options);
678
680
  const providerMessageId = (resp && typeof resp.hub_msg_id === "string" && resp.hub_msg_id) ||
679
681
  (resp && typeof resp.message_id === "string"
680
682
  ? resp.message_id
@@ -1022,6 +1022,7 @@ export class Dispatcher {
1022
1022
  accountId: msg.accountId,
1023
1023
  conversationId: msg.conversation.id,
1024
1024
  threadId: msg.conversation.threadId ?? null,
1025
+ type: "error",
1025
1026
  text: `⚠️ Runtime timeout after ${Math.round(this.turnTimeoutMs / 60000)} minute(s); aborted`,
1026
1027
  replyTo: msg.id,
1027
1028
  traceId: msg.trace?.id ?? null,
@@ -1067,6 +1068,7 @@ export class Dispatcher {
1067
1068
  accountId: msg.accountId,
1068
1069
  conversationId: msg.conversation.id,
1069
1070
  threadId: msg.conversation.threadId ?? null,
1071
+ type: "error",
1070
1072
  text: `⚠️ Runtime error: ${truncate(errMsg, 500)}`,
1071
1073
  replyTo: msg.id,
1072
1074
  traceId: msg.trace?.id ?? null,
@@ -1157,6 +1159,7 @@ export class Dispatcher {
1157
1159
  accountId: msg.accountId,
1158
1160
  conversationId: msg.conversation.id,
1159
1161
  threadId: msg.conversation.threadId ?? null,
1162
+ type: "error",
1160
1163
  text: `⚠️ Runtime error: ${truncate(result.error, 500)}`,
1161
1164
  replyTo: msg.id,
1162
1165
  traceId: msg.trace?.id ?? null,
@@ -154,6 +154,7 @@ export interface GatewayOutboundMessage {
154
154
  accountId: string;
155
155
  conversationId: string;
156
156
  threadId?: string | null;
157
+ type?: "message" | "error";
157
158
  text: string;
158
159
  attachments?: GatewayOutboundAttachment[];
159
160
  replyTo?: string | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.60",
3
+ "version": "0.2.61",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,6 +27,9 @@ function makeClient(overrides: Partial<BotCordChannelClient> = {}): BotCordChann
27
27
  sendMessage: vi
28
28
  .fn()
29
29
  .mockResolvedValue({ hub_msg_id: "m_provider", queued: true, status: "queued" }),
30
+ sendTypedMessage: vi
31
+ .fn()
32
+ .mockResolvedValue({ hub_msg_id: "m_provider_typed", queued: true, status: "queued" }),
30
33
  getHubUrl: vi.fn().mockReturnValue("http://127.0.0.1:1"),
31
34
  ...overrides,
32
35
  };
@@ -138,6 +141,34 @@ describe("createBotCordChannel — send()", () => {
138
141
  expect(client.sendMessage).toHaveBeenCalledWith("rm_dm_1", "hey", {});
139
142
  expect(result.providerMessageId).toBeNull();
140
143
  });
144
+
145
+ it("sends runtime diagnostics as BotCord error envelopes", async () => {
146
+ const client = makeClient();
147
+ const channel = createBotCordChannel({
148
+ id: "botcord-main",
149
+ accountId: "ag_self",
150
+ agentId: "ag_self",
151
+ client,
152
+ });
153
+ const result = await channel.send({
154
+ message: {
155
+ channel: "botcord",
156
+ accountId: "ag_self",
157
+ conversationId: "rm_group_a",
158
+ threadId: "tp_42",
159
+ replyTo: "env_source",
160
+ type: "error",
161
+ text: "Runtime error: boom",
162
+ },
163
+ log: silentLog,
164
+ });
165
+ expect(client.sendTypedMessage).toHaveBeenCalledWith("rm_group_a", "error", "Runtime error: boom", {
166
+ topic: "tp_42",
167
+ replyTo: "env_source",
168
+ });
169
+ expect(client.sendMessage).not.toHaveBeenCalled();
170
+ expect(result.providerMessageId).toBe("m_provider_typed");
171
+ });
141
172
  });
142
173
 
143
174
  // ---------------------------------------------------------------------------
@@ -263,6 +294,52 @@ describe("createBotCordChannel — inbox normalization", () => {
263
294
  }
264
295
  });
265
296
 
297
+ it("acks error InboxMessages without dispatching them", async () => {
298
+ const server = await startAuthOkServer();
299
+ const errorMessage = makeInbox({
300
+ hub_msg_id: "m_error_1",
301
+ text: undefined,
302
+ envelope: {
303
+ type: "error",
304
+ from: "ag_peer",
305
+ payload: { error: { code: "agent_error", message: "Runtime error: boom" } },
306
+ } as InboxMessage["envelope"],
307
+ });
308
+ const client = makeClient({
309
+ pollInbox: vi.fn().mockResolvedValue({ messages: [errorMessage], count: 1, has_more: false }),
310
+ getHubUrl: vi.fn().mockReturnValue(server.url),
311
+ });
312
+ const channel = createBotCordChannel({
313
+ id: "botcord-main",
314
+ accountId: "ag_self",
315
+ agentId: "ag_self",
316
+ client,
317
+ hubBaseUrl: server.url,
318
+ });
319
+ const abort = new AbortController();
320
+ const emits: GatewayInboundEnvelope[] = [];
321
+ const startPromise = channel.start({
322
+ config: stubConfig,
323
+ accountId: "ag_self",
324
+ abortSignal: abort.signal,
325
+ log: silentLog,
326
+ emit: async (env) => {
327
+ emits.push(env);
328
+ },
329
+ setStatus: () => {},
330
+ });
331
+ try {
332
+ await vi.waitFor(() => {
333
+ expect(client.ackMessages).toHaveBeenCalledWith(["m_error_1"]);
334
+ });
335
+ expect(emits).toHaveLength(0);
336
+ } finally {
337
+ abort.abort();
338
+ await startPromise;
339
+ await server.close();
340
+ }
341
+ });
342
+
266
343
  it("marks rm_dm_ and rm_oc_ rooms as direct; rm_oc_ also sets streamable + user-kind", async () => {
267
344
  const { emits, server } = await startWithInbox([
268
345
  makeInbox({
@@ -492,6 +492,7 @@ describe("Dispatcher", () => {
492
492
  await dispatcher.handle(makeEnvelope({ id: "msg_error" }));
493
493
 
494
494
  expect(channel.sends.length).toBe(1);
495
+ expect(channel.sends[0].message.type).toBe("error");
495
496
  expect(channel.sends[0].message.text).toContain("Runtime error");
496
497
  expect(channel.sends[0].message.text).toContain("missing openclawAgent");
497
498
  });
@@ -1256,6 +1257,7 @@ describe("Dispatcher", () => {
1256
1257
 
1257
1258
  await dispatcher.handle(makeEnvelope({ id: "m1" }));
1258
1259
  expect(channel.sends.length).toBe(1);
1260
+ expect(channel.sends[0].message.type).toBe("error");
1259
1261
  expect(channel.sends[0].message.text).toContain("Runtime error");
1260
1262
  expect(channel.sends[0].message.text).toContain("boom");
1261
1263
  expect(store.all().length).toBe(0);
@@ -1921,6 +1923,7 @@ describe("Dispatcher", () => {
1921
1923
  await p;
1922
1924
  expect(runtime.calls[0].signal.aborted).toBe(true);
1923
1925
  expect(channel.sends.length).toBe(1);
1926
+ expect(channel.sends[0].message.type).toBe("error");
1924
1927
  expect(channel.sends[0].message.text).toMatch(/Runtime timeout/);
1925
1928
  expect(channel.sends[0].message.conversationId).toBe("rm_g_other");
1926
1929
  expect(channel.sends[0].message.replyTo).toBe("m_to");
@@ -1941,6 +1944,7 @@ describe("Dispatcher", () => {
1941
1944
  }),
1942
1945
  );
1943
1946
  expect(channel.sends.length).toBe(1);
1947
+ expect(channel.sends[0].message.type).toBe("error");
1944
1948
  expect(channel.sends[0].message.text).toContain("Runtime error: boom");
1945
1949
  expect(channel.sends[0].message.conversationId).toBe("rm_g_other");
1946
1950
  expect(channel.sends[0].message.replyTo).toBe("m_err");
@@ -55,6 +55,12 @@ export interface BotCordChannelClient {
55
55
  text: string,
56
56
  options?: { replyTo?: string; topic?: string },
57
57
  ): Promise<{ hub_msg_id?: string; message_id?: string } & Record<string, unknown>>;
58
+ sendTypedMessage?(
59
+ to: string,
60
+ type: "result" | "error",
61
+ text: string,
62
+ options?: { replyTo?: string; topic?: string },
63
+ ): Promise<{ hub_msg_id?: string; message_id?: string } & Record<string, unknown>>;
58
64
  getHubUrl(): string;
59
65
  onTokenRefresh?: (token: string, expiresAt: number) => void;
60
66
  }
@@ -804,7 +810,10 @@ export function createBotCordChannel(options: BotCordChannelOptions): ChannelAda
804
810
  const options: { replyTo?: string; topic?: string } = {};
805
811
  if (message.replyTo) options.replyTo = message.replyTo;
806
812
  if (message.threadId) options.topic = message.threadId;
807
- const resp = await client.sendMessage(message.conversationId, message.text, options);
813
+ const resp =
814
+ message.type === "error" && client.sendTypedMessage
815
+ ? await client.sendTypedMessage(message.conversationId, "error", message.text, options)
816
+ : await client.sendMessage(message.conversationId, message.text, options);
808
817
  const providerMessageId =
809
818
  (resp && typeof resp.hub_msg_id === "string" && resp.hub_msg_id) ||
810
819
  (resp && typeof (resp as { message_id?: unknown }).message_id === "string"
@@ -1264,6 +1264,7 @@ export class Dispatcher {
1264
1264
  accountId: msg.accountId,
1265
1265
  conversationId: msg.conversation.id,
1266
1266
  threadId: msg.conversation.threadId ?? null,
1267
+ type: "error",
1267
1268
  text: `⚠️ Runtime timeout after ${Math.round(this.turnTimeoutMs / 60000)} minute(s); aborted`,
1268
1269
  replyTo: msg.id,
1269
1270
  traceId: msg.trace?.id ?? null,
@@ -1309,6 +1310,7 @@ export class Dispatcher {
1309
1310
  accountId: msg.accountId,
1310
1311
  conversationId: msg.conversation.id,
1311
1312
  threadId: msg.conversation.threadId ?? null,
1313
+ type: "error",
1312
1314
  text: `⚠️ Runtime error: ${truncate(errMsg, 500)}`,
1313
1315
  replyTo: msg.id,
1314
1316
  traceId: msg.trace?.id ?? null,
@@ -1398,6 +1400,7 @@ export class Dispatcher {
1398
1400
  accountId: msg.accountId,
1399
1401
  conversationId: msg.conversation.id,
1400
1402
  threadId: msg.conversation.threadId ?? null,
1403
+ type: "error",
1401
1404
  text: `⚠️ Runtime error: ${truncate(result.error, 500)}`,
1402
1405
  replyTo: msg.id,
1403
1406
  traceId: msg.trace?.id ?? null,
@@ -187,6 +187,7 @@ export interface GatewayOutboundMessage {
187
187
  accountId: string;
188
188
  conversationId: string;
189
189
  threadId?: string | null;
190
+ type?: "message" | "error";
190
191
  text: string;
191
192
  attachments?: GatewayOutboundAttachment[];
192
193
  replyTo?: string | null;