@holon-run/agentinbox 1.4.1 → 1.5.0

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.
@@ -39,7 +39,7 @@ class AdapterRegistry {
39
39
  localEventSource = new NoopSourceAdapter("local_event");
40
40
  remoteSource;
41
41
  defaultDelivery = new NoopDeliveryAdapter();
42
- feishuDelivery = new feishu_1.FeishuDeliveryAdapter();
42
+ feishuDelivery;
43
43
  githubDelivery = new github_1.GithubDeliveryAdapter();
44
44
  telegramDelivery;
45
45
  homeDir;
@@ -49,6 +49,7 @@ class AdapterRegistry {
49
49
  this.remoteModuleRegistry = options?.remoteModuleRegistry ?? new remote_modules_1.RemoteSourceModuleRegistry({
50
50
  githubCallClient: options?.githubCallClient,
51
51
  });
52
+ this.feishuDelivery = new feishu_1.FeishuDeliveryAdapter(options?.feishuClient);
52
53
  this.telegramDelivery = new telegram_1.TelegramDeliveryAdapter(options?.telegramClient);
53
54
  this.remoteSource = new remote_1.RemoteSourceRuntime(store, appendSourceEvent, {
54
55
  homeDir: this.homeDir,
package/dist/src/cli.js CHANGED
@@ -22,9 +22,10 @@ const util_1 = require("./util");
22
22
  async function main() {
23
23
  const rawArgs = process.argv.slice(2);
24
24
  const detectedArgs = normalizeHelpArgs(rawArgs);
25
- const normalized = shouldTreatJsonFlagAsNoop(detectedArgs)
25
+ const jsonStripped = shouldTreatJsonFlagAsNoop(detectedArgs)
26
26
  ? stripNoopJsonFlag(detectedArgs)
27
27
  : detectedArgs;
28
+ const normalized = normalizeAliases(jsonStripped);
28
29
  const shell = parseCommanderShell(normalized);
29
30
  const command = normalized[0];
30
31
  if (!command || command === "help" || command === "--help" || command === "-h") {
@@ -360,14 +361,23 @@ async function main() {
360
361
  if (command === "source" && normalized[1] === "invoke") {
361
362
  const sourceId = normalized[2];
362
363
  const operation = takeFlagValue(normalized, "--operation");
363
- const inputJson = takeFlagValue(normalized, "--input-json");
364
+ const inputJson = takeFlagValue(normalized, "--input-json") ?? takeFlagValue(normalized, "--input");
364
365
  if (!sourceId || !operation || !inputJson) {
365
366
  throw new Error("usage: agentinbox source invoke <sourceId> --operation NAME --input-json JSON");
366
367
  }
367
- await printRemote(client, `/sources/${encodeURIComponent(sourceId)}/invoke`, {
368
- operation,
369
- input: (0, util_1.parseJsonArg)(inputJson, "--input-json"),
370
- });
368
+ try {
369
+ await printRemote(client, `/sources/${encodeURIComponent(sourceId)}/invoke`, {
370
+ operation,
371
+ input: (0, util_1.parseJsonArg)(inputJson, "--input-json"),
372
+ });
373
+ }
374
+ catch (error) {
375
+ const suggestion = await suggestSourceOperation(client, sourceId, operation);
376
+ if (suggestion) {
377
+ throw new Error(`${error instanceof Error ? error.message : String(error)}\nDid you mean: --operation ${suggestion}?`);
378
+ }
379
+ throw error;
380
+ }
371
381
  return;
372
382
  }
373
383
  if (command === "source" && normalized[1] === "poll") {
@@ -731,9 +741,9 @@ async function main() {
731
741
  }
732
742
  if (command === "inbox" && normalized[1] === "read") {
733
743
  const args = normalized.slice(2);
734
- const allowedFlags = ["--agent-id", "--after-entry", "--include-acked", "--limit"];
744
+ const allowedFlags = ["--agent-id", "--after-entry", "--include-acked", "--limit", "--full"];
735
745
  if (positionalArgs(args, ["--agent-id", "--after-entry", "--limit"]).length > 0 || unexpectedFlags(args, allowedFlags).length > 0) {
736
- throw new Error("usage: agentinbox inbox read [--agent-id ID] [--after-entry ID] [--include-acked] [--limit N]");
746
+ throw new Error("usage: agentinbox inbox read [--agent-id ID] [--after-entry ID] [--include-acked] [--limit N] [--full]");
737
747
  }
738
748
  const selection = await selectAgentForCommand(client, {
739
749
  explicitAgentId: takeFlagValue(normalized, "--agent-id"),
@@ -745,7 +755,11 @@ async function main() {
745
755
  limit: parseOptionalLimitArg(normalized),
746
756
  });
747
757
  const response = await requestRemote(client, `/agents/${encodeURIComponent(selection.agentId)}/inbox/entries${query}`, undefined, "GET");
748
- console.log((0, util_1.jsonResponse)(withCommandMetadata(response.data, selection)));
758
+ const data = response.data;
759
+ if (!hasFlag(normalized, "--full") && Array.isArray(data.entries)) {
760
+ data.entries = data.entries.map(compactInboxEntry);
761
+ }
762
+ console.log((0, util_1.jsonResponse)(withCommandMetadata(data, selection)));
749
763
  return;
750
764
  }
751
765
  if (command === "inbox" && normalized[1] === "send") {
@@ -768,8 +782,9 @@ async function main() {
768
782
  if (command === "inbox" && normalized[1] === "watch") {
769
783
  const args = normalized.slice(2);
770
784
  if (positionalArgs(args, ["--agent-id", "--after-entry", "--heartbeat-ms"]).length > 0) {
771
- throw new Error("usage: agentinbox inbox watch [--agent-id ID] [--after-entry ID] [--include-acked] [--heartbeat-ms N]");
785
+ throw new Error("usage: agentinbox inbox watch [--agent-id ID] [--after-entry ID] [--include-acked] [--heartbeat-ms N] [--full]");
772
786
  }
787
+ const fullMode = hasFlag(normalized, "--full");
773
788
  const selection = await selectAgentForCommand(client, {
774
789
  explicitAgentId: takeFlagValue(normalized, "--agent-id"),
775
790
  autoRegister: true,
@@ -788,18 +803,22 @@ async function main() {
788
803
  if (event.event !== "items") {
789
804
  continue;
790
805
  }
791
- console.log((0, util_1.jsonResponse)(event));
806
+ const output = fullMode ? event : {
807
+ ...event,
808
+ entries: event.entries.map((e) => compactInboxEntry(e)),
809
+ };
810
+ console.log((0, util_1.jsonResponse)(output));
792
811
  }
793
812
  return;
794
813
  }
795
814
  if (command === "inbox" && normalized[1] === "ack") {
796
815
  const args = normalized.slice(2);
797
816
  const itemId = takeFlagValue(normalized, "--entry-id") ?? takeFlagValue(normalized, "--entry");
798
- const throughEntryId = takeFlagValue(normalized, "--through");
817
+ const throughEntryId = takeFlagValue(normalized, "--through") ?? takeFlagValue(normalized, "--through-entry-id");
799
818
  const ackAll = hasFlag(normalized, "--all");
800
819
  const modeCount = Number(Boolean(itemId)) + Number(Boolean(throughEntryId)) + Number(ackAll);
801
- if (positionalArgs(args, ["--agent-id", "--entry", "--entry-id", "--through"]).length > 0 || modeCount !== 1) {
802
- throw new Error("usage: agentinbox inbox ack [--agent-id ID] (--through <entryId> | --entry <entryId> | --entry-id <entryId> | --all)");
820
+ if (positionalArgs(args, ["--agent-id", "--entry", "--entry-id", "--through", "--through-entry-id"]).length > 0 || modeCount !== 1) {
821
+ throw new Error("usage: agentinbox inbox ack [--agent-id ID] (--through <entryId> | --through-entry-id <entryId> | --entry <entryId> | --entry-id <entryId> | --all)");
803
822
  }
804
823
  const selection = await selectAgentForCommand(client, {
805
824
  explicitAgentId: takeFlagValue(normalized, "--agent-id"),
@@ -893,11 +912,11 @@ async function main() {
893
912
  const targetRef = takeFlagValue(normalized, "--target");
894
913
  const handleJson = takeFlagValue(normalized, "--handle-json");
895
914
  const operation = takeFlagValue(normalized, "--operation");
896
- const inputJson = takeFlagValue(normalized, "--input-json");
915
+ const inputJson = takeFlagValue(normalized, "--input-json") ?? takeFlagValue(normalized, "--input");
897
916
  if (!operation || !inputJson || (!handleJson && (!provider || !surface || !targetRef))) {
898
917
  throw new Error("usage: agentinbox deliver invoke (--handle-json JSON | --provider PROVIDER --surface SURFACE --target TARGET) --operation NAME --input-json JSON [--source-id SOURCE_ID]");
899
918
  }
900
- await printRemote(client, "/deliveries/invoke", handleJson
919
+ const invokeBody = handleJson
901
920
  ? {
902
921
  sourceId: takeFlagValue(normalized, "--source-id") ?? undefined,
903
922
  deliveryHandle: (0, util_1.parseJsonArg)(handleJson),
@@ -913,7 +932,17 @@ async function main() {
913
932
  replyMode: takeFlagValue(normalized, "--reply-mode") ?? undefined,
914
933
  operation,
915
934
  input: (0, util_1.parseJsonArg)(inputJson, "--input-json"),
916
- });
935
+ };
936
+ try {
937
+ await printRemote(client, "/deliveries/invoke", invokeBody);
938
+ }
939
+ catch (error) {
940
+ const suggestion = await suggestDeliveryOperation(client, invokeBody);
941
+ if (suggestion) {
942
+ throw new Error(`${error instanceof Error ? error.message : String(error)}\nDid you mean: --operation ${suggestion}?`);
943
+ }
944
+ throw error;
945
+ }
917
946
  return;
918
947
  }
919
948
  if (command === "status") {
@@ -924,6 +953,10 @@ async function main() {
924
953
  await printRemote(client, "/gc", {});
925
954
  return;
926
955
  }
956
+ const suggestion = suggestUnknownCommand(normalized);
957
+ if (suggestion) {
958
+ throw new Error(`unknown command: ${normalized.join(" ")}\nDid you mean: ${suggestion}?`);
959
+ }
927
960
  throw new Error(`unknown command: ${normalized.join(" ")}`);
928
961
  }
929
962
  const COMMAND_SHELL_SPECS = [
@@ -1258,6 +1291,127 @@ function normalizeHelpArgs(args) {
1258
1291
  }
1259
1292
  return [args[1], "--help"];
1260
1293
  }
1294
+ function normalizeAliases(args) {
1295
+ // Top-level command aliases
1296
+ if (args[0] === "delivery") {
1297
+ return ["deliver", ...args.slice(1)];
1298
+ }
1299
+ // Top-level shortcuts -> inbox subcommands
1300
+ if (args[0] === "entries") {
1301
+ return ["inbox", "read", ...args.slice(1)];
1302
+ }
1303
+ if (args[0] === "ack") {
1304
+ return ["inbox", "ack", ...args.slice(1)];
1305
+ }
1306
+ // inbox subcommand aliases: get/entries/peek -> read
1307
+ if (args[0] === "inbox" && (args[1] === "get" || args[1] === "entries" || args[1] === "peek")) {
1308
+ return [args[0], "read", ...args.slice(2)];
1309
+ }
1310
+ return args;
1311
+ }
1312
+ function levenshtein(a, b) {
1313
+ const matrix = [];
1314
+ for (let i = 0; i <= b.length; i++)
1315
+ matrix[i] = [i];
1316
+ for (let j = 0; j <= a.length; j++)
1317
+ matrix[0][j] = j;
1318
+ for (let i = 1; i <= b.length; i++) {
1319
+ for (let j = 1; j <= a.length; j++) {
1320
+ const cost = a[j - 1] === b[i - 1] ? 0 : 1;
1321
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
1322
+ }
1323
+ }
1324
+ return matrix[b.length][a.length];
1325
+ }
1326
+ function closestMatch(input, candidates) {
1327
+ if (candidates.length === 0)
1328
+ return null;
1329
+ let best = null;
1330
+ let bestDistance = Infinity;
1331
+ for (const candidate of candidates) {
1332
+ const distance = levenshtein(input.toLowerCase(), candidate.toLowerCase());
1333
+ if (distance < bestDistance) {
1334
+ bestDistance = distance;
1335
+ best = candidate;
1336
+ }
1337
+ }
1338
+ // Only suggest if the distance is reasonable (at most half the input length)
1339
+ if (best && bestDistance <= Math.max(1, Math.floor(input.length / 2))) {
1340
+ return best;
1341
+ }
1342
+ return null;
1343
+ }
1344
+ const KNOWN_SUBCOMMANDS = {
1345
+ deliver: ["send", "actions", "invoke"],
1346
+ inbox: ["list", "show", "read", "send", "watch", "ack", "policy", "compact"],
1347
+ daemon: ["start", "stop", "status"],
1348
+ host: ["add", "list", "show", "schema"],
1349
+ stream: ["add", "list", "show", "update", "remove", "pause", "resume", "schema", "poll", "event"],
1350
+ source: ["add", "list", "show", "update", "remove", "pause", "resume", "schema", "actions", "invoke", "poll", "event"],
1351
+ agent: ["register", "list", "current", "show", "remove", "resume", "target"],
1352
+ subscription: ["add", "list", "show", "remove", "poll", "lag", "reset"],
1353
+ timer: ["add", "list", "pause", "resume", "remove"],
1354
+ };
1355
+ function suggestUnknownCommand(args) {
1356
+ const command = args[0];
1357
+ if (!command)
1358
+ return null;
1359
+ // Check top-level command suggestions
1360
+ const topCommands = COMMAND_SHELL_SPECS.map((s) => s.name);
1361
+ const topSuggestion = closestMatch(command, topCommands);
1362
+ if (topSuggestion && topSuggestion !== command) {
1363
+ return topSuggestion;
1364
+ }
1365
+ // Check subcommand suggestions
1366
+ if (args[1]) {
1367
+ const subcommands = KNOWN_SUBCOMMANDS[command];
1368
+ if (subcommands) {
1369
+ const subSuggestion = closestMatch(args[1], subcommands);
1370
+ if (subSuggestion) {
1371
+ return `${command} ${subSuggestion}`;
1372
+ }
1373
+ }
1374
+ }
1375
+ return null;
1376
+ }
1377
+ async function suggestDeliveryOperation(client, invokeBody) {
1378
+ try {
1379
+ const actionsBody = {};
1380
+ if (invokeBody.deliveryHandle) {
1381
+ actionsBody.deliveryHandle = invokeBody.deliveryHandle;
1382
+ }
1383
+ else {
1384
+ if (invokeBody.sourceId)
1385
+ actionsBody.sourceId = invokeBody.sourceId;
1386
+ if (invokeBody.provider)
1387
+ actionsBody.provider = invokeBody.provider;
1388
+ if (invokeBody.surface)
1389
+ actionsBody.surface = invokeBody.surface;
1390
+ if (invokeBody.targetRef)
1391
+ actionsBody.targetRef = invokeBody.targetRef;
1392
+ if (invokeBody.threadRef)
1393
+ actionsBody.threadRef = invokeBody.threadRef;
1394
+ if (invokeBody.replyMode)
1395
+ actionsBody.replyMode = invokeBody.replyMode;
1396
+ }
1397
+ const response = await requestRemote(client, "/deliveries/actions", actionsBody, "POST");
1398
+ const operationNames = response.data.operations.map((op) => op.name);
1399
+ return closestMatch(String(invokeBody.operation), operationNames);
1400
+ }
1401
+ catch {
1402
+ return null;
1403
+ }
1404
+ }
1405
+ async function suggestSourceOperation(client, sourceId, operation) {
1406
+ try {
1407
+ const response = await requestRemote(client, `/sources/${encodeURIComponent(sourceId)}/actions`, undefined, "GET");
1408
+ const operationNames = response.data.operations.map((op) => op.name);
1409
+ return closestMatch(operation, operationNames);
1410
+ }
1411
+ catch {
1412
+ return null;
1413
+ }
1414
+ }
1261
1415
  function stripNoopJsonFlag(args) {
1262
1416
  return args.filter((arg) => arg !== "--json");
1263
1417
  }
@@ -1380,6 +1534,34 @@ function withCommandMetadata(data, selection) {
1380
1534
  ...(selection.warnings.length > 0 ? { warnings: selection.warnings } : {}),
1381
1535
  };
1382
1536
  }
1537
+ const COMPACT_ENTRY_FIELDS = [
1538
+ "entryId", "kind", "summary", "eventVariant",
1539
+ "occurredAt", "ackedAt", "deliveryHandle", "metadata",
1540
+ ];
1541
+ const COMPACT_DIGEST_FIELDS = [
1542
+ "threadId", "revision", "groupKey", "resourceRef", "eventFamily",
1543
+ ];
1544
+ /**
1545
+ * Strip verbose/internal fields from an inbox entry, keeping only the fields
1546
+ * an agent needs for decisions and ack. Use `--full` on `inbox read`/`watch`
1547
+ * to bypass this and see the complete entry.
1548
+ */
1549
+ function compactInboxEntry(entry) {
1550
+ const compact = {};
1551
+ for (const field of COMPACT_ENTRY_FIELDS) {
1552
+ if (entry[field] !== undefined) {
1553
+ compact[field] = entry[field];
1554
+ }
1555
+ }
1556
+ if (entry.kind === "digest_snapshot") {
1557
+ for (const field of COMPACT_DIGEST_FIELDS) {
1558
+ if (entry[field] !== undefined) {
1559
+ compact[field] = entry[field];
1560
+ }
1561
+ }
1562
+ }
1563
+ return compact;
1564
+ }
1383
1565
  function normalizeSubscriptionAddOutput(data, shortcutUsed) {
1384
1566
  if (shortcutUsed) {
1385
1567
  return data;
@@ -1568,20 +1750,24 @@ Usage:
1568
1750
  Usage:
1569
1751
  agentinbox inbox list [--limit N]
1570
1752
  agentinbox inbox show <agentId>
1571
- agentinbox inbox read [--agent-id ID] [--after-entry ID] [--include-acked] [--limit N]
1753
+ agentinbox inbox read [--agent-id ID] [--after-entry ID] [--include-acked] [--limit N] [--full] (aliases: inbox get, inbox entries, inbox peek)
1572
1754
  agentinbox inbox send --agent-id ID --message TEXT [--sender SENDER]
1573
- agentinbox inbox watch [--agent-id ID] [--after-entry ID] [--include-acked] [--heartbeat-ms N]
1574
- agentinbox inbox ack [--agent-id ID] (--through <entryId> | --entry <entryId> | --entry-id <entryId> | --all)
1755
+ agentinbox inbox watch [--agent-id ID] [--after-entry ID] [--include-acked] [--heartbeat-ms N] [--full]
1756
+ agentinbox inbox ack [--agent-id ID] (--through <entryId> | --through-entry-id <entryId> | --entry <entryId> | --entry-id <entryId> | --all)
1575
1757
  agentinbox inbox policy show [--agent-id ID]
1576
1758
  agentinbox inbox policy set [--agent-id ID] [--enabled|--disabled] [--window-ms N] [--max-items N] [--max-thread-age-ms N]
1577
1759
  agentinbox inbox compact <agentId>
1760
+
1761
+ Aliases: inbox get/entries/peek -> inbox read, top-level entries -> inbox read, top-level ack -> inbox ack
1578
1762
  `,
1579
1763
  deliver: `agentinbox deliver
1580
1764
 
1581
1765
  Usage:
1582
1766
  agentinbox deliver send --provider PROVIDER --surface SURFACE --target TARGET [--source-id SOURCE_ID] [--kind KIND] [--payload-json JSON]
1583
1767
  agentinbox deliver actions (--handle-json JSON | --provider PROVIDER --surface SURFACE --target TARGET) [--source-id SOURCE_ID]
1584
- agentinbox deliver invoke (--handle-json JSON | --provider PROVIDER --surface SURFACE --target TARGET) --operation NAME --input-json JSON [--source-id SOURCE_ID]
1768
+ agentinbox deliver invoke (--handle-json JSON | --provider PROVIDER --surface SURFACE --target TARGET) --operation NAME --input-json JSON [--source-id SOURCE_ID] (--input alias for --input-json)
1769
+
1770
+ Aliases: delivery -> deliver
1585
1771
  `,
1586
1772
  status: `agentinbox status
1587
1773
 
package/dist/src/http.js CHANGED
@@ -75,6 +75,17 @@ const deliveryTargetByFieldsSchema = {
75
75
  replyMode: { type: "string" },
76
76
  },
77
77
  };
78
+ const authDescriptorSchema = {
79
+ type: "object",
80
+ additionalProperties: false,
81
+ required: ["type"],
82
+ properties: {
83
+ type: { type: "string", enum: ["bearer", "header"] },
84
+ token: { type: "string" },
85
+ headerName: { type: "string" },
86
+ headerValue: { type: "string" },
87
+ },
88
+ };
78
89
  function buildFastifyServer(service) {
79
90
  const app = (0, fastify_1.default)({
80
91
  logger: false,
@@ -992,6 +1003,134 @@ function buildFastifyServer(service) {
992
1003
  const params = request.params;
993
1004
  return service.resumeAgent(decodeURIComponent(params.agentId));
994
1005
  });
1006
+ app.get("/agents/:agentId/operator-bindings", {
1007
+ schema: {
1008
+ tags: ["operators"],
1009
+ params: {
1010
+ type: "object",
1011
+ required: ["agentId"],
1012
+ properties: {
1013
+ agentId: { type: "string", minLength: 1 },
1014
+ },
1015
+ },
1016
+ response: {
1017
+ 200: {
1018
+ type: "object",
1019
+ required: ["bindings"],
1020
+ properties: {
1021
+ bindings: { type: "array", items: jsonObjectSchema },
1022
+ },
1023
+ },
1024
+ },
1025
+ },
1026
+ }, async (request) => {
1027
+ const params = request.params;
1028
+ return { bindings: service.listOperatorTransportBindings(decodeURIComponent(params.agentId)) };
1029
+ });
1030
+ app.post("/agents/:agentId/operator-bindings", {
1031
+ schema: {
1032
+ tags: ["operators"],
1033
+ params: {
1034
+ type: "object",
1035
+ required: ["agentId"],
1036
+ properties: {
1037
+ agentId: { type: "string", minLength: 1 },
1038
+ },
1039
+ },
1040
+ body: {
1041
+ type: "object",
1042
+ additionalProperties: false,
1043
+ required: ["transport", "operatorActorId", "holonBaseUrl"],
1044
+ properties: {
1045
+ bindingId: { type: "string" },
1046
+ transport: { type: "string", minLength: 1 },
1047
+ operatorActorId: { type: "string", minLength: 1 },
1048
+ holonBaseUrl: { type: "string", minLength: 1 },
1049
+ deliveryCallbackUrl: { type: "string" },
1050
+ deliveryAuth: authDescriptorSchema,
1051
+ holonAuth: authDescriptorSchema,
1052
+ defaultRouteId: { type: "string" },
1053
+ capabilities: { type: "array", items: { type: "string" } },
1054
+ provider: { type: "string" },
1055
+ metadata: jsonObjectSchema,
1056
+ syncWithHolon: { type: "boolean" },
1057
+ },
1058
+ },
1059
+ response: {
1060
+ 200: jsonObjectSchema,
1061
+ 400: errorResponseSchema,
1062
+ },
1063
+ },
1064
+ }, async (request) => {
1065
+ const params = request.params;
1066
+ const body = request.body;
1067
+ return service.registerOperatorTransportBinding(decodeURIComponent(params.agentId), {
1068
+ bindingId: optionalString(body.bindingId) ?? null,
1069
+ transport: String(body.transport),
1070
+ operatorActorId: String(body.operatorActorId),
1071
+ holonBaseUrl: String(body.holonBaseUrl),
1072
+ deliveryCallbackUrl: optionalString(body.deliveryCallbackUrl) ?? null,
1073
+ deliveryAuth: body.deliveryAuth ? body.deliveryAuth : null,
1074
+ holonAuth: body.holonAuth ? body.holonAuth : null,
1075
+ defaultRouteId: optionalString(body.defaultRouteId) ?? null,
1076
+ capabilities: Array.isArray(body.capabilities) ? body.capabilities.map(String) : undefined,
1077
+ provider: optionalString(body.provider) ?? null,
1078
+ metadata: body.metadata && typeof body.metadata === "object" ? body.metadata : undefined,
1079
+ syncWithHolon: typeof body.syncWithHolon === "boolean" ? body.syncWithHolon : undefined,
1080
+ });
1081
+ });
1082
+ app.post("/agents/:agentId/operator-ingress", {
1083
+ schema: {
1084
+ tags: ["operators"],
1085
+ params: {
1086
+ type: "object",
1087
+ required: ["agentId"],
1088
+ properties: {
1089
+ agentId: { type: "string", minLength: 1 },
1090
+ },
1091
+ },
1092
+ body: {
1093
+ type: "object",
1094
+ additionalProperties: false,
1095
+ required: ["text", "bindingId"],
1096
+ properties: {
1097
+ text: { type: "string", minLength: 1 },
1098
+ bindingId: { type: "string", minLength: 1 },
1099
+ actorId: { type: "string" },
1100
+ replyRouteId: { type: "string" },
1101
+ provider: { type: "string" },
1102
+ upstreamProvider: { type: "string" },
1103
+ providerMessageRef: { type: "string" },
1104
+ correlationId: { type: "string" },
1105
+ causationId: { type: "string" },
1106
+ metadata: jsonObjectSchema,
1107
+ sourceItemId: { type: "string" },
1108
+ deliveryHandle: deliveryHandleSchema,
1109
+ },
1110
+ },
1111
+ response: {
1112
+ 200: jsonObjectSchema,
1113
+ 400: errorResponseSchema,
1114
+ },
1115
+ },
1116
+ }, async (request) => {
1117
+ const params = request.params;
1118
+ const body = request.body;
1119
+ return service.forwardOperatorIngress(decodeURIComponent(params.agentId), {
1120
+ text: String(body.text),
1121
+ bindingId: String(body.bindingId),
1122
+ actorId: optionalString(body.actorId) ?? null,
1123
+ replyRouteId: optionalString(body.replyRouteId) ?? null,
1124
+ provider: optionalString(body.provider) ?? null,
1125
+ upstreamProvider: optionalString(body.upstreamProvider) ?? null,
1126
+ providerMessageRef: optionalString(body.providerMessageRef) ?? null,
1127
+ correlationId: optionalString(body.correlationId) ?? null,
1128
+ causationId: optionalString(body.causationId) ?? null,
1129
+ metadata: body.metadata && typeof body.metadata === "object" ? body.metadata : undefined,
1130
+ sourceItemId: optionalString(body.sourceItemId) ?? null,
1131
+ deliveryHandle: body.deliveryHandle ? body.deliveryHandle : null,
1132
+ });
1133
+ });
995
1134
  app.get("/agents/:agentId/targets", {
996
1135
  schema: {
997
1136
  tags: ["agents"],
@@ -1812,6 +1951,52 @@ function buildFastifyServer(service) {
1812
1951
  },
1813
1952
  },
1814
1953
  }, async (request) => service.invokeDelivery(request.body));
1954
+ app.post("/delivery-routes/:routeId/messages", {
1955
+ schema: {
1956
+ tags: ["deliveries"],
1957
+ params: {
1958
+ type: "object",
1959
+ required: ["routeId"],
1960
+ properties: {
1961
+ routeId: { type: "string", minLength: 1 },
1962
+ },
1963
+ },
1964
+ body: {
1965
+ type: "object",
1966
+ additionalProperties: false,
1967
+ required: ["text"],
1968
+ properties: {
1969
+ text: { type: "string", minLength: 1 },
1970
+ kind: { type: "string" },
1971
+ targetAgentId: { type: "string" },
1972
+ target_agent_id: { type: "string" },
1973
+ delivery_intent_id: { type: "string" },
1974
+ binding_id: { type: "string" },
1975
+ route_id: { type: "string" },
1976
+ created_at: { type: "string" },
1977
+ correlation_id: { type: ["string", "null"] },
1978
+ causation_id: { type: ["string", "null"] },
1979
+ metadata: jsonObjectSchema,
1980
+ },
1981
+ },
1982
+ response: {
1983
+ 200: jsonObjectSchema,
1984
+ 400: errorResponseSchema,
1985
+ 404: errorResponseSchema,
1986
+ },
1987
+ },
1988
+ }, async (request) => {
1989
+ const params = request.params;
1990
+ const routeId = decodeURIComponent(params.routeId);
1991
+ assertDeliveryRouteAuthorized(service, routeId, request.headers.authorization);
1992
+ const body = request.body;
1993
+ return service.deliverOperatorReply(routeId, {
1994
+ text: String(body.text),
1995
+ kind: optionalString(body.kind) ?? null,
1996
+ targetAgentId: optionalString(body.targetAgentId) ?? optionalString(body.target_agent_id) ?? null,
1997
+ metadata: body.metadata && typeof body.metadata === "object" ? body.metadata : undefined,
1998
+ });
1999
+ });
1815
2000
  app.setNotFoundHandler((_request, reply) => {
1816
2001
  void reply.code(404).send({ error: "not found" });
1817
2002
  });
@@ -1825,6 +2010,14 @@ function buildFastifyServer(service) {
1825
2010
  void reply.code(404).send({ error: message });
1826
2011
  return;
1827
2012
  }
2013
+ if (message.startsWith("missing delivery route authorization")) {
2014
+ void reply.code(401).send({ error: message });
2015
+ return;
2016
+ }
2017
+ if (message.startsWith("invalid delivery route authorization")) {
2018
+ void reply.code(403).send({ error: message });
2019
+ return;
2020
+ }
1828
2021
  if (isBadRequestError(message)) {
1829
2022
  void reply.code(400).send({ error: message });
1830
2023
  return;
@@ -1872,6 +2065,23 @@ function optionalString(value) {
1872
2065
  const trimmed = value.trim();
1873
2066
  return trimmed.length > 0 ? trimmed : undefined;
1874
2067
  }
2068
+ function assertDeliveryRouteAuthorized(service, routeId, authorization) {
2069
+ const binding = service.getOperatorBindingForReplyRoute(routeId);
2070
+ const deliveryAuth = binding?.deliveryAuth;
2071
+ if (!deliveryAuth) {
2072
+ return;
2073
+ }
2074
+ if (deliveryAuth.type !== "bearer" || !deliveryAuth.token) {
2075
+ throw new Error("invalid delivery route authorization");
2076
+ }
2077
+ if (!authorization) {
2078
+ throw new Error("missing delivery route authorization");
2079
+ }
2080
+ const [scheme, token] = authorization.split(/\s+/, 2);
2081
+ if (scheme?.toLowerCase() !== "bearer" || token !== deliveryAuth.token) {
2082
+ throw new Error("invalid delivery route authorization");
2083
+ }
2084
+ }
1875
2085
  function parseOptionalPositiveInteger(value) {
1876
2086
  if (value === undefined) {
1877
2087
  return undefined;
@@ -1909,6 +2119,9 @@ function isBadRequestError(message) {
1909
2119
  message.startsWith("deliver send is not supported") ||
1910
2120
  message.startsWith("deliveryHandle requires") ||
1911
2121
  message.startsWith("delivery operations are not supported") ||
2122
+ message.startsWith("operator binding requires") ||
2123
+ message.startsWith("operator ingress requires") ||
2124
+ message.startsWith("operator reply route requires") ||
1912
2125
  message.startsWith("unsupported delivery surface") ||
1913
2126
  message.startsWith("unsupported delivery operation") ||
1914
2127
  message.startsWith("source operations are not supported") ||
@@ -23,7 +23,7 @@ const DEFAULT_ACKED_RETENTION_MS = 24 * 60 * 60 * 1000;
23
23
  const DEFAULT_OFFLINE_AGENT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
24
24
  const DEFAULT_GC_INTERVAL_MS = 60 * 1000;
25
25
  const DEFAULT_SYNC_INTERVAL_MS = 2_000;
26
- const DEFAULT_IDLE_SOURCE_GRACE_MS = 5 * 60 * 1000;
26
+ const DEFAULT_IDLE_SOURCE_GRACE_MS = 30 * 60 * 1000;
27
27
  const DEFAULT_INBOX_AGGREGATION_WINDOW_MS = 30 * 1000;
28
28
  const DEFAULT_INBOX_AGGREGATION_MAX_ITEMS = 20;
29
29
  const DEFAULT_INBOX_AGGREGATION_MAX_THREAD_AGE_MS = 24 * 60 * 60 * 1000;
@@ -404,6 +404,7 @@ class AgentInboxService {
404
404
  if (!templateSpec) {
405
405
  continue;
406
406
  }
407
+ validateFollowTemplateArgs(templateSpec.argsSchema ?? [], templateArgs, templateSpec.templateId);
407
408
  await this.adapters.sourceAdapterFor(source.sourceType).validateSource?.(source);
408
409
  return { source, templateSpec };
409
410
  }
@@ -1686,6 +1687,144 @@ class AgentInboxService {
1686
1687
  this.store.insertDelivery(storedAttempt);
1687
1688
  return { ...storedAttempt, note: result.note, operation: request.operation };
1688
1689
  }
1690
+ async registerOperatorTransportBinding(agentId, input) {
1691
+ if (!input.transport) {
1692
+ throw new Error("operator binding requires transport");
1693
+ }
1694
+ if (!input.operatorActorId) {
1695
+ throw new Error("operator binding requires operatorActorId");
1696
+ }
1697
+ if (!input.holonBaseUrl) {
1698
+ throw new Error("operator binding requires holonBaseUrl");
1699
+ }
1700
+ const now = (0, util_1.nowIso)();
1701
+ const current = input.bindingId ? this.store.getOperatorTransportBinding(input.bindingId) : null;
1702
+ const binding = {
1703
+ bindingId: input.bindingId ?? (0, util_1.generateCanonicalId)("opb"),
1704
+ agentId,
1705
+ transport: input.transport,
1706
+ operatorActorId: input.operatorActorId,
1707
+ holonBaseUrl: input.holonBaseUrl.replace(/\/+$/, ""),
1708
+ deliveryCallbackUrl: input.deliveryCallbackUrl ?? null,
1709
+ deliveryAuth: input.deliveryAuth ?? null,
1710
+ holonAuth: input.holonAuth ?? null,
1711
+ defaultRouteId: input.defaultRouteId ?? null,
1712
+ capabilities: input.capabilities ?? ["prompt"],
1713
+ provider: input.provider ?? null,
1714
+ metadata: input.metadata ?? {},
1715
+ createdAt: current?.createdAt ?? now,
1716
+ updatedAt: now,
1717
+ };
1718
+ const stored = this.store.upsertOperatorTransportBinding(binding);
1719
+ const holon = input.syncWithHolon === false ? undefined : await this.syncOperatorBindingToHolon(stored);
1720
+ return holon === undefined ? stored : { ...stored, holon };
1721
+ }
1722
+ listOperatorTransportBindings(agentId) {
1723
+ return this.store.listOperatorTransportBindings(agentId);
1724
+ }
1725
+ getOperatorBindingForReplyRoute(routeId) {
1726
+ const route = this.store.getOperatorReplyRoute(routeId);
1727
+ return route ? this.store.getOperatorTransportBinding(route.bindingId) : null;
1728
+ }
1729
+ async forwardOperatorIngress(agentId, input) {
1730
+ if (!input.text) {
1731
+ throw new Error("operator ingress requires text");
1732
+ }
1733
+ const binding = this.store.getOperatorTransportBinding(input.bindingId);
1734
+ if (!binding || binding.agentId !== agentId) {
1735
+ throw new Error(`unknown operator binding for agent ${agentId}: ${input.bindingId}`);
1736
+ }
1737
+ const replyRouteId = input.replyRouteId ?? binding.defaultRouteId ?? null;
1738
+ if (replyRouteId && input.deliveryHandle) {
1739
+ const now = (0, util_1.nowIso)();
1740
+ this.store.upsertOperatorReplyRoute({
1741
+ routeId: replyRouteId,
1742
+ bindingId: binding.bindingId,
1743
+ agentId,
1744
+ deliveryHandle: input.deliveryHandle,
1745
+ sourceId: typeof input.metadata?.sourceId === "string" ? input.metadata.sourceId : null,
1746
+ metadata: input.metadata ?? {},
1747
+ createdAt: now,
1748
+ updatedAt: now,
1749
+ });
1750
+ }
1751
+ const holon = await this.postHolonJson(binding, `/control/agents/${encodeURIComponent(agentId)}/operator-ingress`, {
1752
+ text: input.text,
1753
+ actor_id: input.actorId ?? binding.operatorActorId,
1754
+ binding_id: binding.bindingId,
1755
+ reply_route_id: replyRouteId,
1756
+ provider: input.provider ?? binding.provider ?? binding.transport,
1757
+ upstream_provider: input.upstreamProvider ?? binding.provider ?? binding.transport,
1758
+ provider_message_ref: input.providerMessageRef ?? null,
1759
+ correlation_id: input.correlationId ?? null,
1760
+ causation_id: input.causationId ?? null,
1761
+ metadata: {
1762
+ ...(input.metadata ?? {}),
1763
+ source_item_id: input.sourceItemId ?? undefined,
1764
+ },
1765
+ });
1766
+ return {
1767
+ accepted: true,
1768
+ bindingId: binding.bindingId,
1769
+ replyRouteId,
1770
+ holon,
1771
+ };
1772
+ }
1773
+ async deliverOperatorReply(routeId, input) {
1774
+ if (!input.text) {
1775
+ throw new Error("operator reply route requires text");
1776
+ }
1777
+ const route = this.store.getOperatorReplyRoute(routeId);
1778
+ if (!route) {
1779
+ throw new Error(`unknown operator reply route: ${routeId}`);
1780
+ }
1781
+ return this.sendDelivery({
1782
+ sourceId: route.sourceId ?? undefined,
1783
+ deliveryHandle: route.deliveryHandle,
1784
+ kind: input.kind ?? "reply",
1785
+ payload: {
1786
+ text: input.text,
1787
+ targetAgentId: input.targetAgentId ?? route.agentId,
1788
+ routeId,
1789
+ metadata: input.metadata ?? {},
1790
+ },
1791
+ });
1792
+ }
1793
+ async syncOperatorBindingToHolon(binding) {
1794
+ if (!binding.defaultRouteId) {
1795
+ throw new Error("operator binding sync requires defaultRouteId");
1796
+ }
1797
+ if (!binding.deliveryCallbackUrl) {
1798
+ throw new Error("operator binding sync requires deliveryCallbackUrl");
1799
+ }
1800
+ return this.postHolonJson(binding, `/control/agents/${encodeURIComponent(binding.agentId)}/operator-bindings`, omitNullish({
1801
+ binding_id: binding.bindingId,
1802
+ transport: binding.transport,
1803
+ operator_actor_id: binding.operatorActorId,
1804
+ default_route_id: binding.defaultRouteId,
1805
+ delivery_callback_url: binding.deliveryCallbackUrl,
1806
+ delivery_auth: holonOperatorDeliveryAuth(binding.deliveryAuth),
1807
+ capabilities: holonOperatorCapabilities(binding.capabilities),
1808
+ provider: binding.provider,
1809
+ metadata: binding.metadata,
1810
+ }));
1811
+ }
1812
+ async postHolonJson(binding, path, payload) {
1813
+ const response = await fetch(`${binding.holonBaseUrl}${path}`, {
1814
+ method: "POST",
1815
+ headers: {
1816
+ "content-type": "application/json",
1817
+ ...authHeaders(binding.holonAuth),
1818
+ },
1819
+ body: JSON.stringify(payload),
1820
+ });
1821
+ const text = await response.text();
1822
+ const body = text ? parseJsonResponse(text) : {};
1823
+ if (!response.ok) {
1824
+ throw new Error(`Holon operator route call failed: ${response.status} ${text}`);
1825
+ }
1826
+ return body;
1827
+ }
1689
1828
  async sendCanonicalDeliveryViaModule(source, handle, payload, attempt) {
1690
1829
  const operations = await this.adapters.listDeliveryOperations(source, handle);
1691
1830
  const canonicalOperation = operations?.find((operation) => operation.canonicalTextAlias);
@@ -2111,6 +2250,14 @@ class AgentInboxService {
2111
2250
  const pendingState = state?.status === "dirty" ? state : null;
2112
2251
  const inbox = this.ensureInboxForAgent(buffer.agentId);
2113
2252
  const unackedItems = this.store.listInboxEntries(inbox.inboxId, { includeAcked: false });
2253
+ if (unackedItems.length === 0) {
2254
+ // All buffered items were acked before the flush window expired.
2255
+ // Nothing to dispatch — clean up and skip.
2256
+ this.store.deleteActivationDispatchState(buffer.agentId, buffer.targetId);
2257
+ buffer.inFlight = false;
2258
+ this.notificationBuffers.delete(key);
2259
+ return;
2260
+ }
2114
2261
  const dispatched = await this.dispatchActivationTarget({
2115
2262
  agentId: buffer.agentId,
2116
2263
  targetId: buffer.targetId,
@@ -2928,6 +3075,24 @@ class AgentInboxService {
2928
3075
  await this.pollSubscription(subscription.subscriptionId);
2929
3076
  }
2930
3077
  catch (error) {
3078
+ // Self-heal: if the subscription was deleted between listing and polling
3079
+ // (race with lifecycle cleanup), skip silently.
3080
+ if (!this.store.getSubscription(subscription.subscriptionId)) {
3081
+ continue;
3082
+ }
3083
+ // If the source was deleted but the subscription remains (orphaned),
3084
+ // clean up the subscription to stop repeated errors on every cycle.
3085
+ if (!this.store.getSource(subscription.sourceId)) {
3086
+ try {
3087
+ await this.removeSubscription(subscription.subscriptionId);
3088
+ }
3089
+ catch {
3090
+ this.store.deleteSubscription(subscription.subscriptionId);
3091
+ this.clearSubscriptionRuntimeState(subscription);
3092
+ }
3093
+ console.warn(`subscription ${subscription.subscriptionId} orphaned (source ${subscription.sourceId} no longer exists); removed`);
3094
+ continue;
3095
+ }
2931
3096
  console.warn(`subscription poll failed for ${subscription.subscriptionId}:`, error);
2932
3097
  }
2933
3098
  }
@@ -3439,6 +3604,26 @@ function providerForSourceType(sourceType) {
3439
3604
  }
3440
3605
  return null;
3441
3606
  }
3607
+ function authHeaders(auth) {
3608
+ if (!auth) {
3609
+ return {};
3610
+ }
3611
+ if (auth.type === "bearer" && auth.token) {
3612
+ return { authorization: `Bearer ${auth.token}` };
3613
+ }
3614
+ if (auth.type === "header" && auth.headerName && auth.headerValue) {
3615
+ return { [auth.headerName]: auth.headerValue };
3616
+ }
3617
+ return {};
3618
+ }
3619
+ function parseJsonResponse(text) {
3620
+ try {
3621
+ return JSON.parse(text);
3622
+ }
3623
+ catch {
3624
+ return { text };
3625
+ }
3626
+ }
3442
3627
  class ActivationDispatcher {
3443
3628
  async dispatch(targetUrl, activation) {
3444
3629
  try {
@@ -3515,8 +3700,11 @@ function inboxAggregationPolicy(inbox) {
3515
3700
  };
3516
3701
  }
3517
3702
  function meetsNotificationThreshold(policy, totalUnackedCount) {
3703
+ if (totalUnackedCount === 0) {
3704
+ return false;
3705
+ }
3518
3706
  if (policy.minUnackedItems == null) {
3519
- return true;
3707
+ return totalUnackedCount > 0;
3520
3708
  }
3521
3709
  return totalUnackedCount >= policy.minUnackedItems;
3522
3710
  }
@@ -4045,6 +4233,26 @@ function deliverSendUnsupportedMessage(handle, operations) {
4045
4233
  const names = operations.map((item) => item.name).join(", ");
4046
4234
  return `deliver send is not supported for ${handle.provider}/${handle.surface}; use deliver invoke with one of: ${names}. ${deliveryActionsHelp(handle)}`;
4047
4235
  }
4236
+ function omitNullish(record) {
4237
+ return Object.fromEntries(Object.entries(record).filter(([, value]) => value !== null && value !== undefined));
4238
+ }
4239
+ function holonOperatorDeliveryAuth(auth) {
4240
+ if (!auth) {
4241
+ throw new Error("operator binding sync requires deliveryAuth");
4242
+ }
4243
+ if (auth.type === "bearer" && auth.token) {
4244
+ return { kind: "bearer", bearer_token: auth.token };
4245
+ }
4246
+ throw new Error("operator binding sync supports only bearer deliveryAuth with a token");
4247
+ }
4248
+ function holonOperatorCapabilities(capabilities) {
4249
+ const normalized = new Set(capabilities.map((capability) => capability.trim().toLowerCase()).filter(Boolean));
4250
+ return {
4251
+ text: normalized.size === 0 || normalized.has("prompt") || normalized.has("text") || normalized.has("send_prompt"),
4252
+ ...(normalized.has("markdown") ? { markdown: true } : {}),
4253
+ ...(normalized.has("attachments") || normalized.has("attachment") ? { attachments: true } : {}),
4254
+ };
4255
+ }
4048
4256
  function deliveryActionsHelp(handle) {
4049
4257
  return `Run \`agentinbox deliver actions --provider ${handle.provider} --surface ${handle.surface} --target ${handle.targetRef}\` to inspect supported operations and input schemas.`;
4050
4258
  }
@@ -177,6 +177,10 @@ class RemoteSourceRuntime {
177
177
  }
178
178
  const binding = managedBindingForSource(source);
179
179
  await this.client.sourceDelete(binding.namespace, binding.sourceKey);
180
+ // Clean up in-memory state to prevent stale references after DB deletion
181
+ this.errorCounts.delete(sourceId);
182
+ this.nextRetryAt.delete(sourceId);
183
+ this.inFlight.delete(sourceId);
180
184
  }
181
185
  status() {
182
186
  return {
package/dist/src/store.js CHANGED
@@ -1514,6 +1514,85 @@ class AgentInboxStore {
1514
1514
  const rows = this.getAll("select * from deliveries order by created_at desc");
1515
1515
  return rows.map((row) => this.mapDelivery(row));
1516
1516
  }
1517
+ upsertOperatorTransportBinding(binding) {
1518
+ this.run(`
1519
+ insert into operator_transport_bindings (
1520
+ binding_id, agent_id, transport, operator_actor_id, holon_base_url,
1521
+ delivery_callback_url, delivery_auth_json, holon_auth_json, default_route_id,
1522
+ capabilities_json, provider, metadata_json, created_at, updated_at
1523
+ ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1524
+ on conflict(binding_id) do update set
1525
+ agent_id = excluded.agent_id,
1526
+ transport = excluded.transport,
1527
+ operator_actor_id = excluded.operator_actor_id,
1528
+ holon_base_url = excluded.holon_base_url,
1529
+ delivery_callback_url = excluded.delivery_callback_url,
1530
+ delivery_auth_json = excluded.delivery_auth_json,
1531
+ holon_auth_json = excluded.holon_auth_json,
1532
+ default_route_id = excluded.default_route_id,
1533
+ capabilities_json = excluded.capabilities_json,
1534
+ provider = excluded.provider,
1535
+ metadata_json = excluded.metadata_json,
1536
+ updated_at = excluded.updated_at
1537
+ `, [
1538
+ binding.bindingId,
1539
+ binding.agentId,
1540
+ binding.transport,
1541
+ binding.operatorActorId,
1542
+ binding.holonBaseUrl,
1543
+ binding.deliveryCallbackUrl ?? null,
1544
+ binding.deliveryAuth ? JSON.stringify(binding.deliveryAuth) : null,
1545
+ binding.holonAuth ? JSON.stringify(binding.holonAuth) : null,
1546
+ binding.defaultRouteId ?? null,
1547
+ JSON.stringify(binding.capabilities),
1548
+ binding.provider ?? null,
1549
+ JSON.stringify(binding.metadata),
1550
+ binding.createdAt,
1551
+ binding.updatedAt,
1552
+ ]);
1553
+ this.persist();
1554
+ return this.getOperatorTransportBinding(binding.bindingId);
1555
+ }
1556
+ getOperatorTransportBinding(bindingId) {
1557
+ const row = this.getOne("select * from operator_transport_bindings where binding_id = ?", [bindingId]);
1558
+ return row ? this.mapOperatorTransportBinding(row) : null;
1559
+ }
1560
+ listOperatorTransportBindings(agentId) {
1561
+ const rows = agentId
1562
+ ? this.getAll("select * from operator_transport_bindings where agent_id = ? order by created_at asc", [agentId])
1563
+ : this.getAll("select * from operator_transport_bindings order by created_at asc");
1564
+ return rows.map((row) => this.mapOperatorTransportBinding(row));
1565
+ }
1566
+ upsertOperatorReplyRoute(route) {
1567
+ this.run(`
1568
+ insert into operator_reply_routes (
1569
+ route_id, binding_id, agent_id, source_id, delivery_handle_json,
1570
+ metadata_json, created_at, updated_at
1571
+ ) values (?, ?, ?, ?, ?, ?, ?, ?)
1572
+ on conflict(route_id) do update set
1573
+ binding_id = excluded.binding_id,
1574
+ agent_id = excluded.agent_id,
1575
+ source_id = excluded.source_id,
1576
+ delivery_handle_json = excluded.delivery_handle_json,
1577
+ metadata_json = excluded.metadata_json,
1578
+ updated_at = excluded.updated_at
1579
+ `, [
1580
+ route.routeId,
1581
+ route.bindingId,
1582
+ route.agentId,
1583
+ route.sourceId ?? null,
1584
+ JSON.stringify(route.deliveryHandle),
1585
+ JSON.stringify(route.metadata),
1586
+ route.createdAt,
1587
+ route.updatedAt,
1588
+ ]);
1589
+ this.persist();
1590
+ return this.getOperatorReplyRoute(route.routeId);
1591
+ }
1592
+ getOperatorReplyRoute(routeId) {
1593
+ const row = this.getOne("select * from operator_reply_routes where route_id = ?", [routeId]);
1594
+ return row ? this.mapOperatorReplyRoute(row) : null;
1595
+ }
1517
1596
  insertStream(stream) {
1518
1597
  this.run("insert into streams (stream_id, source_id, stream_key, backend, created_at) values (?, ?, ?, ?, ?)", [stream.streamId, stream.sourceId, stream.streamKey, stream.backend, stream.createdAt]);
1519
1598
  this.persist();
@@ -2030,6 +2109,36 @@ class AgentInboxStore {
2030
2109
  createdAt: String(row.created_at),
2031
2110
  };
2032
2111
  }
2112
+ mapOperatorTransportBinding(row) {
2113
+ return {
2114
+ bindingId: String(row.binding_id),
2115
+ agentId: String(row.agent_id),
2116
+ transport: String(row.transport),
2117
+ operatorActorId: String(row.operator_actor_id),
2118
+ holonBaseUrl: String(row.holon_base_url),
2119
+ deliveryCallbackUrl: row.delivery_callback_url ? String(row.delivery_callback_url) : null,
2120
+ deliveryAuth: row.delivery_auth_json ? parseJson(row.delivery_auth_json) : null,
2121
+ holonAuth: row.holon_auth_json ? parseJson(row.holon_auth_json) : null,
2122
+ defaultRouteId: row.default_route_id ? String(row.default_route_id) : null,
2123
+ capabilities: parseJson(row.capabilities_json),
2124
+ provider: row.provider ? String(row.provider) : null,
2125
+ metadata: parseJson(row.metadata_json),
2126
+ createdAt: String(row.created_at),
2127
+ updatedAt: String(row.updated_at),
2128
+ };
2129
+ }
2130
+ mapOperatorReplyRoute(row) {
2131
+ return {
2132
+ routeId: String(row.route_id),
2133
+ bindingId: String(row.binding_id),
2134
+ agentId: String(row.agent_id),
2135
+ sourceId: row.source_id ? String(row.source_id) : null,
2136
+ deliveryHandle: parseJson(row.delivery_handle_json),
2137
+ metadata: parseJson(row.metadata_json),
2138
+ createdAt: String(row.created_at),
2139
+ updatedAt: String(row.updated_at),
2140
+ };
2141
+ }
2033
2142
  resolveInboxEntryId(inboxId, refOrItemId) {
2034
2143
  try {
2035
2144
  return (0, util_1.parseEntryRef)(refOrItemId);
@@ -0,0 +1,29 @@
1
+ CREATE TABLE `operator_transport_bindings` (
2
+ `binding_id` text PRIMARY KEY NOT NULL,
3
+ `agent_id` text NOT NULL,
4
+ `transport` text NOT NULL,
5
+ `operator_actor_id` text NOT NULL,
6
+ `holon_base_url` text NOT NULL,
7
+ `delivery_callback_url` text,
8
+ `delivery_auth_json` text,
9
+ `holon_auth_json` text,
10
+ `default_route_id` text,
11
+ `capabilities_json` text NOT NULL,
12
+ `provider` text,
13
+ `metadata_json` text NOT NULL,
14
+ `created_at` text NOT NULL,
15
+ `updated_at` text NOT NULL
16
+ );
17
+ CREATE INDEX `idx_operator_bindings_agent_transport` ON `operator_transport_bindings` (`agent_id`,`transport`);
18
+
19
+ CREATE TABLE `operator_reply_routes` (
20
+ `route_id` text PRIMARY KEY NOT NULL,
21
+ `binding_id` text NOT NULL,
22
+ `agent_id` text NOT NULL,
23
+ `source_id` text,
24
+ `delivery_handle_json` text NOT NULL,
25
+ `metadata_json` text NOT NULL,
26
+ `created_at` text NOT NULL,
27
+ `updated_at` text NOT NULL
28
+ );
29
+ CREATE INDEX `idx_operator_reply_routes_binding` ON `operator_reply_routes` (`binding_id`);
package/drizzle/schema.ts CHANGED
@@ -282,6 +282,38 @@ export const deliveries = sqliteTable("deliveries", {
282
282
  createdAt: text("created_at").notNull(),
283
283
  });
284
284
 
285
+ export const operatorTransportBindings = sqliteTable("operator_transport_bindings", {
286
+ bindingId: text("binding_id").primaryKey(),
287
+ agentId: text("agent_id").notNull(),
288
+ transport: text("transport").notNull(),
289
+ operatorActorId: text("operator_actor_id").notNull(),
290
+ holonBaseUrl: text("holon_base_url").notNull(),
291
+ deliveryCallbackUrl: text("delivery_callback_url"),
292
+ deliveryAuthJson: text("delivery_auth_json"),
293
+ holonAuthJson: text("holon_auth_json"),
294
+ defaultRouteId: text("default_route_id"),
295
+ capabilitiesJson: text("capabilities_json").notNull(),
296
+ provider: text("provider"),
297
+ metadataJson: text("metadata_json").notNull(),
298
+ createdAt: text("created_at").notNull(),
299
+ updatedAt: text("updated_at").notNull(),
300
+ }, (table) => ({
301
+ agentTransportIdx: index("idx_operator_bindings_agent_transport").on(table.agentId, table.transport),
302
+ }));
303
+
304
+ export const operatorReplyRoutes = sqliteTable("operator_reply_routes", {
305
+ routeId: text("route_id").primaryKey(),
306
+ bindingId: text("binding_id").notNull(),
307
+ agentId: text("agent_id").notNull(),
308
+ sourceId: text("source_id"),
309
+ deliveryHandleJson: text("delivery_handle_json").notNull(),
310
+ metadataJson: text("metadata_json").notNull(),
311
+ createdAt: text("created_at").notNull(),
312
+ updatedAt: text("updated_at").notNull(),
313
+ }, (table) => ({
314
+ bindingIdx: index("idx_operator_reply_routes_binding").on(table.bindingId),
315
+ }));
316
+
285
317
  export const streams = sqliteTable("streams", {
286
318
  streamId: text("stream_id").primaryKey(),
287
319
  sourceId: text("source_id").notNull(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@holon-run/agentinbox",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Local event subscription and delivery service for agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {