@holon-run/agentinbox 1.3.0 → 1.3.1

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.
@@ -17,6 +17,8 @@ const DEFAULT_ACTIVATION_MAX_ITEMS = 20;
17
17
  const DEFAULT_NOTIFY_LEASE_MS = 10 * 60 * 1000;
18
18
  const DEFAULT_NOTIFY_RETRY_MS = 5_000;
19
19
  const MAX_TERMINAL_DEFER_RETRY_MS = 5 * 60 * 1000;
20
+ const MAX_WEBHOOK_RETRY_MS = 5 * 60 * 1000;
21
+ const MAX_WEBHOOK_CONSECUTIVE_FAILURES = 12;
20
22
  const DEFAULT_ACKED_RETENTION_MS = 24 * 60 * 60 * 1000;
21
23
  const DEFAULT_OFFLINE_AGENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
22
24
  const DEFAULT_GC_INTERVAL_MS = 60 * 1000;
@@ -2097,6 +2099,10 @@ class AgentInboxService {
2097
2099
  : uniqueSorted(entries.map((entry) => entry.sourceId)),
2098
2100
  entries: unackedItems,
2099
2101
  });
2102
+ // Permanent errors and max-retries-exceeded signal "offline":
2103
+ if (dispatched === "offline") {
2104
+ this.store.deleteActivationDispatchState(buffer.agentId, buffer.targetId);
2105
+ }
2100
2106
  if (dispatched === "retryable_failure") {
2101
2107
  if (state && state.status === "dirty" && state.leaseExpiresAt == null) {
2102
2108
  this.store.upsertActivationDispatchState({
@@ -2276,10 +2282,17 @@ class AgentInboxService {
2276
2282
  return;
2277
2283
  }
2278
2284
  if (dispatched === "retryable_failure") {
2285
+ let backoffMs = DEFAULT_NOTIFY_RETRY_MS;
2286
+ if (target.kind === "webhook") {
2287
+ const updatedTarget = this.getActivationTargetIfExists(agentId, targetId);
2288
+ if (updatedTarget) {
2289
+ backoffMs = webhookDeferRetryMs(updatedTarget.consecutiveFailures);
2290
+ }
2291
+ }
2279
2292
  this.store.upsertActivationDispatchState({
2280
2293
  ...state,
2281
2294
  status: "dirty",
2282
- leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
2295
+ leaseExpiresAt: new Date(Date.now() + backoffMs).toISOString(),
2283
2296
  deferReason: null,
2284
2297
  deferAttempts: 0,
2285
2298
  firstDeferredAt: null,
@@ -2548,6 +2561,35 @@ class AgentInboxService {
2548
2561
  return "offline";
2549
2562
  }
2550
2563
  }
2564
+ if (target.kind === "webhook") {
2565
+ const statusCode = extractHttpStatus(error);
2566
+ // 404 / 410: endpoint gone
2567
+ // 409 / 422: expired or invalid webhook config
2568
+ if (statusCode === 404 || statusCode === 410 || statusCode === 409 || statusCode === 422) {
2569
+ this.logger.info("activation.webhook_permanent_error", {
2570
+ agentId: input.agentId,
2571
+ targetId: target.targetId,
2572
+ statusCode,
2573
+ message,
2574
+ });
2575
+ this.markActivationTargetOffline(target.targetId, message);
2576
+ this.reconcileAgentStatus(target.agentId);
2577
+ return "offline";
2578
+ }
2579
+ // Max consecutive transient failures exceeded
2580
+ if (target.consecutiveFailures + 1 >= MAX_WEBHOOK_CONSECUTIVE_FAILURES) {
2581
+ this.logger.info("activation.webhook_max_retries_exceeded", {
2582
+ agentId: input.agentId,
2583
+ targetId: target.targetId,
2584
+ consecutiveFailures: target.consecutiveFailures + 1,
2585
+ maxRetries: MAX_WEBHOOK_CONSECUTIVE_FAILURES,
2586
+ });
2587
+ this.markActivationTargetOffline(target.targetId, message);
2588
+ this.reconcileAgentStatus(target.agentId);
2589
+ return "offline";
2590
+ }
2591
+ // Transient error: fall through to markActivationTargetDispatchFailure
2592
+ }
2551
2593
  this.markActivationTargetDispatchFailure(target.targetId, message);
2552
2594
  return "retryable_failure";
2553
2595
  }
@@ -3374,7 +3416,9 @@ class ActivationDispatcher {
3374
3416
  body: JSON.stringify(activation),
3375
3417
  });
3376
3418
  if (!response.ok) {
3377
- throw new Error(`activation dispatch failed for ${targetUrl}: ${response.status}`);
3419
+ const error = new Error(`activation dispatch failed for ${targetUrl}: ${response.status}`);
3420
+ error.statusCode = response.status;
3421
+ throw error;
3378
3422
  }
3379
3423
  }
3380
3424
  catch (error) {
@@ -3593,6 +3637,19 @@ function terminalDeferRetryMs(attempts) {
3593
3637
  const multiplier = 2 ** (normalizedAttempts - 1);
3594
3638
  return Math.min(DEFAULT_NOTIFY_RETRY_MS * multiplier, MAX_TERMINAL_DEFER_RETRY_MS);
3595
3639
  }
3640
+ function webhookDeferRetryMs(consecutiveFailures) {
3641
+ const normalized = Math.max(1, consecutiveFailures);
3642
+ const multiplier = 2 ** (normalized - 1);
3643
+ return Math.min(DEFAULT_NOTIFY_RETRY_MS * multiplier, MAX_WEBHOOK_RETRY_MS);
3644
+ }
3645
+ function extractHttpStatus(error) {
3646
+ if (error instanceof Error) {
3647
+ const code = error.statusCode;
3648
+ if (typeof code === "number")
3649
+ return code;
3650
+ }
3651
+ return null;
3652
+ }
3596
3653
  function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
3597
3654
  if (sourceType === "github_repo_ci") {
3598
3655
  const parts = eventVariant.split(".");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holon-run/agentinbox",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Local event subscription and delivery service for agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -193,6 +193,43 @@ agentinbox source invoke <sourceId> --operation get_message_context --input-json
193
193
  agentinbox deliver invoke --handle-json '<deliveryHandle-json>' --operation send_text --input-json '{"text":"Acknowledged","uxcAuth":"feishu-default"}'
194
194
  ```
195
195
 
196
+ Feishu messages may carry files, images, or cloud document links. List the
197
+ attachments first and then save them locally:
198
+
199
+ ```bash
200
+ # List attachments for a message
201
+ agentinbox source invoke <sourceId> \
202
+ --operation list_message_attachments \
203
+ --input-json '{"messageId":"<messageId>"}'
204
+
205
+ # Save one attachment by messageId + attachmentId
206
+ agentinbox source invoke <sourceId> \
207
+ --operation save_attachment \
208
+ --input-json '{"messageId":"<messageId>","attachmentId":"<attachmentId>","outputDir":"/tmp/agentinbox-feishu"}'
209
+
210
+ # Save with explicit output path and identity
211
+ agentinbox source invoke <sourceId> \
212
+ --operation save_attachment \
213
+ --input-json '{"messageId":"<messageId>","attachmentId":"<attachmentId>","outputPath":"/tmp/report.pdf","identity":"bot"}'
214
+ ```
215
+
216
+ Output formats by attachment kind:
217
+
218
+ | Kind | Default | `format` options |
219
+ |---|---|---|
220
+ | `image` / `file` | original binary | `"original"` only |
221
+ | `doc` / `docx` / `wiki` | `.md` (Markdown) | N/A |
222
+ | `sheet` | `.xlsx` | `"csv"` (requires `subId`) |
223
+ | `bitable` / `base` | `.base` | `"csv"` (requires `subId`) |
224
+ | `drive_file` | original file | `"original"` only |
225
+
226
+ Optional `save_attachment` parameters: `outputPath`, `outputDir`, `fileName`,
227
+ `format`, `subId`, `identity` (`"user"` or `"bot"`), `overwrite`.
228
+
229
+ Rich text and file replies are available through `send_post`, `send_file`,
230
+ and `send_file_from_path` delivery operations. Use `deliver actions` to
231
+ inspect available schemas.
232
+
196
233
  Advanced source/subscription commands:
197
234
 
198
235
  ```bash