@holon-run/agentinbox 1.2.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.
package/dist/src/cli.js CHANGED
@@ -533,6 +533,27 @@ async function main() {
533
533
  await printRemote(client, `/agents/${encodeURIComponent(agentId)}/targets/${encodeURIComponent(targetId)}/resume`, {});
534
534
  return;
535
535
  }
536
+ if (command === "agent" && normalized[1] === "target" && normalized[2] === "update") {
537
+ const [agentId, targetId] = normalized.slice(3, 5);
538
+ if (!agentId || !targetId) {
539
+ throw new Error("usage: agentinbox agent target update <agentId> <targetId> [--url URL] [--activation-mode MODE] [--notify-lease-ms N] [--min-unacked-items N]");
540
+ }
541
+ const body = {};
542
+ const url = takeFlagValue(normalized, "--url");
543
+ if (url)
544
+ body.url = url;
545
+ const activationMode = takeFlagValue(normalized, "--activation-mode");
546
+ if (activationMode)
547
+ body.activationMode = activationMode;
548
+ const notifyLeaseMs = takeFlagValue(normalized, "--notify-lease-ms");
549
+ if (notifyLeaseMs)
550
+ body.notifyLeaseMs = parseOptionalNumber(notifyLeaseMs);
551
+ const minUnackedItems = takeFlagValue(normalized, "--min-unacked-items");
552
+ if (minUnackedItems)
553
+ body.minUnackedItems = parseOptionalNumber(minUnackedItems);
554
+ await printRemote(client, `/agents/${encodeURIComponent(agentId)}/targets/${encodeURIComponent(targetId)}`, body, "PATCH");
555
+ return;
556
+ }
536
557
  if (command === "subscription" && normalized[1] === "add") {
537
558
  const args = normalized.slice(2);
538
559
  const positionals = positionalArgs(args, [
@@ -1154,11 +1175,17 @@ function clientArgsForCommand(args) {
1154
1175
  if (isAgentTargetAddWebhookCommand(args)) {
1155
1176
  return withoutFlagValues(args, "--url");
1156
1177
  }
1178
+ if (isAgentTargetUpdateCommand(args)) {
1179
+ return withoutFlagValues(args, "--url");
1180
+ }
1157
1181
  return args;
1158
1182
  }
1159
1183
  function isAgentTargetAddWebhookCommand(args) {
1160
1184
  return args[0] === "agent" && args[1] === "target" && args[2] === "add" && args[3] === "webhook";
1161
1185
  }
1186
+ function isAgentTargetUpdateCommand(args) {
1187
+ return args[0] === "agent" && args[1] === "target" && args[2] === "update";
1188
+ }
1162
1189
  function withoutFlagValues(args, flag) {
1163
1190
  const filtered = [];
1164
1191
  for (let index = 0; index < args.length; index += 1) {
@@ -1514,6 +1541,7 @@ Usage:
1514
1541
  agentinbox agent target list <agentId> [--limit N]
1515
1542
  agentinbox agent target remove <agentId> <targetId>
1516
1543
  agentinbox agent target resume <agentId> <targetId>
1544
+ agentinbox agent target update <agentId> <targetId> [--url URL] [--activation-mode MODE] [--notify-lease-ms N] [--min-unacked-items N]
1517
1545
  `,
1518
1546
  timer: `agentinbox timer
1519
1547
 
package/dist/src/http.js CHANGED
@@ -1061,6 +1061,45 @@ function buildFastifyServer(service) {
1061
1061
  minUnackedItems: body.minUnackedItems ?? null,
1062
1062
  });
1063
1063
  });
1064
+ app.patch("/agents/:agentId/targets/:targetId", {
1065
+ schema: {
1066
+ tags: ["agents"],
1067
+ params: {
1068
+ type: "object",
1069
+ required: ["agentId", "targetId"],
1070
+ properties: {
1071
+ agentId: { type: "string", minLength: 1 },
1072
+ targetId: { type: "string", minLength: 1 },
1073
+ },
1074
+ },
1075
+ body: {
1076
+ type: "object",
1077
+ additionalProperties: false,
1078
+ properties: {
1079
+ url: { type: "string" },
1080
+ activationMode: { type: "string" },
1081
+ notifyLeaseMs: { type: "integer", minimum: 1 },
1082
+ minUnackedItems: { type: "integer", minimum: 1 },
1083
+ },
1084
+ },
1085
+ response: {
1086
+ 200: jsonObjectSchema,
1087
+ 400: errorResponseSchema,
1088
+ },
1089
+ },
1090
+ }, async (request) => {
1091
+ const params = request.params;
1092
+ const body = request.body;
1093
+ if (!body.url && body.activationMode === undefined && body.notifyLeaseMs === undefined && body.minUnackedItems === undefined) {
1094
+ throw new Error("at least one of --url, --activation-mode, --notify-lease-ms, --min-unacked-items must be provided");
1095
+ }
1096
+ return service.updateWebhookActivationTarget(decodeURIComponent(params.agentId), decodeURIComponent(params.targetId), {
1097
+ url: body.url,
1098
+ activationMode: body.activationMode,
1099
+ notifyLeaseMs: body.notifyLeaseMs ?? null,
1100
+ minUnackedItems: body.minUnackedItems ?? null,
1101
+ });
1102
+ });
1064
1103
  app.delete("/agents/:agentId/targets/:targetId", {
1065
1104
  schema: {
1066
1105
  tags: ["agents"],
@@ -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;
@@ -774,6 +776,29 @@ class AgentInboxService {
774
776
  this.markAgentActive(agentId);
775
777
  return target;
776
778
  }
779
+ updateWebhookActivationTarget(agentId, targetId, input) {
780
+ this.getAgent(agentId);
781
+ const existing = this.getActivationTarget(targetId);
782
+ if (existing.agentId !== agentId) {
783
+ throw new Error(`activation target ${targetId} does not belong to agent ${agentId}`);
784
+ }
785
+ if (existing.kind !== "webhook") {
786
+ throw new Error(`activation target ${targetId} is not a webhook target`);
787
+ }
788
+ validateNotifyLeaseMs(input.notifyLeaseMs);
789
+ validateMinUnackedItems(input.minUnackedItems);
790
+ const mode = normalizeWebhookActivationMode(input.activationMode ?? existing.mode);
791
+ const now = (0, util_1.nowIso)();
792
+ const target = this.store.updateWebhookActivationTarget(targetId, {
793
+ url: input.url ?? existing.url,
794
+ mode,
795
+ notifyLeaseMs: input.notifyLeaseMs ?? existing.notifyLeaseMs,
796
+ minUnackedItems: input.minUnackedItems !== undefined ? input.minUnackedItems : existing.minUnackedItems ?? null,
797
+ updatedAt: now,
798
+ lastSeenAt: now,
799
+ });
800
+ return target;
801
+ }
777
802
  listActivationTargets(agentId, limit) {
778
803
  if (agentId) {
779
804
  this.getAgent(agentId);
@@ -2074,6 +2099,10 @@ class AgentInboxService {
2074
2099
  : uniqueSorted(entries.map((entry) => entry.sourceId)),
2075
2100
  entries: unackedItems,
2076
2101
  });
2102
+ // Permanent errors and max-retries-exceeded signal "offline":
2103
+ if (dispatched === "offline") {
2104
+ this.store.deleteActivationDispatchState(buffer.agentId, buffer.targetId);
2105
+ }
2077
2106
  if (dispatched === "retryable_failure") {
2078
2107
  if (state && state.status === "dirty" && state.leaseExpiresAt == null) {
2079
2108
  this.store.upsertActivationDispatchState({
@@ -2253,10 +2282,17 @@ class AgentInboxService {
2253
2282
  return;
2254
2283
  }
2255
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
+ }
2256
2292
  this.store.upsertActivationDispatchState({
2257
2293
  ...state,
2258
2294
  status: "dirty",
2259
- leaseExpiresAt: new Date(Date.now() + DEFAULT_NOTIFY_RETRY_MS).toISOString(),
2295
+ leaseExpiresAt: new Date(Date.now() + backoffMs).toISOString(),
2260
2296
  deferReason: null,
2261
2297
  deferAttempts: 0,
2262
2298
  firstDeferredAt: null,
@@ -2525,6 +2561,35 @@ class AgentInboxService {
2525
2561
  return "offline";
2526
2562
  }
2527
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
+ }
2528
2593
  this.markActivationTargetDispatchFailure(target.targetId, message);
2529
2594
  return "retryable_failure";
2530
2595
  }
@@ -3351,7 +3416,9 @@ class ActivationDispatcher {
3351
3416
  body: JSON.stringify(activation),
3352
3417
  });
3353
3418
  if (!response.ok) {
3354
- 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;
3355
3422
  }
3356
3423
  }
3357
3424
  catch (error) {
@@ -3570,6 +3637,19 @@ function terminalDeferRetryMs(attempts) {
3570
3637
  const multiplier = 2 ** (normalizedAttempts - 1);
3571
3638
  return Math.min(DEFAULT_NOTIFY_RETRY_MS * multiplier, MAX_TERMINAL_DEFER_RETRY_MS);
3572
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
+ }
3573
3653
  function summarizeSourceEvent(sourceType, sourceKey, eventVariant) {
3574
3654
  if (sourceType === "github_repo_ci") {
3575
3655
  const parts = eventVariant.split(".");
@@ -14,11 +14,9 @@ const node_child_process_1 = require("node:child_process");
14
14
  const node_crypto_1 = require("node:crypto");
15
15
  const node_fs_1 = require("node:fs");
16
16
  const node_path_1 = require("node:path");
17
- const node_os_1 = require("node:os");
18
17
  const node_util_1 = require("node:util");
19
18
  exports.FEISHU_OPENAPI_ENDPOINT = "https://open.feishu.cn/open-apis";
20
19
  exports.FEISHU_IM_SCHEMA_URL = "https://raw.githubusercontent.com/holon-run/uxc/main/skills/feishu-openapi-skill/references/feishu-im.openapi.json";
21
- const FEISHU_BOT_SCHEMA_FILE = (0, node_path_1.join)((0, node_os_1.tmpdir)(), "agentinbox-feishu-bot.openapi.json");
22
20
  exports.DEFAULT_FEISHU_EVENT_TYPES = ["im.message.receive_v1"];
23
21
  const DEFAULT_CONTEXT_BOUND_SECONDS = 7 * 24 * 60 * 60;
24
22
  const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
@@ -131,10 +129,8 @@ class FeishuUxcClient {
131
129
  operation: "get:/bot/v3/info",
132
130
  payload: {},
133
131
  options: {
134
- auth: input.auth,
135
- schema_url: ensureFeishuBotSchemaFile(),
136
- refresh_schema: true,
137
- no_cache: true,
132
+ auth: input.auth ?? undefined,
133
+ schema_url: exports.FEISHU_IM_SCHEMA_URL,
138
134
  },
139
135
  });
140
136
  const openId = findStringField(response.data, "open_id");
@@ -1873,50 +1869,3 @@ function asStringArray(value) {
1873
1869
  .map((item) => asString(item))
1874
1870
  .filter((item) => Boolean(item));
1875
1871
  }
1876
- function ensureFeishuBotSchemaFile() {
1877
- const body = JSON.stringify(FEISHU_BOT_OPENAPI_SCHEMA);
1878
- (0, node_fs_1.writeFileSync)(FEISHU_BOT_SCHEMA_FILE, body, "utf8");
1879
- return FEISHU_BOT_SCHEMA_FILE;
1880
- }
1881
- const FEISHU_BOT_OPENAPI_SCHEMA = {
1882
- openapi: "3.0.3",
1883
- info: {
1884
- title: "Feishu Bot Info",
1885
- version: "1.0.0",
1886
- },
1887
- servers: [
1888
- {
1889
- url: exports.FEISHU_OPENAPI_ENDPOINT,
1890
- },
1891
- ],
1892
- // Feishu's docs site does not currently expose this as a stable
1893
- // machine-readable OpenAPI document to UXC, so keep the tiny operation
1894
- // schema needed for bot self-identification close to the adapter.
1895
- paths: {
1896
- "/bot/v3/info": {
1897
- get: {
1898
- operationId: "get:/bot/v3/info",
1899
- summary: "Get bot information for the current app",
1900
- responses: {
1901
- "200": {
1902
- description: "OK",
1903
- },
1904
- },
1905
- security: [
1906
- {
1907
- bearerAuth: [],
1908
- },
1909
- ],
1910
- },
1911
- },
1912
- },
1913
- components: {
1914
- securitySchemes: {
1915
- bearerAuth: {
1916
- type: "http",
1917
- scheme: "bearer",
1918
- bearerFormat: "tenant_access_token",
1919
- },
1920
- },
1921
- },
1922
- };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holon-run/agentinbox",
3
- "version": "1.2.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