@songsid/agend 2.1.4-beta.32 → 2.1.4-beta.33

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.
@@ -3932,8 +3932,6 @@ export class FleetManager {
3932
3932
  }
3933
3933
  /** Handle outbound tool calls from a daemon instance */
3934
3934
  async handleOutboundFromInstance(instanceName, msg) {
3935
- if (this.worlds.size === 0)
3936
- return;
3937
3935
  this.touchActivity(instanceName);
3938
3936
  this.setTopicIcon(instanceName, "green");
3939
3937
  const tool = msg.tool;
@@ -3943,13 +3941,21 @@ export class FleetManager {
3943
3941
  const senderSessionName = msg.senderSessionName;
3944
3942
  const respond = (result, error) => {
3945
3943
  const ipc = this.instanceIpcClients.get(instanceName);
3944
+ let sent = false;
3946
3945
  if (fleetRequestId) {
3947
- ipc?.send({ type: "fleet_outbound_response", fleetRequestId, result, error });
3946
+ sent = ipc?.send({ type: "fleet_outbound_response", fleetRequestId, result, error }) ?? false;
3948
3947
  }
3949
3948
  else {
3950
- ipc?.send({ type: "fleet_outbound_response", requestId, result, error });
3949
+ sent = ipc?.send({ type: "fleet_outbound_response", requestId, result, error }) ?? false;
3950
+ }
3951
+ if (!sent) {
3952
+ this.logger.warn({ instanceName, tool, requestId, fleetRequestId, error }, "Fleet outbound result could not be returned — instance IPC is disconnected");
3951
3953
  }
3952
3954
  };
3955
+ if (this.worlds.size === 0) {
3956
+ respond(null, "Channel adapters are not ready — retry shortly");
3957
+ return;
3958
+ }
3953
3959
  // Resolve threadId: use sender's topic_id if sender is a known fleet instance,
3954
3960
  // fall back to general topic if sender is unknown, or IPC owner if no sender.
3955
3961
  const senderInstanceName = senderSessionName && this.fleetConfig?.instances[senderSessionName]
@@ -3994,17 +4000,30 @@ export class FleetManager {
3994
4000
  if (tool === "reply") {
3995
4001
  const ticket = this.replyDeduper.begin(instanceName, String(args.text ?? ""), Array.isArray(args.files) ? args.files : []);
3996
4002
  if (ticket.duplicate) {
3997
- this.logger.info({ instanceName }, "Duplicate reply suppressed — replaying the original send's outcome");
4003
+ this.logger.info({ instanceName }, "Concurrent duplicate reply joined — awaiting the original send's outcome");
3998
4004
  ticket.subscribe(respond);
3999
4005
  return;
4000
4006
  }
4001
4007
  const original = respond;
4002
4008
  const respondAndRecord = (result, error) => {
4003
4009
  ticket.complete(result, error);
4010
+ // Return the platform outcome first. Bookkeeping below must never turn
4011
+ // a confirmed Discord/Telegram POST into a tool error if a secondary
4012
+ // log/button side effect happens to throw.
4004
4013
  original(result, error);
4014
+ // The adapter resolves only after the platform POST returns. Keep
4015
+ // outward-facing logs/cancel state on the same confirmation boundary:
4016
+ // a routed-but-failed reply is not a delivered reply.
4017
+ if (!error && result != null) {
4018
+ try {
4019
+ this.afterReplyRouted(instanceName, args, senderSessionName);
4020
+ }
4021
+ catch (err) {
4022
+ this.logger.warn({ err, instanceName }, "Reply delivered but post-delivery bookkeeping failed");
4023
+ }
4024
+ }
4005
4025
  };
4006
4026
  if (routeToolCall(outAdapter, tool, args, threadId, respondAndRecord)) {
4007
- this.afterReplyRouted(instanceName, args, senderSessionName);
4008
4027
  return;
4009
4028
  }
4010
4029
  // routeToolCall knows "reply"; not handling it means the world changed.