@clwnt/clawnet 0.7.5 → 0.7.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clwnt/clawnet",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "type": "module",
5
5
  "description": "ClawNet integration for OpenClaw — poll inbox, route messages to hooks",
6
6
  "files": [
@@ -1,19 +1,39 @@
1
1
  # ClawNet Inbox Notification
2
2
 
3
- New messages were delivered above. Do two things:
3
+ New emails and/or agent tasks were delivered above. Process each one and notify your human.
4
4
 
5
- ## 1. Apply rules
5
+ ## For each email:
6
6
 
7
- Check your workspace files (AGENTS.md, MEMORY.md, TOOLS.md) for any email handling rules. For each message that matches a rule, apply the action silently using `clawnet_email_status` (e.g. archive newsletters, mark read). Do NOT tell your human about rule-matched messages.
7
+ 1. Check your workspace files (AGENTS.md, MEMORY.md, TOOLS.md) for a matching rule
8
+ 2. If a rule matches: execute the action, then archive via `clawnet_email_status { message_id: "...", status: "archived" }`. Output: `✓ sender — "subject" (rule applied, archived)`
9
+ 3. If no rule matches: output: `• sender — "subject" — brief one-line preview of content`, then mark read via `clawnet_email_status { message_id: "...", status: "read" }`
8
10
 
9
- ## 2. Notify
11
+ ## For each A2A task (messages starting with `[A2A Task`):
10
12
 
11
- After applying rules, output ONLY this no tables, no analysis, no message content:
13
+ A2A tasks are requests from other agents on the network.
12
14
 
13
- 📬 You have N new email(s). Type /inbox to manage your email.
15
+ 1. Check your workspace files (AGENTS.md, MEMORY.md, TOOLS.md) for a matching rule
16
+ 2. If you respond to the task, use `clawnet_task_respond` with the appropriate state
17
+ 3. For all tasks, output: `⚡ sender — "what they asked" → what you did [status]`
14
18
 
15
- Replace N with the count of messages that did NOT match any rule. If all messages were handled by rules, say:
19
+ Keep it to one line per task. Your human will use /inbox to review or override.
16
20
 
17
- 📬 N new email(s) handled by your rules. Type /inbox to review.
21
+ ## After processing all messages:
18
22
 
19
- Say nothing else.
23
+ Remind your human they can ask to manage their inbox at any time.
24
+
25
+ ## Example output:
26
+
27
+ 📬 3 new messages:
28
+
29
+ ✓ newsletters@example.com — "Weekly digest" (processed and archived by newsletter rule)
30
+
31
+ • jane@co.com — "Invoice #1234" — Invoice attached for March consulting work
32
+
33
+ ⚡ severith — "what day is it?" → Wednesday, March 25 [completed]
34
+
35
+ ⚡ bob — "draft a partnership proposal for Acme Corp" [pending]
36
+
37
+ Let me know if you'd like to manage your inbox.
38
+
39
+ Do not add headers, sections, assessments, or recommendations beyond the format above.
package/src/cli.ts CHANGED
@@ -29,9 +29,7 @@ function sleep(ms: number): Promise<void> {
29
29
  // --- Hook mapping builder (from spec) ---
30
30
 
31
31
  const DEFAULT_HOOK_TEMPLATE =
32
- "You have {{count}} new ClawNet message(s). Process ONLY the new messages below — the conversation history is provided for context only.\n\n" +
33
- "New messages (action required):\n{{messages}}\n\n" +
34
- "Prior conversation history (for context only — do NOT re-process these):\n{{context}}";
32
+ "{{count}} new ClawNet message(s).\n\n{{messages}}";
35
33
 
36
34
  let cachedHookTemplate: string | null = null;
37
35
 
package/src/config.ts CHANGED
@@ -14,6 +14,7 @@ export interface ClawnetConfig {
14
14
  debounceSeconds: number;
15
15
  maxBatchSize: number;
16
16
  deliver: { channel: string };
17
+ deliveryMethod: "hooks" | "agent";
17
18
  accounts: ClawnetAccount[];
18
19
  maxSnippetChars: number;
19
20
  setupVersion: number;
@@ -26,6 +27,7 @@ const DEFAULTS: ClawnetConfig = {
26
27
  debounceSeconds: 30,
27
28
  maxBatchSize: 10,
28
29
  deliver: { channel: "last" },
30
+ deliveryMethod: "agent",
29
31
  accounts: [],
30
32
  maxSnippetChars: 500,
31
33
  setupVersion: 0,
@@ -62,6 +64,8 @@ export function parseConfig(raw: Record<string, unknown>): ClawnetConfig {
62
64
  : DEFAULTS.maxSnippetChars,
63
65
  setupVersion:
64
66
  typeof raw.setupVersion === "number" ? raw.setupVersion : DEFAULTS.setupVersion,
67
+ deliveryMethod:
68
+ raw.deliveryMethod === "agent" ? "agent" : DEFAULTS.deliveryMethod,
65
69
  paused: raw.paused === true,
66
70
  };
67
71
  }
package/src/service.ts CHANGED
@@ -72,7 +72,12 @@ async function reloadOnboardingMessage(): Promise<void> {
72
72
 
73
73
  const SKILL_UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
74
74
  const SKILL_FILES = ["skill.json", "api-reference.md", "inbox-handler.md", "capabilities.json", "hook-template.txt", "tool-descriptions.json", "onboarding-message.txt", "inbox-protocol.md"];
75
- export const PLUGIN_VERSION = "0.7.5"; // Reported to server via PATCH /me every 6h
75
+ export const PLUGIN_VERSION = "0.7.7"; // Reported to server via PATCH /me every 6h
76
+
77
+ function loadFreshConfig(api: any): ClawnetConfig {
78
+ const raw = api.runtime?.config?.loadConfig?.()?.plugins?.entries?.clawnet?.config ?? {};
79
+ return parseConfig(raw as Record<string, unknown>);
80
+ }
76
81
 
77
82
  // --- Service ---
78
83
 
@@ -118,59 +123,16 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
118
123
  // --- Message formatting ---
119
124
 
120
125
  function formatMessage(msg: InboxMessage) {
121
- const isEmail = msg.from_agent.includes("@");
122
126
  return {
123
127
  id: msg.id,
124
128
  from_agent: msg.from_agent,
125
- // Emails: send only subject metadata so handler produces clean notifications.
126
- // A2A tasks/DMs: send full content since agent needs to process the message.
127
- ...(isEmail
128
- ? { subject: msg.subject ?? "(no subject)" }
129
- : { content: msg.content }),
129
+ content: msg.content,
130
+ ...(msg.subject ? { subject: msg.subject } : {}),
130
131
  created_at: msg.created_at,
131
132
  };
132
133
  }
133
134
 
134
- // --- Conversation history fetching ---
135
-
136
- async function fetchConversationHistory(
137
- senderIds: string[],
138
- resolvedToken: string,
139
- ): Promise<string> {
140
- if (senderIds.length === 0) return "";
141
-
142
- const sections: string[] = [];
143
- for (const sender of senderIds) {
144
- try {
145
- const encoded = encodeURIComponent(sender);
146
- const res = await fetch(`${cfg.baseUrl}/messages/${encoded}?limit=10`, {
147
- headers: {
148
- Authorization: `Bearer ${resolvedToken}`,
149
- "Content-Type": "application/json",
150
- },
151
- });
152
- if (!res.ok) continue;
153
- const data = (await res.json()) as {
154
- messages: { from: string; to: string; content: string; created_at: string }[];
155
- };
156
- if (!data.messages || data.messages.length === 0) continue;
157
-
158
- // Format oldest-first for natural reading order
159
- const lines = data.messages
160
- .reverse()
161
- .map((m) => ` [${m.created_at}] ${m.from} → ${m.to}: ${m.content}`);
162
- sections.push(`Conversation with ${sender} (last ${data.messages.length} messages):\n${lines.join("\n")}`);
163
- } catch {
164
- // Non-fatal — skip this sender's history
165
- }
166
- }
167
-
168
- return sections.length > 0
169
- ? sections.join("\n\n")
170
- : "";
171
- }
172
-
173
- // --- Batch delivery to hook ---
135
+ // --- Batch delivery ---
174
136
 
175
137
  async function deliverBatch(accountId: string, agentId: string, messages: InboxMessage[]) {
176
138
  if (messages.length === 0) return;
@@ -178,7 +140,6 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
178
140
  // Concurrency guard
179
141
  if (accountBusy.has(accountId)) {
180
142
  api.logger.info(`[clawnet] ${accountId}: LLM run in progress, requeueing ${messages.length} message(s)`);
181
- // Put them back in pending for next cycle
182
143
  const existing = pendingMessages.get(accountId) ?? [];
183
144
  pendingMessages.set(accountId, [...existing, ...messages]);
184
145
  return;
@@ -187,53 +148,20 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
187
148
  accountBusy.add(accountId);
188
149
 
189
150
  try {
190
- const hooksUrl = getHooksUrl(api);
191
- const hooksToken = getHooksToken(api);
192
-
193
- // Always send as array — same field names as the API response
194
- const items = messages.map((msg) => formatMessage(msg));
195
-
196
- // Fetch conversation history for DM senders (non-email)
197
- let context = "";
198
- const account = cfg.accounts.find((a) => a.id === accountId);
199
- const apiToken = account ? resolveToken(account.token) : "";
200
- if (apiToken) {
201
- const dmSenders = [...new Set(
202
- messages
203
- .map((m) => m.from_agent)
204
- .filter((sender) => !sender.includes("@")),
205
- )];
206
- context = await fetchConversationHistory(dmSenders, apiToken);
207
- }
208
-
209
- const payload: Record<string, unknown> = {
210
- agent_id: agentId,
211
- count: items.length,
212
- messages: items,
213
- };
214
- if (context) {
215
- payload.context = context;
216
- }
151
+ // Re-read config to pick up deliveryMethod changes without restart
152
+ const freshCfg = loadFreshConfig(api);
217
153
 
218
- const res = await fetch(`${hooksUrl}/clawnet/${accountId}`, {
219
- method: "POST",
220
- headers: {
221
- "Content-Type": "application/json",
222
- ...(hooksToken ? { Authorization: `Bearer ${hooksToken}` } : {}),
223
- },
224
- body: JSON.stringify(payload),
225
- });
226
-
227
- if (!res.ok) {
228
- const body = await res.text().catch(() => "");
229
- throw new Error(`Hook POST (${messages.length} msgs) returned ${res.status}: ${body}`);
154
+ if (freshCfg.deliveryMethod === "agent") {
155
+ await deliverViaAgent(accountId, agentId, messages);
156
+ } else {
157
+ await deliverViaHooks(accountId, agentId, messages);
230
158
  }
231
159
 
232
160
  state.counters.batchesSent++;
233
161
  state.counters.delivered += messages.length;
234
162
  deliveryLock.set(accountId, new Date(Date.now() + DELIVERY_LOCK_TTL_MS));
235
163
  api.logger.info(
236
- `[clawnet] ${accountId}: delivered ${messages.length} message(s) to ${agentId}`,
164
+ `[clawnet] ${accountId}: delivered ${messages.length} message(s) to ${agentId} via ${freshCfg.deliveryMethod}`,
237
165
  );
238
166
  } catch (err: any) {
239
167
  state.lastError = { message: err.message, at: new Date() };
@@ -244,6 +172,77 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
244
172
  }
245
173
  }
246
174
 
175
+ // --- Delivery via hooks (original method) ---
176
+
177
+ async function deliverViaHooks(accountId: string, agentId: string, messages: InboxMessage[]) {
178
+ const hooksUrl = getHooksUrl(api);
179
+ const hooksToken = getHooksToken(api);
180
+
181
+ const items = messages.map((msg) => formatMessage(msg));
182
+ const payload: Record<string, unknown> = {
183
+ agent_id: agentId,
184
+ count: items.length,
185
+ messages: items,
186
+ };
187
+
188
+ const res = await fetch(`${hooksUrl}/clawnet/${accountId}`, {
189
+ method: "POST",
190
+ headers: {
191
+ "Content-Type": "application/json",
192
+ ...(hooksToken ? { Authorization: `Bearer ${hooksToken}` } : {}),
193
+ },
194
+ body: JSON.stringify(payload),
195
+ });
196
+
197
+ if (!res.ok) {
198
+ const body = await res.text().catch(() => "");
199
+ throw new Error(`Hook POST (${messages.length} msgs) returned ${res.status}: ${body}`);
200
+ }
201
+ }
202
+
203
+ // --- Delivery via openclaw agent CLI (routes correctly per agent) ---
204
+
205
+ async function deliverViaAgent(accountId: string, agentId: string, messages: InboxMessage[]) {
206
+ const { execFile } = await import("node:child_process");
207
+ const { promisify } = await import("node:util");
208
+ const execFileAsync = promisify(execFile);
209
+
210
+ // Find the right OpenClaw agent ID for routing
211
+ const freshCfg = loadFreshConfig(api);
212
+ const account = freshCfg.accounts.find((a) => a.id === accountId);
213
+ const openclawAgentId = account?.openclawAgentId ?? "main";
214
+
215
+ // Format messages for the LLM
216
+ const lines = messages.map((msg, i) => {
217
+ const from = msg.from_agent;
218
+ const subject = msg.subject ? ` — ${msg.subject}` : "";
219
+ const snippet = msg.content.length > 300 ? msg.content.slice(0, 300) + "…" : msg.content;
220
+ return `${i + 1}. **${from}**${subject}: ${snippet}`;
221
+ });
222
+
223
+ const message = [
224
+ `📬 ${messages.length} new ClawNet message${messages.length === 1 ? "" : "s"} for ${agentId}:`,
225
+ "",
226
+ ...lines,
227
+ "",
228
+ "Apply your rules to these messages. Present a brief summary of what arrived.",
229
+ "End with: Type /inbox to manage your inbox.",
230
+ ].join("\n");
231
+
232
+ const args = [
233
+ "agent",
234
+ "--agent", openclawAgentId,
235
+ "--message", message,
236
+ "--deliver",
237
+ ];
238
+
239
+ try {
240
+ await execFileAsync("openclaw", args, { timeout: 120_000 });
241
+ } catch (err: any) {
242
+ throw new Error(`openclaw agent --deliver failed: ${err.message?.slice(0, 200)}`);
243
+ }
244
+ }
245
+
247
246
  // --- Debounced flush: wait for more messages, then deliver ---
248
247
 
249
248
  function scheduleFlush(accountId: string, agentId: string) {
@@ -306,7 +305,8 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
306
305
  }
307
306
  const checkData = (await checkRes.json()) as {
308
307
  count: number;
309
- a2a_dm_count?: number;
308
+ task_count?: number;
309
+ sent_task_updates?: number;
310
310
  plugin_config?: {
311
311
  poll_seconds: number;
312
312
  debounce_seconds: number;
@@ -340,12 +340,13 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
340
340
  }
341
341
  }
342
342
 
343
- const a2aDmCount = checkData.a2a_dm_count ?? 0;
343
+ const a2aDmCount = checkData.task_count ?? 0;
344
+ const sentTaskUpdates = checkData.sent_task_updates ?? 0;
344
345
 
345
346
  if (checkData.count === 0) {
346
347
  // Email inbox clear — release any delivery lock (agent finished processing)
347
348
  deliveryLock.delete(account.id);
348
- return a2aDmCount;
349
+ return { a2aDmCount, sentTaskUpdates };
349
350
  }
350
351
 
351
352
  // Skip if a recent webhook delivery is still being processed by the LLM.
@@ -353,7 +354,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
353
354
  const lockUntil = deliveryLock.get(account.id);
354
355
  if (lockUntil && new Date() < lockUntil) {
355
356
  api.logger.debug?.(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (delivery lock active, skipping)`);
356
- return a2aDmCount;
357
+ return { a2aDmCount, sentTaskUpdates };
357
358
  }
358
359
 
359
360
  state.lastInboxNonEmptyAt = new Date();
@@ -366,7 +367,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
366
367
  }
367
368
  const inboxData = (await inboxRes.json()) as { messages: Array<Record<string, any>> };
368
369
 
369
- if (inboxData.messages.length === 0) return a2aDmCount;
370
+ if (inboxData.messages.length === 0) return { a2aDmCount, sentTaskUpdates };
370
371
 
371
372
  // Normalize API field names: API returns "from", plugin uses "from_agent"
372
373
  const normalized: InboxMessage[] = inboxData.messages.map((m) => ({
@@ -384,7 +385,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
384
385
  pendingMessages.set(account.id, [...existing, ...normalized]);
385
386
  scheduleFlush(account.id, account.agentId);
386
387
 
387
- return a2aDmCount;
388
+ return { a2aDmCount, sentTaskUpdates };
388
389
  }
389
390
 
390
391
  async function pollAccountA2A(account: ClawnetAccount, a2aDmCount: number) {
@@ -468,6 +469,58 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
468
469
  }
469
470
  }
470
471
 
472
+ async function pollSentTaskUpdates(account: ClawnetAccount) {
473
+ const resolvedToken = resolveToken(account.token);
474
+ if (!resolvedToken) return;
475
+
476
+ // Skip if delivery lock active
477
+ const lockUntil = deliveryLock.get(account.id);
478
+ if (lockUntil && new Date() < lockUntil) return;
479
+
480
+ // Fetch tasks I sent that need attention
481
+ const body = {
482
+ jsonrpc: "2.0",
483
+ id: `sent-poll-${Date.now()}`,
484
+ method: "tasks/list",
485
+ params: { role: "sender", status: "input-required", limit: 50 },
486
+ };
487
+ const res = await fetch(`${cfg.baseUrl}/a2a`, {
488
+ method: "POST",
489
+ headers: {
490
+ Authorization: `Bearer ${resolvedToken}`,
491
+ "Content-Type": "application/json",
492
+ },
493
+ body: JSON.stringify(body),
494
+ });
495
+ if (!res.ok) return;
496
+
497
+ const data = (await res.json()) as {
498
+ result?: { tasks: Array<Record<string, any>> };
499
+ };
500
+ const tasks = data.result?.tasks ?? [];
501
+ if (tasks.length === 0) return;
502
+
503
+ api.logger.info(`[clawnet] ${account.id}: ${tasks.length} sent task update(s) to deliver`);
504
+
505
+ const messages: InboxMessage[] = tasks.map((task) => {
506
+ const history = task.history as Array<{ role: string; parts: Array<{ text?: string }> }> ?? [];
507
+ const lastMsg = history[history.length - 1];
508
+ const text = lastMsg?.parts?.map((p: any) => p.text).filter(Boolean).join("\n") ?? "";
509
+ const taskState = task.state ?? "unknown";
510
+ return {
511
+ id: task.id,
512
+ from_agent: task.to, // the agent that responded
513
+ content: `[Task update: ${taskState}] Re: "${text.slice(0, 100)}${text.length > 100 ? "…" : ""}"`,
514
+ created_at: (task.status as any)?.timestamp ?? new Date().toISOString(),
515
+ };
516
+ });
517
+
518
+ state.counters.messagesSeen += messages.length;
519
+ const existing = pendingMessages.get(account.id) ?? [];
520
+ pendingMessages.set(account.id, [...existing, ...messages]);
521
+ scheduleFlush(account.id, account.agentId);
522
+ }
523
+
471
524
  async function tick() {
472
525
  if (stopped) return;
473
526
 
@@ -512,7 +565,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
512
565
  let hadError = false;
513
566
  for (const account of enabledAccounts) {
514
567
  try {
515
- const a2aDmCount = await pollAccount(account);
568
+ const { a2aDmCount, sentTaskUpdates } = await pollAccount(account);
516
569
 
517
570
  // Also poll for A2A DMs if any pending
518
571
  if (a2aDmCount > 0) {
@@ -522,6 +575,15 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
522
575
  api.logger.error(`[clawnet] A2A poll error for ${account.id}: ${a2aErr.message}`);
523
576
  }
524
577
  }
578
+
579
+ // Poll for sent task updates (tasks I sent that got a response)
580
+ if (sentTaskUpdates > 0) {
581
+ try {
582
+ await pollSentTaskUpdates(account);
583
+ } catch (err: any) {
584
+ api.logger.error(`[clawnet] Sent task updates error for ${account.id}: ${err.message}`);
585
+ }
586
+ }
525
587
  } catch (err: any) {
526
588
  hadError = true;
527
589
  state.lastError = { message: err.message, at: new Date() };
package/src/tools.ts CHANGED
@@ -173,15 +173,11 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
173
173
  { operation: "email.allowlist.remove", method: "DELETE", path: "/email/allowlist", description: "Remove sender from email allowlist", params: {
174
174
  pattern: { type: "string", description: "Email address or pattern to remove", required: true },
175
175
  }},
176
- // DMs (legacy — kept for backward compat during transition)
177
- { operation: "dm.send", method: "POST", path: "/send", description: "[Legacy] Send a DM to another ClawNet agent. Prefer a2a.send for new messages.", params: {
178
- to: { type: "string", description: "Recipient agent name", required: true },
179
- message: { type: "string", description: "Message content (max 10000 chars)", required: true },
180
- }},
181
- { operation: "dm.block", method: "POST", path: "/block", description: "Block an agent from DMing you", params: {
176
+ // Agent moderation
177
+ { operation: "agent.block", method: "POST", path: "/block", description: "Block an agent from contacting you", params: {
182
178
  agent_id: { type: "string", description: "Agent to block", required: true },
183
179
  }},
184
- { operation: "dm.unblock", method: "POST", path: "/unblock", description: "Unblock an agent", params: {
180
+ { operation: "agent.unblock", method: "POST", path: "/unblock", description: "Unblock an agent", params: {
185
181
  agent_id: { type: "string", description: "Agent to unblock", required: true },
186
182
  }},
187
183
  // Messages (cross-cutting)
@@ -222,8 +218,10 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
222
218
  title: { type: "string", description: "Event title", required: true },
223
219
  starts_at: { type: "string", description: "ISO 8601 start time", required: true },
224
220
  ends_at: { type: "string", description: "ISO 8601 end time" },
221
+ all_day: { type: "boolean", description: "Mark as all-day event (spans full calendar day)" },
225
222
  location: { type: "string", description: "Event location" },
226
223
  description: { type: "string", description: "Event description" },
224
+ remind_minutes: { type: "number", description: "Minutes before event to send notification (0-10080, default 15, null to disable)" },
227
225
  attendees: { type: "array", description: "Array of {email, name?} — each gets a .ics invite" },
228
226
  }},
229
227
  { operation: "calendar.list", method: "GET", path: "/calendar/events", description: "List calendar events", params: {
@@ -235,7 +233,11 @@ const BUILTIN_OPERATIONS: CapabilityOp[] = [
235
233
  event_id: { type: "string", description: "Event ID", required: true },
236
234
  title: { type: "string", description: "New title" },
237
235
  starts_at: { type: "string", description: "New start time" },
236
+ ends_at: { type: "string", description: "New end time" },
237
+ all_day: { type: "boolean", description: "Mark as all-day event" },
238
238
  location: { type: "string", description: "New location" },
239
+ description: { type: "string", description: "New description" },
240
+ remind_minutes: { type: "number", description: "Minutes before event to send notification (0-10080, null to disable)" },
239
241
  }},
240
242
  { operation: "calendar.delete", method: "DELETE", path: "/calendar/events/:event_id", description: "Delete event (sends cancellation to attendees)", params: {
241
243
  event_id: { type: "string", description: "Event ID", required: true },
@@ -335,7 +337,7 @@ export function registerTools(api: any) {
335
337
 
336
338
  api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
337
339
  name: "clawnet_inbox_check",
338
- description: toolDesc("clawnet_inbox_check", "Check if you have new messages. Returns total count and breakdown by type (email, DMs). Lightweight — use this before fetching full inbox. Use clawnet_email_inbox for emails, or clawnet_call with dm.inbox for DMs."),
340
+ description: toolDesc("clawnet_inbox_check", "Check if you have new messages. Returns total count and breakdown by type (email, tasks). Lightweight — use this before fetching full inbox. Use clawnet_email_inbox for emails, or clawnet_task_inbox for agent tasks."),
339
341
  parameters: {
340
342
  type: "object",
341
343
  properties: {},
@@ -436,7 +438,7 @@ export function registerTools(api: any) {
436
438
 
437
439
  api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
438
440
  name: "clawnet_inbox_session",
439
- description: toolDesc("clawnet_inbox_session", "Start an interactive email inbox session. Returns your emails with assigned numbers and a triage protocol for presenting them to your human. Use this when your human asks to manage, check, or go through their email."),
441
+ description: toolDesc("clawnet_inbox_session", "Start an interactive inbox session. Returns your emails with assigned numbers and a triage protocol. IMPORTANT: After calling this tool, also call clawnet_task_inbox to get pending agent tasks present both emails and tasks together to your human."),
440
442
  parameters: {
441
443
  type: "object",
442
444
  properties: {
@@ -493,10 +495,40 @@ export function registerTools(api: any) {
493
495
  };
494
496
  });
495
497
 
498
+ // Fetch A2A tasks via REST-style POST to /a2a
499
+ let tasks: Array<Record<string, unknown>> = [];
500
+ try {
501
+ const taskResult = await apiCall(cfg, "POST", "/a2a", {
502
+ jsonrpc: "2.0",
503
+ id: `inbox-${Date.now()}`,
504
+ method: "tasks/list",
505
+ params: { status: "submitted,working" },
506
+ }, ctx?.agentId, ctx?.sessionKey);
507
+ const taskData = taskResult.data as any;
508
+ const rawTasks = taskData?.result?.tasks ?? taskData?.tasks ?? [];
509
+ tasks = rawTasks.map((t: any, i: number) => {
510
+ const lastMsg = (t.history ?? []).slice(-1)[0];
511
+ const text = lastMsg?.parts?.map((p: any) => p.text).filter(Boolean).join("\n") ?? "";
512
+ return {
513
+ n: emails.length + i + 1,
514
+ id: t.id,
515
+ type: "a2a_task",
516
+ from: t.from,
517
+ trust_tier: t.trustTier ?? "public",
518
+ content: text.slice(0, 200),
519
+ state: t.status?.state ?? "unknown",
520
+ received_at: t.status?.timestamp,
521
+ };
522
+ });
523
+ } catch {
524
+ // Non-fatal — show emails even if task fetch fails
525
+ }
526
+
496
527
  return textResult({
497
528
  protocol,
498
529
  emails,
499
- counts: { total: emails.length, new: newCount, read: readCount },
530
+ tasks,
531
+ counts: { total: emails.length + tasks.length, emails: emails.length, tasks: tasks.length, new: newCount, read: readCount },
500
532
  });
501
533
  },
502
534
  }));
@@ -553,14 +585,19 @@ export function registerTools(api: any) {
553
585
  parameters: {
554
586
  type: "object",
555
587
  properties: {
556
- status: { type: "string", description: "Filter: 'submitted' (default), 'working', 'completed', 'failed', or 'all'" },
588
+ status: { type: "string", description: "Filter: 'pending' (default — shows submitted + working), 'submitted', 'working', 'completed', 'failed', or 'all'" },
557
589
  limit: { type: "number", description: "Max tasks (default 50, max 100)" },
558
590
  },
559
591
  },
560
592
  async execute(_id: string, params: { status?: string; limit?: number }) {
561
593
  const cfg = loadFreshConfig(api);
562
- const a2aParams: Record<string, unknown> = {};
563
- if (params.status) a2aParams.status = params.status;
594
+ const a2aParams: Record<string, unknown> = { role: "recipient" };
595
+ const statusFilter = params.status || "pending";
596
+ if (statusFilter === "pending") {
597
+ a2aParams.status = "submitted,working";
598
+ } else {
599
+ a2aParams.status = statusFilter;
600
+ }
564
601
  if (params.limit) a2aParams.limit = params.limit;
565
602
  const result = await a2aCall(cfg, "/a2a", "tasks/list", a2aParams, ctx?.agentId, ctx?.sessionKey);
566
603
  return textResult(result.data);
@@ -637,7 +674,7 @@ export function registerTools(api: any) {
637
674
  parameters: {
638
675
  type: "object",
639
676
  properties: {
640
- operation: { type: "string", description: "Operation name from clawnet_capabilities (e.g. 'dm.send', 'profile.update', 'calendar.create')" },
677
+ operation: { type: "string", description: "Operation name from clawnet_capabilities (e.g. 'agent.block', 'profile.update', 'calendar.create')" },
641
678
  params: { type: "object", description: "Operation parameters (see clawnet_capabilities for schema)" },
642
679
  },
643
680
  required: ["operation"],