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

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/daemon.js CHANGED
@@ -688,6 +688,13 @@ export class Daemon extends EventEmitter {
688
688
  guardian = null;
689
689
  adapter = null;
690
690
  pendingIpcRequests = new Map();
691
+ /**
692
+ * Daemon-global request sequence. MCP request ids are only process-local:
693
+ * Claude can have more than one MCP child connected to the same daemon and
694
+ * each starts at 1, so using requestId alone lets one caller steal another's
695
+ * response.
696
+ */
697
+ fleetRequestSeq = 0;
691
698
  // Track chatId/threadId from inbound messages for automatic outbound routing
692
699
  lastChatId;
693
700
  lastThreadId;
@@ -3608,7 +3615,10 @@ export class Daemon extends EventEmitter {
3608
3615
  if (!error && result != null && TURN_REPLY_TOOLS.has(tool)) {
3609
3616
  this.turnOutboundDelivered = true;
3610
3617
  }
3611
- this.ipcServer?.send(socket, { requestId, result, error });
3618
+ const sent = this.ipcServer?.send(socket, { requestId, result, error }) ?? false;
3619
+ if (!sent) {
3620
+ this.logger.warn({ tool, requestId }, "Tool result could not be returned — originating MCP socket is closed");
3621
+ }
3612
3622
  };
3613
3623
  // Repo checkout — handled locally in daemon (no fleet-manager)
3614
3624
  if (tool === "checkout_repo") {
@@ -3621,13 +3631,7 @@ export class Daemon extends EventEmitter {
3621
3631
  }
3622
3632
  if (tool === "set_display_name" || tool === "set_description") {
3623
3633
  const type = tool === "set_display_name" ? "fleet_set_display_name" : "fleet_set_description";
3624
- const fleetReqId = `${tool === "set_display_name" ? "dn" : "desc"}_${requestId}`;
3625
- this.ipcServer?.broadcast({
3626
- type,
3627
- payload: args,
3628
- meta: { instance_name: this.name },
3629
- fleetRequestId: fleetReqId,
3630
- });
3634
+ const fleetReqId = `${tool === "set_display_name" ? "dn" : "desc"}_${++this.fleetRequestSeq}_${requestId}`;
3631
3635
  const timeout = setTimeout(() => {
3632
3636
  this.pendingIpcRequests.delete(fleetReqId);
3633
3637
  respond(null, `${tool} timed out`);
@@ -3636,16 +3640,16 @@ export class Daemon extends EventEmitter {
3636
3640
  clearTimeout(timeout);
3637
3641
  respond(respMsg.result, respMsg.error);
3638
3642
  });
3639
- return;
3640
- }
3641
- if (tool === TASK_TOOL) {
3642
- const fleetReqId = `task_${requestId}`;
3643
3643
  this.ipcServer?.broadcast({
3644
- type: "fleet_task",
3644
+ type,
3645
3645
  payload: args,
3646
3646
  meta: { instance_name: this.name },
3647
3647
  fleetRequestId: fleetReqId,
3648
3648
  });
3649
+ return;
3650
+ }
3651
+ if (tool === TASK_TOOL) {
3652
+ const fleetReqId = `task_${++this.fleetRequestSeq}_${requestId}`;
3649
3653
  const timeout = setTimeout(() => {
3650
3654
  this.pendingIpcRequests.delete(fleetReqId);
3651
3655
  respond(null, `Task operation timed out after ${daemonBudgetMs(tool) / 1000}s`);
@@ -3654,6 +3658,12 @@ export class Daemon extends EventEmitter {
3654
3658
  clearTimeout(timeout);
3655
3659
  respond(respMsg.result, respMsg.error);
3656
3660
  });
3661
+ this.ipcServer?.broadcast({
3662
+ type: "fleet_task",
3663
+ payload: args,
3664
+ meta: { instance_name: this.name },
3665
+ fleetRequestId: fleetReqId,
3666
+ });
3657
3667
  return;
3658
3668
  }
3659
3669
  if (DECISION_TOOLS.has(tool)) {
@@ -3662,13 +3672,7 @@ export class Daemon extends EventEmitter {
3662
3672
  list_decisions: "fleet_decision_list",
3663
3673
  update_decision: "fleet_decision_update",
3664
3674
  };
3665
- const fleetReqId = `dec_${requestId}`;
3666
- this.ipcServer?.broadcast({
3667
- type: typeMap[tool],
3668
- payload: args,
3669
- meta: { instance_name: this.name, working_directory: this.config.working_directory },
3670
- fleetRequestId: fleetReqId,
3671
- });
3675
+ const fleetReqId = `dec_${++this.fleetRequestSeq}_${requestId}`;
3672
3676
  const timeout = setTimeout(() => {
3673
3677
  this.pendingIpcRequests.delete(fleetReqId);
3674
3678
  respond(null, `Decision operation timed out after ${daemonBudgetMs(tool) / 1000}s`);
@@ -3677,6 +3681,12 @@ export class Daemon extends EventEmitter {
3677
3681
  clearTimeout(timeout);
3678
3682
  respond(respMsg.result, respMsg.error);
3679
3683
  });
3684
+ this.ipcServer?.broadcast({
3685
+ type: typeMap[tool],
3686
+ payload: args,
3687
+ meta: { instance_name: this.name, working_directory: this.config.working_directory },
3688
+ fleetRequestId: fleetReqId,
3689
+ });
3680
3690
  return;
3681
3691
  }
3682
3692
  if (SCHEDULE_TOOLS.has(tool)) {
@@ -3688,13 +3698,7 @@ export class Daemon extends EventEmitter {
3688
3698
  };
3689
3699
  // Use fleetRequestId (not requestId) to avoid MCP server resolving the
3690
3700
  // pending tool call prematurely when it receives the broadcast.
3691
- const fleetReqId = `sched_${requestId}`;
3692
- this.ipcServer?.broadcast({
3693
- type: typeMap[tool],
3694
- payload: args,
3695
- meta: { chat_id: this.lastChatId, thread_id: this.lastThreadId, instance_name: this.name },
3696
- fleetRequestId: fleetReqId,
3697
- });
3701
+ const fleetReqId = `sched_${++this.fleetRequestSeq}_${requestId}`;
3698
3702
  // Wait for fleet_schedule_response via pending request map
3699
3703
  const timeout = setTimeout(() => {
3700
3704
  this.pendingIpcRequests.delete(fleetReqId);
@@ -3704,6 +3708,12 @@ export class Daemon extends EventEmitter {
3704
3708
  clearTimeout(timeout);
3705
3709
  respond(respMsg.result, respMsg.error);
3706
3710
  });
3711
+ this.ipcServer?.broadcast({
3712
+ type: typeMap[tool],
3713
+ payload: args,
3714
+ meta: { chat_id: this.lastChatId, thread_id: this.lastThreadId, instance_name: this.name },
3715
+ fleetRequestId: fleetReqId,
3716
+ });
3707
3717
  return;
3708
3718
  }
3709
3719
  if (CROSS_INSTANCE_TOOLS.has(tool)) {
@@ -3711,15 +3721,8 @@ export class Daemon extends EventEmitter {
3711
3721
  if (this.topicMode && this.ipcServer) {
3712
3722
  // Use fleetRequestId (not requestId) to avoid MCP server resolving the
3713
3723
  // pending tool call prematurely when it receives the broadcast.
3714
- const fleetReqId = `xmsg_${requestId}`;
3724
+ const fleetReqId = `xmsg_${++this.fleetRequestSeq}_${requestId}`;
3715
3725
  const senderSessionName = this.socketSessionNames.get(socket);
3716
- this.ipcServer.broadcast({
3717
- type: "fleet_outbound",
3718
- tool,
3719
- args,
3720
- fleetRequestId: fleetReqId,
3721
- senderSessionName,
3722
- });
3723
3726
  // Shared table, so delete_instance and the deployment tools get the same
3724
3727
  // budget here as the MCP client expects (the ad-hoc list omitted them).
3725
3728
  const crossTimeoutMs = daemonBudgetMs(tool);
@@ -3731,6 +3734,13 @@ export class Daemon extends EventEmitter {
3731
3734
  clearTimeout(timeout);
3732
3735
  respond(respMsg.result, respMsg.error);
3733
3736
  });
3737
+ this.ipcServer.broadcast({
3738
+ type: "fleet_outbound",
3739
+ tool,
3740
+ args,
3741
+ fleetRequestId: fleetReqId,
3742
+ senderSessionName,
3743
+ });
3734
3744
  }
3735
3745
  else {
3736
3746
  respond(null, "Cross-instance messaging requires topic mode");
@@ -3764,8 +3774,24 @@ export class Daemon extends EventEmitter {
3764
3774
  // The fleet manager's IPC client receives this and routes to shared adapter.
3765
3775
  // Use fleetRequestId (not requestId) to avoid other MCP sessions on this daemon
3766
3776
  // from prematurely resolving their pending requests when they receive the broadcast.
3767
- const fleetReqId = `tool_${requestId}`;
3777
+ // requestId is local to one MCP server process. Claude may keep multiple
3778
+ // MCP children attached (main session, resumed session, subagent); those
3779
+ // processes all emit requestId=1,2,... and previously overwrote this
3780
+ // shared pending map. The wrong caller could then receive another POST's
3781
+ // success + messageId while its own reply had never been confirmed.
3782
+ const fleetReqId = `tool_${++this.fleetRequestSeq}_${requestId}`;
3768
3783
  const outboundKey = fleetReqId;
3784
+ const timeout = setTimeout(() => {
3785
+ this.pendingIpcRequests.delete(outboundKey);
3786
+ respond(null, `Fleet outbound timed out after ${daemonBudgetMs(tool) / 1000}s`);
3787
+ }, daemonBudgetMs(tool));
3788
+ this.pendingIpcRequests.set(outboundKey, (respMsg) => {
3789
+ clearTimeout(timeout);
3790
+ respond(respMsg.result, respMsg.error);
3791
+ });
3792
+ // Register the waiter before broadcasting. A fast/mock adapter can settle
3793
+ // immediately; broadcasting first left a response-before-registration
3794
+ // window that degraded into a timeout despite a completed POST.
3769
3795
  this.ipcServer?.broadcast({
3770
3796
  type: "fleet_outbound",
3771
3797
  tool,
@@ -3776,14 +3802,6 @@ export class Daemon extends EventEmitter {
3776
3802
  // from a secondary world, and message ids are not portable across bots.
3777
3803
  adapterId: this.lastAdapterId,
3778
3804
  });
3779
- const timeout = setTimeout(() => {
3780
- this.pendingIpcRequests.delete(outboundKey);
3781
- respond(null, `Fleet outbound timed out after ${daemonBudgetMs(tool) / 1000}s`);
3782
- }, daemonBudgetMs(tool));
3783
- this.pendingIpcRequests.set(outboundKey, (respMsg) => {
3784
- clearTimeout(timeout);
3785
- respond(respMsg.result, respMsg.error);
3786
- });
3787
3805
  return;
3788
3806
  }
3789
3807
  const adapter = adapters[0];