@songsid/agend 2.0.7 → 2.0.8-beta.10
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/README.md +3 -6
- package/dist/backend/antigravity.js +6 -2
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.js +3 -2
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.js +4 -2
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.js +4 -2
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/opencode.js.map +1 -1
- package/dist/backend/types.d.ts +8 -0
- package/dist/backend/types.js +12 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +57 -0
- package/dist/channel/adapters/discord.js +623 -0
- package/dist/channel/adapters/discord.js.map +1 -0
- package/dist/channel/adapters/telegram.d.ts +3 -0
- package/dist/channel/adapters/telegram.js +7 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/factory.js +10 -1
- package/dist/channel/factory.js.map +1 -1
- package/dist/channel/types.d.ts +6 -1
- package/dist/cli.js +2 -3
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.js +98 -27
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +19 -0
- package/dist/fleet-manager.js +234 -44
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/session-management/SKILL.md +56 -1
- package/dist/instance-lifecycle.js +7 -0
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/outbound-handlers.d.ts +1 -0
- package/dist/outbound-handlers.js +3 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/quickstart.js +2 -3
- package/dist/quickstart.js.map +1 -1
- package/dist/tmux-manager.d.ts +1 -1
- package/dist/tmux-manager.js.map +1 -1
- package/dist/topic-commands.js +12 -0
- package/dist/topic-commands.js.map +1 -1
- package/package.json +3 -5
package/dist/fleet-manager.js
CHANGED
|
@@ -78,6 +78,11 @@ export class FleetManager {
|
|
|
78
78
|
topicIcons = {};
|
|
79
79
|
lastActivity = new Map();
|
|
80
80
|
lastInboundUser = new Map(); // instanceName → last username
|
|
81
|
+
// Pending "🛑 Cancel" button per instance — sent after a message is delivered,
|
|
82
|
+
// retired when the agent replies, when cancelled, or after a timeout.
|
|
83
|
+
pendingCancelMessages = new Map();
|
|
84
|
+
// Last user message delivered to each instance — used to react ✅ on completion.
|
|
85
|
+
lastInboundMsg = new Map();
|
|
81
86
|
topicArchiver;
|
|
82
87
|
controlClient = null;
|
|
83
88
|
classicChannels = null;
|
|
@@ -739,6 +744,15 @@ export class FleetManager {
|
|
|
739
744
|
}
|
|
740
745
|
return;
|
|
741
746
|
}
|
|
747
|
+
if (data.callbackData.startsWith("cancel:")) {
|
|
748
|
+
const instanceName = data.callbackData.slice("cancel:".length);
|
|
749
|
+
// Idempotent: a button click only acts while the button is live. A
|
|
750
|
+
// second click (entry already cleared) is a no-op — don't re-send the
|
|
751
|
+
// interrupt key. (The /cancel command path calls cancelInstance directly.)
|
|
752
|
+
if (this.pendingCancelMessages.has(instanceName))
|
|
753
|
+
this.cancelInstance(instanceName);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
742
756
|
}, this.logger, "adapter.callback_query"));
|
|
743
757
|
this.adapter.on("topic_closed", safeHandler(async (data) => {
|
|
744
758
|
// Skip unbind if we archived this topic ourselves
|
|
@@ -819,6 +833,15 @@ export class FleetManager {
|
|
|
819
833
|
const result = await this.topicCommands.sendCompact(target.name);
|
|
820
834
|
await data.respond(result);
|
|
821
835
|
}
|
|
836
|
+
else if (data.command === "cancel") {
|
|
837
|
+
const target = this.routing.resolve(data.channelId);
|
|
838
|
+
if (!target) {
|
|
839
|
+
await data.respond("No active agent in this channel.");
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const ok = this.cancelInstance(target.name);
|
|
843
|
+
await data.respond(ok ? `🛑 Sent cancel to ${target.name}.` : `❌ ${target.name} not running.`);
|
|
844
|
+
}
|
|
822
845
|
else if (data.command === "ctx") {
|
|
823
846
|
const target = this.routing.resolve(data.channelId);
|
|
824
847
|
if (!target) {
|
|
@@ -888,8 +911,9 @@ export class FleetManager {
|
|
|
888
911
|
: "💬 Collaboration mode **OFF** — use `/chat` to talk to the agent.");
|
|
889
912
|
}
|
|
890
913
|
else if (data.command === "update") {
|
|
891
|
-
|
|
892
|
-
|
|
914
|
+
const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
|
|
915
|
+
if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
|
|
916
|
+
await data.respond("⛔ Not authorized");
|
|
893
917
|
return;
|
|
894
918
|
}
|
|
895
919
|
await data.respond("📦 Updating AgEnD... Fleet will restart automatically.");
|
|
@@ -900,8 +924,9 @@ export class FleetManager {
|
|
|
900
924
|
child.unref();
|
|
901
925
|
}
|
|
902
926
|
else if (data.command === "doctor") {
|
|
903
|
-
|
|
904
|
-
|
|
927
|
+
const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
|
|
928
|
+
if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
|
|
929
|
+
await data.respond("⛔ Not authorized");
|
|
905
930
|
return;
|
|
906
931
|
}
|
|
907
932
|
try {
|
|
@@ -1024,6 +1049,14 @@ export class FleetManager {
|
|
|
1024
1049
|
else {
|
|
1025
1050
|
adapter.editMessage(data.chatId, data.messageId, `⏳ Continuing to wait for ${instanceName}.`).catch(() => { });
|
|
1026
1051
|
}
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
if (data.callbackData.startsWith("cancel:")) {
|
|
1055
|
+
const instanceName = data.callbackData.slice("cancel:".length);
|
|
1056
|
+
// Idempotent: only the first click (while the button is live) acts.
|
|
1057
|
+
if (this.pendingCancelMessages.has(instanceName))
|
|
1058
|
+
this.cancelInstance(instanceName);
|
|
1059
|
+
return;
|
|
1027
1060
|
}
|
|
1028
1061
|
}, this.logger, `adapter[${adapterId}].callback_query`));
|
|
1029
1062
|
adapter.on("topic_closed", safeHandler(async (data) => {
|
|
@@ -1104,6 +1137,15 @@ export class FleetManager {
|
|
|
1104
1137
|
const result = await this.topicCommands.sendCompact(target.name);
|
|
1105
1138
|
await data.respond(result);
|
|
1106
1139
|
}
|
|
1140
|
+
else if (data.command === "cancel") {
|
|
1141
|
+
const target = this.routing.resolve(data.channelId);
|
|
1142
|
+
if (!target) {
|
|
1143
|
+
await data.respond("No active agent in this channel.");
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const ok = this.cancelInstance(target.name);
|
|
1147
|
+
await data.respond(ok ? `🛑 Sent cancel to ${target.name}.` : `❌ ${target.name} not running.`);
|
|
1148
|
+
}
|
|
1107
1149
|
else if (data.command === "ctx") {
|
|
1108
1150
|
const target = this.routing.resolve(data.channelId);
|
|
1109
1151
|
if (!target) {
|
|
@@ -1156,8 +1198,9 @@ export class FleetManager {
|
|
|
1156
1198
|
: "💬 Collaboration mode **OFF** — use `/chat` to talk to the agent.");
|
|
1157
1199
|
}
|
|
1158
1200
|
else if (data.command === "update") {
|
|
1159
|
-
|
|
1160
|
-
|
|
1201
|
+
const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
|
|
1202
|
+
if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
|
|
1203
|
+
await data.respond("⛔ Not authorized");
|
|
1161
1204
|
return;
|
|
1162
1205
|
}
|
|
1163
1206
|
await data.respond("📦 Updating AgEnD... Fleet will restart automatically.");
|
|
@@ -1168,8 +1211,9 @@ export class FleetManager {
|
|
|
1168
1211
|
child.unref();
|
|
1169
1212
|
}
|
|
1170
1213
|
else if (data.command === "doctor") {
|
|
1171
|
-
|
|
1172
|
-
|
|
1214
|
+
const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
|
|
1215
|
+
if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
|
|
1216
|
+
await data.respond("⛔ Not authorized");
|
|
1173
1217
|
return;
|
|
1174
1218
|
}
|
|
1175
1219
|
try {
|
|
@@ -1563,6 +1607,17 @@ export class FleetManager {
|
|
|
1563
1607
|
await msgAdapter?.sendText(chatId, result);
|
|
1564
1608
|
return;
|
|
1565
1609
|
}
|
|
1610
|
+
// Handle /cancel command
|
|
1611
|
+
if (text === "/cancel" || text.startsWith("/cancel@")) {
|
|
1612
|
+
const cancelTarget = this.routing.resolve(chatId);
|
|
1613
|
+
if (!cancelTarget || cancelTarget.kind !== "classic") {
|
|
1614
|
+
await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const ok = this.cancelInstance(cancelTarget.name);
|
|
1618
|
+
await msgAdapter?.sendText(chatId, ok ? `🛑 已送出取消給 ${cancelTarget.name}。` : `❌ ${cancelTarget.name} 未在執行。`);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1566
1621
|
// Handle /ctx command
|
|
1567
1622
|
if (text === "/ctx" || text.startsWith("/ctx@")) {
|
|
1568
1623
|
const ctxTarget = this.routing.resolve(chatId);
|
|
@@ -1650,6 +1705,8 @@ export class FleetManager {
|
|
|
1650
1705
|
instance: generalInstance, sender: msg.username,
|
|
1651
1706
|
text: (text ?? "").slice(0, 2000), ts: new Date().toISOString(),
|
|
1652
1707
|
});
|
|
1708
|
+
this.trackInboundMsg(generalInstance, msg);
|
|
1709
|
+
void this.sendCancelButton(generalInstance);
|
|
1653
1710
|
}
|
|
1654
1711
|
}
|
|
1655
1712
|
return;
|
|
@@ -1687,7 +1744,7 @@ export class FleetManager {
|
|
|
1687
1744
|
const inboundAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
1688
1745
|
// React immediately — before any other Discord API calls
|
|
1689
1746
|
if (msg.chatId && msg.messageId) {
|
|
1690
|
-
inboundAdapter.react(
|
|
1747
|
+
inboundAdapter.react(this.reactTarget(msg), msg.messageId, "👀")
|
|
1691
1748
|
.catch(e => this.logger.debug({ err: e.message }, "Auto-react failed"));
|
|
1692
1749
|
}
|
|
1693
1750
|
// These may hit Discord API (topic icon, archive) — do after react
|
|
@@ -1726,6 +1783,8 @@ export class FleetManager {
|
|
|
1726
1783
|
instance: instanceName, sender: msg.username,
|
|
1727
1784
|
text: (text ?? "").slice(0, 2000), ts: new Date().toISOString(),
|
|
1728
1785
|
});
|
|
1786
|
+
this.trackInboundMsg(instanceName, msg);
|
|
1787
|
+
void this.sendCancelButton(instanceName);
|
|
1729
1788
|
}
|
|
1730
1789
|
/** Handle outbound tool calls from a daemon instance */
|
|
1731
1790
|
/** Warn (but don't block) when rate limits are high. 30-min debounce per instance. */
|
|
@@ -1799,6 +1858,9 @@ export class FleetManager {
|
|
|
1799
1858
|
// Route standard channel tools (reply, react, edit_message, download_attachment)
|
|
1800
1859
|
if (routeToolCall(outAdapter, tool, args, threadId, respond)) {
|
|
1801
1860
|
if (tool === "reply") {
|
|
1861
|
+
// Agent answered — retire its pending cancel button and mark ✅ done.
|
|
1862
|
+
this.clearCancelButton(instanceName);
|
|
1863
|
+
this.reactDone(instanceName);
|
|
1802
1864
|
const replyTo = this.lastInboundUser.get(instanceName) ?? "user";
|
|
1803
1865
|
this.logger.info(`${instanceName} → ${replyTo}: ${(args.text ?? "").slice(0, 100)}`);
|
|
1804
1866
|
this.emitSseEvent("message", {
|
|
@@ -1883,6 +1945,8 @@ export class FleetManager {
|
|
|
1883
1945
|
payload: { schedule_id: id, message: `[Scheduled] ${message}`, label },
|
|
1884
1946
|
meta: { chat_id: reply_chat_id, thread_id: reply_thread_id, user: "scheduler" },
|
|
1885
1947
|
});
|
|
1948
|
+
// A scheduled trigger also puts the instance to work — show a cancel button.
|
|
1949
|
+
void this.sendCancelButton(target);
|
|
1886
1950
|
return true;
|
|
1887
1951
|
};
|
|
1888
1952
|
if (deliver()) {
|
|
@@ -2508,6 +2572,114 @@ export class FleetManager {
|
|
|
2508
2572
|
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send notification (no topic)"));
|
|
2509
2573
|
}
|
|
2510
2574
|
}
|
|
2575
|
+
// ── Cancel button ────────────────────────────────────────────────────
|
|
2576
|
+
// Sent after delivering a user message to an instance; clicking it (or
|
|
2577
|
+
// /cancel) sends Escape to the instance's pane to interrupt generation.
|
|
2578
|
+
/** Send a "🛑 Cancel" button to the instance's topic/channel after delivery. */
|
|
2579
|
+
async sendCancelButton(instanceName) {
|
|
2580
|
+
// Replace any stale button for this instance first.
|
|
2581
|
+
this.clearCancelButton(instanceName);
|
|
2582
|
+
const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
2583
|
+
if (!adapter)
|
|
2584
|
+
return;
|
|
2585
|
+
const adapterId = this.instanceWorldBinding.get(instanceName);
|
|
2586
|
+
const groupId = this.getChannelConfig(adapterId)?.group_id;
|
|
2587
|
+
const topicId = this.fleetConfig?.instances[instanceName]?.topic_id;
|
|
2588
|
+
let chatId;
|
|
2589
|
+
let threadId;
|
|
2590
|
+
if (topicId != null && groupId) {
|
|
2591
|
+
// Fleet topic instance.
|
|
2592
|
+
chatId = String(groupId);
|
|
2593
|
+
threadId = String(topicId);
|
|
2594
|
+
}
|
|
2595
|
+
else {
|
|
2596
|
+
// Classic instance: chatId from the routing table.
|
|
2597
|
+
for (const [cid, target] of this.routing.entries()) {
|
|
2598
|
+
if (target.kind === "classic" && target.name === instanceName) {
|
|
2599
|
+
chatId = cid;
|
|
2600
|
+
break;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
// General / flat fallback: post to the group (no thread).
|
|
2604
|
+
if (!chatId && groupId)
|
|
2605
|
+
chatId = String(groupId);
|
|
2606
|
+
}
|
|
2607
|
+
if (!chatId)
|
|
2608
|
+
return;
|
|
2609
|
+
try {
|
|
2610
|
+
const sent = await adapter.notifyAlert(chatId, {
|
|
2611
|
+
type: "cancel",
|
|
2612
|
+
instanceName,
|
|
2613
|
+
message: "👀 處理中…",
|
|
2614
|
+
choices: [{ id: `cancel:${instanceName}`, label: "🛑 取消" }],
|
|
2615
|
+
}, threadId ? { threadId } : undefined);
|
|
2616
|
+
const timer = setTimeout(() => this.clearCancelButton(instanceName), 30_000);
|
|
2617
|
+
this.pendingCancelMessages.set(instanceName, {
|
|
2618
|
+
adapterId, chatId: sent.chatId, messageId: sent.messageId, threadId: sent.threadId ?? threadId, timer,
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
catch (e) {
|
|
2622
|
+
this.logger.debug({ err: e.message, instanceName }, "Failed to send cancel button");
|
|
2623
|
+
}
|
|
2624
|
+
}
|
|
2625
|
+
/** Retire (delete) the pending cancel button — on reply, cancel, or timeout. */
|
|
2626
|
+
clearCancelButton(instanceName) {
|
|
2627
|
+
const pending = this.pendingCancelMessages.get(instanceName);
|
|
2628
|
+
if (!pending)
|
|
2629
|
+
return;
|
|
2630
|
+
clearTimeout(pending.timer);
|
|
2631
|
+
this.pendingCancelMessages.delete(instanceName);
|
|
2632
|
+
const adapter = (pending.adapterId ? this.worlds.get(pending.adapterId)?.adapter : undefined)
|
|
2633
|
+
?? this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
2634
|
+
if (!adapter)
|
|
2635
|
+
return;
|
|
2636
|
+
if (adapter.deleteMessage) {
|
|
2637
|
+
adapter.deleteMessage(pending.chatId, pending.messageId)
|
|
2638
|
+
.catch((e) => this.logger.debug({ err: e.message, instanceName }, "Failed to delete cancel button"));
|
|
2639
|
+
}
|
|
2640
|
+
else {
|
|
2641
|
+
// Fallback for adapters without delete: at least drop the button.
|
|
2642
|
+
const remove = adapter.editMessageRemoveButtons?.bind(adapter) ?? adapter.editMessage.bind(adapter);
|
|
2643
|
+
remove(pending.chatId, pending.messageId, "✅").catch(() => { });
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
/**
|
|
2647
|
+
* Reaction target chat id. Telegram reactions key on the supergroup chat_id
|
|
2648
|
+
* (the topic thread is NOT a chat_id), so a forum-topic message must react on
|
|
2649
|
+
* msg.chatId — reacting on threadId silently fails. Discord reactions key on
|
|
2650
|
+
* the channel/thread id.
|
|
2651
|
+
*/
|
|
2652
|
+
reactTarget(msg) {
|
|
2653
|
+
return msg.source === "telegram" ? msg.chatId : (msg.threadId ?? msg.chatId);
|
|
2654
|
+
}
|
|
2655
|
+
/** Remember the user message just delivered, so we can react ✅ when done. */
|
|
2656
|
+
trackInboundMsg(instanceName, msg) {
|
|
2657
|
+
if (!msg.chatId || !msg.messageId)
|
|
2658
|
+
return;
|
|
2659
|
+
this.lastInboundMsg.set(instanceName, {
|
|
2660
|
+
adapterId: msg.adapterId, chatId: msg.chatId, threadId: msg.threadId ?? undefined, messageId: msg.messageId, source: msg.source,
|
|
2661
|
+
});
|
|
2662
|
+
}
|
|
2663
|
+
/** React ✅ on the last user message after the agent replies. */
|
|
2664
|
+
reactDone(instanceName) {
|
|
2665
|
+
const m = this.lastInboundMsg.get(instanceName);
|
|
2666
|
+
if (!m)
|
|
2667
|
+
return;
|
|
2668
|
+
this.lastInboundMsg.delete(instanceName);
|
|
2669
|
+
const adapter = (m.adapterId ? this.worlds.get(m.adapterId)?.adapter : undefined)
|
|
2670
|
+
?? this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
2671
|
+
adapter?.react(this.reactTarget(m), m.messageId, "✅").catch(() => { });
|
|
2672
|
+
}
|
|
2673
|
+
/** Interrupt an instance's current generation (cancel button / /cancel). */
|
|
2674
|
+
cancelInstance(instanceName) {
|
|
2675
|
+
const daemon = this.daemons.get(instanceName);
|
|
2676
|
+
if (!daemon)
|
|
2677
|
+
return false;
|
|
2678
|
+
daemon.sendEscape().catch(e => this.logger.warn({ err: e, instanceName }, "sendEscape failed"));
|
|
2679
|
+
this.lastInboundMsg.delete(instanceName);
|
|
2680
|
+
this.clearCancelButton(instanceName);
|
|
2681
|
+
return true;
|
|
2682
|
+
}
|
|
2511
2683
|
queueMirrorMessage(text) {
|
|
2512
2684
|
const mirrorTopicId = this.fleetConfig?.channel?.mirror_topic_id;
|
|
2513
2685
|
if (mirrorTopicId == null || !this.adapter)
|
|
@@ -2846,10 +3018,17 @@ When users create specialized instances, suggest these configurations:
|
|
|
2846
3018
|
// Skip empty bot messages (e.g., reactions) — don't pollute chat log
|
|
2847
3019
|
if (msg.isBotMessage && !text && !msg.attachments?.length)
|
|
2848
3020
|
return;
|
|
3021
|
+
// Save attachments FIRST so the chat-log records their inbox paths
|
|
3022
|
+
// (consistent with the /chat path). Otherwise a non-@mention image is
|
|
3023
|
+
// saved to inbox but its path never reaches the agent — the log keeps
|
|
3024
|
+
// only a pathless filename, so later context can't locate the file.
|
|
3025
|
+
const saved = msg.attachments?.length ? await this.saveClassicAttachment(instanceName, msg) : undefined;
|
|
2849
3026
|
// Log every message (including other bots) to chat-logs
|
|
2850
|
-
const collabAttachTag =
|
|
2851
|
-
? ` [${
|
|
2852
|
-
:
|
|
3027
|
+
const collabAttachTag = saved
|
|
3028
|
+
? ` [${saved.kind === "photo" ? "📷" : "📎"} saved: ${saved.paths.join(", ")}]`
|
|
3029
|
+
: (msg.attachments?.length
|
|
3030
|
+
? ` [${msg.attachments.map(a => `${a.kind === "photo" ? "📷" : "📎"} ${a.filename || a.kind}`).join(", ")}]`
|
|
3031
|
+
: "");
|
|
2853
3032
|
ClassicChannelManager.logMessage(instanceName, msg.username, text + collabAttachTag, msg.timestamp, msg.replyToText);
|
|
2854
3033
|
this.logger.info({ instanceName, user: msg.username, textLen: text.length, attachments: msg.attachments?.length ?? 0, source: msg.source }, "Collab mode message");
|
|
2855
3034
|
// Check for @mention trigger: must be exact <@BOT_USER_ID>, not @everyone/@here
|
|
@@ -2857,19 +3036,16 @@ When users create specialized instances, suggest these configurations:
|
|
|
2857
3036
|
const mentionTag = adapterBotUserId ? `<@${adapterBotUserId}>` : null;
|
|
2858
3037
|
const isMentioned = mentionTag && text.includes(mentionTag);
|
|
2859
3038
|
if (!isMentioned) {
|
|
2860
|
-
//
|
|
2861
|
-
if (
|
|
2862
|
-
const
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
const
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
reactAdapter.react(noMentionReactChatId, msg.messageId, emoji)
|
|
2871
|
-
.catch(e => this.logger.debug({ err: e.message }, "Auto-react failed"));
|
|
2872
|
-
}
|
|
3039
|
+
// Bare attachment (no @mention) — already saved above; just acknowledge.
|
|
3040
|
+
if (saved) {
|
|
3041
|
+
const reactAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
3042
|
+
const noMentionReactChatId = msg.threadId ?? msg.chatId;
|
|
3043
|
+
if (reactAdapter && noMentionReactChatId && msg.messageId) {
|
|
3044
|
+
const emoji = msg.source === "telegram"
|
|
3045
|
+
? (saved.kind === "photo" ? "👌" : "👍")
|
|
3046
|
+
: (saved.kind === "photo" ? "📸" : "📎");
|
|
3047
|
+
reactAdapter.react(noMentionReactChatId, msg.messageId, emoji)
|
|
3048
|
+
.catch(e => this.logger.debug({ err: e.message }, "Auto-react failed"));
|
|
2873
3049
|
}
|
|
2874
3050
|
}
|
|
2875
3051
|
return;
|
|
@@ -2887,8 +3063,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
2887
3063
|
// Block /raw bypass
|
|
2888
3064
|
if (cleanText.startsWith("/raw "))
|
|
2889
3065
|
return;
|
|
2890
|
-
//
|
|
2891
|
-
const saved = await this.saveClassicAttachment(instanceName, msg);
|
|
3066
|
+
// Attachments already saved at the top of the collab block.
|
|
2892
3067
|
if (saved && classicAdapter && collabReactChatId && msg.messageId) {
|
|
2893
3068
|
const emoji = msg.source === "telegram"
|
|
2894
3069
|
? (saved.kind === "photo" ? "👌" : "👍")
|
|
@@ -3045,24 +3220,37 @@ When users create specialized instances, suggest these configurations:
|
|
|
3045
3220
|
this.logger.warn({ instanceName }, "Classic channel instance IPC not connected");
|
|
3046
3221
|
return;
|
|
3047
3222
|
}
|
|
3223
|
+
const meta = {
|
|
3224
|
+
chat_id: msg.chatId,
|
|
3225
|
+
message_id: msg.messageId,
|
|
3226
|
+
user: msg.username,
|
|
3227
|
+
user_id: msg.userId,
|
|
3228
|
+
ts: msg.timestamp.toISOString(),
|
|
3229
|
+
thread_id: msg.threadId ?? "",
|
|
3230
|
+
source: msg.source,
|
|
3231
|
+
...extraMeta,
|
|
3232
|
+
...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
|
|
3233
|
+
};
|
|
3234
|
+
// If the triggering message carried no image of its own, surface the most
|
|
3235
|
+
// recent image saved earlier in this channel (logged as "[📷 saved: <path>]"
|
|
3236
|
+
// by an untriggered collab message) as image_path, so the agent's
|
|
3237
|
+
// read-the-image trigger fires instead of the path sitting inert in context.
|
|
3238
|
+
if (!meta.image_path && logContext) {
|
|
3239
|
+
const saves = [...logContext.matchAll(/\[📷 saved: ([^\]]+)\]/g)];
|
|
3240
|
+
if (saves.length > 0) {
|
|
3241
|
+
meta.image_path = saves[saves.length - 1][1].split(",")[0].trim();
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3048
3244
|
ipc.send({
|
|
3049
3245
|
type: "fleet_inbound",
|
|
3050
3246
|
content: fullText,
|
|
3051
3247
|
targetSession: instanceName,
|
|
3052
|
-
meta
|
|
3053
|
-
chat_id: msg.chatId,
|
|
3054
|
-
message_id: msg.messageId,
|
|
3055
|
-
user: msg.username,
|
|
3056
|
-
user_id: msg.userId,
|
|
3057
|
-
ts: msg.timestamp.toISOString(),
|
|
3058
|
-
thread_id: msg.threadId ?? "",
|
|
3059
|
-
source: msg.source,
|
|
3060
|
-
...extraMeta,
|
|
3061
|
-
...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
|
|
3062
|
-
},
|
|
3248
|
+
meta,
|
|
3063
3249
|
});
|
|
3064
3250
|
this.lastInboundUser.set(instanceName, msg.username);
|
|
3065
3251
|
this.logger.info(`${msg.username} → ${instanceName} (classic): ${text.slice(0, 100)}`);
|
|
3252
|
+
this.trackInboundMsg(instanceName, msg);
|
|
3253
|
+
void this.sendCancelButton(instanceName);
|
|
3066
3254
|
}
|
|
3067
3255
|
/** Paste raw text directly to a classic instance's CLI (no [user:] wrapping) */
|
|
3068
3256
|
pasteRawToClassicInstance(instanceName, text) {
|
|
@@ -3182,9 +3370,11 @@ When users create specialized instances, suggest these configurations:
|
|
|
3182
3370
|
this.topicArchiver.stop();
|
|
3183
3371
|
this.scheduler?.shutdown();
|
|
3184
3372
|
// Stop instances in parallel batches to avoid long sequential waits.
|
|
3185
|
-
// Concurrency
|
|
3186
|
-
|
|
3373
|
+
// Concurrency scales with fleet size — larger fleets tolerate more parallel
|
|
3374
|
+
// tmux ops, while small fleets stay conservative to avoid overwhelming the
|
|
3375
|
+
// tmux server.
|
|
3187
3376
|
const entries = [...this.daemons.entries()];
|
|
3377
|
+
const STOP_CONCURRENCY = entries.length > 30 ? 15 : entries.length >= 10 ? 10 : 5;
|
|
3188
3378
|
for (const [name] of entries)
|
|
3189
3379
|
this.ipcStoppingInstances.add(name);
|
|
3190
3380
|
for (let i = 0; i < entries.length; i += STOP_CONCURRENCY) {
|
|
@@ -3199,9 +3389,9 @@ When users create specialized instances, suggest these configurations:
|
|
|
3199
3389
|
this.daemons.delete(name);
|
|
3200
3390
|
}));
|
|
3201
3391
|
}
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
}
|
|
3392
|
+
// Close IPC clients in parallel — serial close over a large fleet adds
|
|
3393
|
+
// noticeable latency.
|
|
3394
|
+
await Promise.all([...this.instanceIpcClients.values()].map(ipc => Promise.resolve(ipc.close()).catch(() => { })));
|
|
3205
3395
|
this.instanceIpcClients.clear();
|
|
3206
3396
|
this.ipcStoppingInstances.clear();
|
|
3207
3397
|
for (const [, w] of this.worlds) {
|
|
@@ -3547,7 +3737,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
3547
3737
|
if (target && target !== currentVersion) {
|
|
3548
3738
|
const generalId = this.findGeneralInstance();
|
|
3549
3739
|
if (generalId) {
|
|
3550
|
-
this.notifyInstanceTopic(generalId, `🆕 AgEnD v${target} available (current: v${currentVersion}). Use
|
|
3740
|
+
this.notifyInstanceTopic(generalId, `🆕 AgEnD v${target} available (current: v${currentVersion}). Use \`/update\` to upgrade.`);
|
|
3551
3741
|
}
|
|
3552
3742
|
}
|
|
3553
3743
|
}
|