@clwnt/clawnet 0.7.12 → 0.7.14

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.12",
3
+ "version": "0.7.14",
4
4
  "type": "module",
5
5
  "description": "ClawNet integration for OpenClaw — poll inbox, route messages to hooks",
6
6
  "files": [
package/src/service.ts CHANGED
@@ -73,7 +73,7 @@ async function reloadOnboardingMessage(): Promise<void> {
73
73
 
74
74
  const SKILL_UPDATE_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6 hours
75
75
  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"];
76
- export const PLUGIN_VERSION = "0.7.12"; // Reported to server via PATCH /me every 6h
76
+ export const PLUGIN_VERSION = "0.7.13"; // Reported to server via PATCH /me every 6h
77
77
 
78
78
  function loadFreshConfig(api: any): ClawnetConfig {
79
79
  const raw = api.runtime?.config?.loadConfig?.()?.plugins?.entries?.clawnet?.config ?? {};
@@ -141,6 +141,16 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
141
141
  async function deliverBatch(accountId: string, agentId: string, messages: InboxMessage[]) {
142
142
  if (messages.length === 0) return;
143
143
 
144
+ // Delivery lock — respect the cooldown even from flush/debounce paths
145
+ const lockUntil = deliveryLock.get(accountId);
146
+ if (lockUntil && new Date() < lockUntil) {
147
+ const existing = pendingMessages.get(accountId) ?? [];
148
+ const existingIds = new Set(existing.map((m) => m.id));
149
+ const fresh = messages.filter((m) => !existingIds.has(m.id));
150
+ pendingMessages.set(accountId, [...existing, ...fresh]);
151
+ return;
152
+ }
153
+
144
154
  // Concurrency guard
145
155
  if (accountBusy.has(accountId)) {
146
156
  api.logger.info(`[clawnet] ${accountId}: LLM run in progress, requeueing ${messages.length} message(s)`);
@@ -363,19 +373,19 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
363
373
  if (checkData.plugin_config) {
364
374
  const pc = checkData.plugin_config;
365
375
  let changed = false;
366
- if (pc.poll_seconds !== cfg.pollEverySeconds) {
376
+ if (pc.poll_seconds !== undefined && pc.poll_seconds !== cfg.pollEverySeconds) {
367
377
  cfg.pollEverySeconds = pc.poll_seconds;
368
378
  changed = true;
369
379
  }
370
- if (pc.debounce_seconds !== cfg.debounceSeconds) {
380
+ if (pc.debounce_seconds !== undefined && pc.debounce_seconds !== cfg.debounceSeconds) {
371
381
  cfg.debounceSeconds = pc.debounce_seconds;
372
382
  changed = true;
373
383
  }
374
- if (pc.max_batch_size !== cfg.maxBatchSize) {
384
+ if (pc.max_batch_size !== undefined && pc.max_batch_size !== cfg.maxBatchSize) {
375
385
  cfg.maxBatchSize = pc.max_batch_size;
376
386
  changed = true;
377
387
  }
378
- if (pc.deliver_channel !== cfg.deliver.channel) {
388
+ if (pc.deliver_channel !== undefined && pc.deliver_channel !== cfg.deliver.channel) {
379
389
  cfg.deliver.channel = pc.deliver_channel;
380
390
  changed = true;
381
391
  }
@@ -397,8 +407,9 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
397
407
  const notifyCount = checkData.notify_count ?? (checkData.count + a2aDmCount + sentTaskUpdates);
398
408
 
399
409
  if (checkData.count === 0) {
400
- // Email inbox clear — release any delivery lock (agent finished processing)
401
- deliveryLock.delete(account.id);
410
+ // Email inbox clear — only release delivery lock if nothing else needs notification,
411
+ // otherwise tasks/sent-task-updates still need the lock for throttling.
412
+ if (notifyCount === 0) deliveryLock.delete(account.id);
402
413
  return { a2aDmCount, sentTaskUpdates, notifyCount };
403
414
  }
404
415
 
@@ -418,8 +429,8 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
418
429
  state.lastInboxNonEmptyAt = new Date();
419
430
  api.logger.info(`[clawnet] ${account.id}: ${checkData.count} message(s) waiting (${notifyCount} to notify)`);
420
431
 
421
- // Fetch full messages
422
- const inboxRes = await fetch(`${cfg.baseUrl}/inbox`, { headers });
432
+ // Fetch full messages — request max limit so mark-notified covers everything
433
+ const inboxRes = await fetch(`${cfg.baseUrl}/inbox?limit=200`, { headers });
423
434
  if (!inboxRes.ok) {
424
435
  throw new Error(`/inbox returned ${inboxRes.status}`);
425
436
  }
@@ -439,6 +450,22 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
439
450
 
440
451
  state.counters.messagesSeen += normalized.length;
441
452
 
453
+ // Mark ALL fetched emails as notified up front — the delivery batch is capped
454
+ // at maxBatchSize, but the nag timer should reset for every message the agent
455
+ // is being alerted about, not just the ones in the batch.
456
+ const allEmailIds = normalized.map((m) => m.id);
457
+ if (allEmailIds.length > 0) {
458
+ try {
459
+ await fetch(`${cfg.baseUrl}/inbox/mark-notified`, {
460
+ method: "POST",
461
+ headers,
462
+ body: JSON.stringify({ message_ids: allEmailIds }),
463
+ });
464
+ } catch {
465
+ // Non-fatal — deliverBatch will still mark its batch after delivery
466
+ }
467
+ }
468
+
442
469
  // Add to pending (dedup by ID) and schedule debounced flush
443
470
  const existing = pendingMessages.get(account.id) ?? [];
444
471
  const existingIds = new Set(existing.map((m) => m.id));
@@ -467,7 +494,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
467
494
  jsonrpc: "2.0",
468
495
  id: `poll-${Date.now()}`,
469
496
  method: "tasks/list",
470
- params: { status: "submitted", limit: 50 },
497
+ params: { status: "submitted", limit: 100 },
471
498
  };
472
499
  const res = await fetch(`${cfg.baseUrl}/a2a`, {
473
500
  method: "POST",
@@ -505,6 +532,24 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
505
532
  });
506
533
 
507
534
  state.counters.messagesSeen += messages.length;
535
+
536
+ // Mark all fetched tasks as notified up front (same rationale as emails)
537
+ const allTaskIds = messages.map((m) => m.id);
538
+ if (allTaskIds.length > 0) {
539
+ try {
540
+ await fetch(`${cfg.baseUrl}/inbox/mark-notified`, {
541
+ method: "POST",
542
+ headers: {
543
+ Authorization: `Bearer ${resolvedToken}`,
544
+ "Content-Type": "application/json",
545
+ },
546
+ body: JSON.stringify({ task_ids: allTaskIds }),
547
+ });
548
+ } catch {
549
+ // Non-fatal
550
+ }
551
+ }
552
+
508
553
  const existing = pendingMessages.get(account.id) ?? [];
509
554
  const existingIds = new Set(existing.map((m) => m.id));
510
555
  const freshTasks = messages.filter((m) => !existingIds.has(m.id));
@@ -525,7 +570,7 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
525
570
  jsonrpc: "2.0",
526
571
  id: `sent-poll-${Date.now()}`,
527
572
  method: "tasks/list",
528
- params: { role: "sender", status: "input-required,completed,failed", limit: 50 },
573
+ params: { role: "sender", status: "input-required,completed,failed", limit: 100 },
529
574
  };
530
575
  const res = await fetch(`${cfg.baseUrl}/a2a`, {
531
576
  method: "POST",
@@ -560,6 +605,24 @@ export function createClawnetService(params: { api: any; cfg: ClawnetConfig }) {
560
605
  });
561
606
 
562
607
  state.counters.messagesSeen += messages.length;
608
+
609
+ // Mark all fetched sent-task updates as notified up front (same rationale as emails)
610
+ const allSentTaskIds = messages.map((m) => m.id);
611
+ if (allSentTaskIds.length > 0) {
612
+ try {
613
+ await fetch(`${cfg.baseUrl}/inbox/mark-notified`, {
614
+ method: "POST",
615
+ headers: {
616
+ Authorization: `Bearer ${resolvedToken}`,
617
+ "Content-Type": "application/json",
618
+ },
619
+ body: JSON.stringify({ task_ids: allSentTaskIds }),
620
+ });
621
+ } catch {
622
+ // Non-fatal
623
+ }
624
+ }
625
+
563
626
  const existing = pendingMessages.get(account.id) ?? [];
564
627
  const existingIds = new Set(existing.map((m) => m.id));
565
628
  const freshUpdates = messages.filter((m) => !existingIds.has(m.id));
package/src/tools.ts CHANGED
@@ -468,6 +468,24 @@ export function registerTools(api: any) {
468
468
  },
469
469
  }), { optional: true });
470
470
 
471
+ api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
472
+ name: "clawnet_email_bulk_status",
473
+ description: toolDesc("clawnet_email_bulk_status", "Set the status of multiple emails at once. Use to archive, read, or snooze many emails in one call instead of one at a time."),
474
+ parameters: {
475
+ type: "object",
476
+ properties: {
477
+ message_ids: { type: "array", items: { type: "string" }, description: "Array of message IDs (e.g. ['msg_abc123', 'msg_def456'])" },
478
+ status: { type: "string", enum: ["archived", "read", "snoozed", "new"], description: "New status to apply to all messages" },
479
+ },
480
+ required: ["message_ids", "status"],
481
+ },
482
+ async execute(_id: string, params: { message_ids: string[]; status: string }) {
483
+ const cfg = loadFreshConfig(api);
484
+ const result = await apiCall(cfg, "PATCH", `/messages/bulk/status`, { message_ids: params.message_ids, status: params.status }, ctx?.agentId, ctx?.sessionKey);
485
+ return textResult(result.data);
486
+ },
487
+ }), { optional: true });
488
+
471
489
  api.registerTool((ctx: { agentId?: string; sessionKey?: string }) => ({
472
490
  name: "clawnet_inbox_session",
473
491
  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."),