@songsid/agend 2.1.7-beta.5 β 2.1.7-beta.7
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/backend/codex.d.ts +34 -9
- package/dist/backend/codex.js +197 -33
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +51 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +11 -0
- package/dist/channel/adapters/discord.js +16 -2
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/cli.js +79 -132
- package/dist/cli.js.map +1 -1
- package/dist/completion-install.d.ts +32 -0
- package/dist/completion-install.js +125 -1
- package/dist/completion-install.js.map +1 -1
- package/dist/config-validator.js +45 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +4 -1
- package/dist/daemon.js +23 -6
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +109 -0
- package/dist/delivery-outbox.js +306 -10
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +67 -7
- package/dist/fleet-manager.js +215 -59
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +10 -0
- package/dist/instance-config-impact.js +5 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +10 -2
- package/dist/instance-lifecycle.js +50 -32
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +17 -0
- package/dist/instructions.js +24 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- package/dist/ls-rows.d.ts +50 -0
- package/dist/ls-rows.js +152 -0
- package/dist/ls-rows.js.map +1 -0
- package/dist/outbound-handlers.d.ts +1 -1
- package/dist/outbound-handlers.js +6 -7
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +2 -1
- package/dist/outbound-schemas.js +3 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/quickstart.js +9 -3
- package/dist/quickstart.js.map +1 -1
- package/dist/remote-credential-check.d.ts +26 -7
- package/dist/remote-credential-check.js +157 -10
- package/dist/remote-credential-check.js.map +1 -1
- package/dist/settings-api.d.ts +17 -0
- package/dist/settings-api.js +62 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/status-emojis.d.ts +118 -0
- package/dist/status-emojis.js +206 -0
- package/dist/status-emojis.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/ui/dashboard.html +7 -5
- package/dist/ui/settings.html +168 -6
- package/dist/ui/view.html +138 -45
- package/dist/usage/usage-api.js +30 -0
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -16,17 +16,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
16
16
|
const __dirname = dirname(__filename);
|
|
17
17
|
/** Fallback access policy for a channel with no `access:` block β open (no gate). */
|
|
18
18
|
const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
|
|
19
|
-
|
|
20
|
-
* Telegram bot reactions replace a single slot per message. Keep delivery state
|
|
21
|
-
* presentation here so supported emoji choices can change without altering the
|
|
22
|
-
* status reconciliation logic.
|
|
23
|
-
*/
|
|
24
|
-
const TELEGRAM_DELIVERY_STATUS_REACTIONS = new Map([
|
|
25
|
-
["π", "π"], // queued / processing
|
|
26
|
-
["β³", "π"],
|
|
27
|
-
["β
", "π"], // confirmed
|
|
28
|
-
["β", "π"], // failed
|
|
29
|
-
]);
|
|
19
|
+
import { STATUS_EMOJI_KEYS, builtinStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
|
|
30
20
|
import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
31
21
|
import { loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG } from "./config.js";
|
|
32
22
|
import { EventLog } from "./event-log.js";
|
|
@@ -196,19 +186,10 @@ const TOOL_PROGRESS_EDIT_MIN_MS = 4_000;
|
|
|
196
186
|
const PROGRESS_MIN_ELAPSED_MS = 30_000;
|
|
197
187
|
/** How much of a tool summary the progress line will show before eliding. */
|
|
198
188
|
const PROGRESS_ACTIVITY_MAX_CHARS = 48;
|
|
199
|
-
/**
|
|
200
|
-
* Emoji AgEnD itself stamps on messages as the delivery-status ladder
|
|
201
|
-
* (β³ queued, π delivered, β
confirmed, β failed). These are machine
|
|
202
|
-
* indicators, not opinions, so they never enter the reactions queue β from
|
|
203
|
-
* anyone. This exact-emoji filter is the ONLY bot filtering left: bot-to-bot
|
|
204
|
-
* reactions are otherwise delivered on purpose (agents signal each other), and
|
|
205
|
-
* π«‘ passes too β it reads as a deliberate acknowledgement, not plumbing.
|
|
206
|
-
*/
|
|
207
|
-
const DELIVERY_STATUS_EMOJIS = new Set(["π", "β³", "β
", "β"]);
|
|
208
189
|
/**
|
|
209
190
|
* Reactions that are neither delivery plumbing nor meaningful conversational
|
|
210
|
-
* feedback.
|
|
211
|
-
*
|
|
191
|
+
* feedback. Delivery-status emojis are configurable (#1005) and filtered by
|
|
192
|
+
* FleetManager.isOwnStatusReaction, not by a fixed set.
|
|
212
193
|
*/
|
|
213
194
|
const IGNORED_REACTION_EMOJIS = new Set(["π·"]);
|
|
214
195
|
/**
|
|
@@ -286,6 +267,10 @@ export function deriveSpawnConcurrency(freeMemMB, cores) {
|
|
|
286
267
|
const byMemory = Math.floor(freeMemMB / 300);
|
|
287
268
|
return Math.max(2, Math.min(10, byMemory, Math.max(1, cores)));
|
|
288
269
|
}
|
|
270
|
+
/** #926: how often reply obligations are checked, the owner-reminder grace, and the default overdue window. */
|
|
271
|
+
const REPLY_OBLIGATION_SWEEP_MS = 30_000;
|
|
272
|
+
const REPLY_REMINDER_GRACE_MS = 60_000;
|
|
273
|
+
const DEFAULT_REPLY_OVERDUE_MINUTES = 15;
|
|
289
274
|
export class FleetManager {
|
|
290
275
|
dataDir;
|
|
291
276
|
static signalTarget = null;
|
|
@@ -303,6 +288,8 @@ export class FleetManager {
|
|
|
303
288
|
deliveryPumpScheduled = false;
|
|
304
289
|
deliveryPumpRunning = false;
|
|
305
290
|
deliveryPumpTimer = null;
|
|
291
|
+
/** #926: periodic reply-obligation sweep (owner reminders, requester overdue notices). */
|
|
292
|
+
replyObligationTimer = null;
|
|
306
293
|
activeDurableTargets = new Set();
|
|
307
294
|
/** Live view of lifecycle.daemons β used throughout; not deprecated. */
|
|
308
295
|
get daemons() { return this.lifecycle.daemons; }
|
|
@@ -595,6 +582,14 @@ export class FleetManager {
|
|
|
595
582
|
}
|
|
596
583
|
});
|
|
597
584
|
outbox.on("generation_recovered", () => this.scheduleDeliveryOutboxPump());
|
|
585
|
+
this.replyObligationTimer = setInterval(() => this.sweepReplyObligations(), REPLY_OBLIGATION_SWEEP_MS);
|
|
586
|
+
this.replyObligationTimer.unref?.();
|
|
587
|
+
// #856: the text the target daemon received differs from what was
|
|
588
|
+
// admitted. Transport has never been seen to do this; a warning and the
|
|
589
|
+
// attempt row's flag are the whole response until it has.
|
|
590
|
+
outbox.on("content_digest_mismatch", (event) => {
|
|
591
|
+
this.logger.warn(event, "Delivered text differs from the admitted text (content digest mismatch)");
|
|
592
|
+
});
|
|
598
593
|
outbox.on("expired", (event) => {
|
|
599
594
|
this.scheduleDeliveryOutboxPump();
|
|
600
595
|
if (event.count)
|
|
@@ -652,8 +647,38 @@ export class FleetManager {
|
|
|
652
647
|
this.logger.error({ err, source: name, operationId: prior.operationId }, "Could not persist post-restart delivery outcome notice");
|
|
653
648
|
}
|
|
654
649
|
}
|
|
650
|
+
// #926: a restart kills the turn that was working on a request, and a
|
|
651
|
+
// resumed CLI does not pick it back up. Remind the owner once per restart.
|
|
652
|
+
try {
|
|
653
|
+
outbox.remindReplyObligations(name, { reason: "restart", graceMs: 0, since: new Date() });
|
|
654
|
+
}
|
|
655
|
+
catch (err) {
|
|
656
|
+
this.logger.error({ err, owner: name }, "Could not persist the post-restart reply reminder");
|
|
657
|
+
}
|
|
655
658
|
this.scheduleDeliveryOutboxPump();
|
|
656
659
|
}
|
|
660
|
+
/**
|
|
661
|
+
* #926: the two reply-obligation safety nets. Idle owners with an unanswered
|
|
662
|
+
* request get one reminder (after the grace); requesters get one overdue
|
|
663
|
+
* notice when the owner is not working and nothing came back in time.
|
|
664
|
+
*/
|
|
665
|
+
sweepReplyObligations(now = new Date()) {
|
|
666
|
+
const outbox = this.deliveryOutbox;
|
|
667
|
+
if (!outbox || this.shuttingDown)
|
|
668
|
+
return;
|
|
669
|
+
const stateOf = (name) => this.daemons.get(name)?.getInstanceState();
|
|
670
|
+
try {
|
|
671
|
+
for (const name of this.daemons.keys()) {
|
|
672
|
+
if (stateOf(name) === "idle")
|
|
673
|
+
outbox.remindReplyObligations(name, { now, graceMs: REPLY_REMINDER_GRACE_MS, reason: "turn-ended" });
|
|
674
|
+
}
|
|
675
|
+
const minutes = this.fleetConfig?.defaults?.reply_overdue_minutes ?? DEFAULT_REPLY_OVERDUE_MINUTES;
|
|
676
|
+
outbox.notifyOverdueReplyObligations({ now, overdueMs: minutes * 60_000, ownerIdle: name => stateOf(name) !== "working" });
|
|
677
|
+
}
|
|
678
|
+
catch (err) {
|
|
679
|
+
this.logger.error({ err }, "Reply-obligation sweep failed");
|
|
680
|
+
}
|
|
681
|
+
}
|
|
657
682
|
scheduleDeliveryOutboxPump(delayMs = 0) {
|
|
658
683
|
if (!this.deliveryOutbox || this.shuttingDown)
|
|
659
684
|
return;
|
|
@@ -4688,7 +4713,7 @@ export class FleetManager {
|
|
|
4688
4713
|
this.logger.debug({ emoji: r.emoji, chatId: r.chatId }, "Reaction in an unrouted channel β ignoring");
|
|
4689
4714
|
return;
|
|
4690
4715
|
}
|
|
4691
|
-
if (
|
|
4716
|
+
if (this.isOwnStatusReaction(r)) {
|
|
4692
4717
|
this.logger.debug({ emoji: r.emoji, user: r.username }, "Ignoring delivery-status emoji as a reaction");
|
|
4693
4718
|
return;
|
|
4694
4719
|
}
|
|
@@ -5229,7 +5254,7 @@ export class FleetManager {
|
|
|
5229
5254
|
// path, so a later delivery status replaces (not stacks onto) this
|
|
5230
5255
|
// acknowledgement.
|
|
5231
5256
|
if (msg.chatId && msg.messageId) {
|
|
5232
|
-
this.reactMessageStatus(generalInstance, msg.chatId, msg.messageId, "
|
|
5257
|
+
this.reactMessageStatus(generalInstance, msg.chatId, msg.messageId, "received", msg.threadId || undefined);
|
|
5233
5258
|
}
|
|
5234
5259
|
this.warnIfRateLimited(generalInstance, msg);
|
|
5235
5260
|
const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, generalInstance);
|
|
@@ -5321,7 +5346,7 @@ export class FleetManager {
|
|
|
5321
5346
|
// status path, so a later delivery status replaces (not stacks onto) this
|
|
5322
5347
|
// acknowledgement. Same bound-adapter routing as the status path itself.
|
|
5323
5348
|
if (msg.chatId && msg.messageId) {
|
|
5324
|
-
this.reactMessageStatus(instanceName, msg.chatId, msg.messageId, "
|
|
5349
|
+
this.reactMessageStatus(instanceName, msg.chatId, msg.messageId, "received", msg.threadId || undefined);
|
|
5325
5350
|
}
|
|
5326
5351
|
// These may hit Discord API (topic icon, archive) β do after react
|
|
5327
5352
|
if (this.topicArchiver.isArchived(threadId)) {
|
|
@@ -5363,7 +5388,7 @@ export class FleetManager {
|
|
|
5363
5388
|
// Reconciled status path (not a bare add): a retry that later
|
|
5364
5389
|
// succeeds replaces this β with β
instead of leaving both. Chat and
|
|
5365
5390
|
// thread travel separately so Telegram addresses the supergroup.
|
|
5366
|
-
this.finishDeliveryStatus(instanceName, msg.chatId, msg.messageId, "
|
|
5391
|
+
this.finishDeliveryStatus(instanceName, msg.chatId, msg.messageId, "failed", msg.threadId || undefined);
|
|
5367
5392
|
}
|
|
5368
5393
|
return;
|
|
5369
5394
|
}
|
|
@@ -6809,7 +6834,129 @@ export class FleetManager {
|
|
|
6809
6834
|
deliveryStatusChains = new Map();
|
|
6810
6835
|
/** #725: in-flight CLI env probes, keyed by backend name. Coalesces concurrent /model requests. */
|
|
6811
6836
|
pendingCliEnvProbes = new Map();
|
|
6812
|
-
|
|
6837
|
+
/**
|
|
6838
|
+
* The status emojis for an instance (#1005): its own `status_emojis`, then
|
|
6839
|
+
* its channel's `options.status_emojis`, then the platform built-in. An
|
|
6840
|
+
* unusable value warns once and falls through, never failing the reaction.
|
|
6841
|
+
* `adapterId` pins the channel for paths that already know the bot (classic).
|
|
6842
|
+
*/
|
|
6843
|
+
resolveStatusEmojisFor(instanceName, adapterId, adapter) {
|
|
6844
|
+
const worldId = adapterId ?? this.getInstanceAdapterId(instanceName);
|
|
6845
|
+
const channel = (worldId ? this.worlds.get(worldId)?.channelConfig : undefined) ?? this.getChannelConfig(worldId ?? undefined);
|
|
6846
|
+
// The adapter that will react decides the vocabulary: a Telegram bot gets
|
|
6847
|
+
// Telegram's reaction set even if the channel lookup fell back elsewhere.
|
|
6848
|
+
const reacting = adapter ?? (worldId ? this.worlds.get(worldId)?.adapter : undefined) ?? this.adapter;
|
|
6849
|
+
const platform = reacting instanceof TelegramAdapter ? "telegram" : channel?.type;
|
|
6850
|
+
const resolved = resolveStatusEmojis({
|
|
6851
|
+
platform,
|
|
6852
|
+
platformConfig: channel?.options?.status_emojis,
|
|
6853
|
+
instanceConfig: this.fleetConfig?.instances[instanceName]?.status_emojis,
|
|
6854
|
+
onInvalid: (source, key, value, problem) => {
|
|
6855
|
+
const at = source === "instance" ? `instances.${instanceName}` : `channel ${channel?.id ?? platform ?? "?"}`;
|
|
6856
|
+
const once = `${at}:${key}:${String(value)}`;
|
|
6857
|
+
if (this.warnedStatusEmojis.has(once))
|
|
6858
|
+
return;
|
|
6859
|
+
this.warnedStatusEmojis.add(once);
|
|
6860
|
+
this.logger.warn({ at, key, value, problem }, `Ignoring status_emojis.${key}: ${problem} β using the default`);
|
|
6861
|
+
},
|
|
6862
|
+
});
|
|
6863
|
+
return { ...resolved, platform };
|
|
6864
|
+
}
|
|
6865
|
+
warnedStatusEmojis = new Set();
|
|
6866
|
+
/**
|
|
6867
|
+
* A Discord connection's server emojis for the Settings picker (#1005).
|
|
6868
|
+
* Cached per connection β a server's emoji set changes rarely β and one
|
|
6869
|
+
* fetch at a time; `refresh` forces a new one. Uses the running adapter, so
|
|
6870
|
+
* the bot token never leaves the server.
|
|
6871
|
+
*/
|
|
6872
|
+
async listGuildEmojis(channelId, refresh = false) {
|
|
6873
|
+
const world = this.worlds.get(channelId);
|
|
6874
|
+
if (!world)
|
|
6875
|
+
return { ok: false, error: `connection "${channelId}" is not running` };
|
|
6876
|
+
if (world.type !== "discord")
|
|
6877
|
+
return { ok: false, error: "only Discord has server custom emoji" };
|
|
6878
|
+
const adapter = world.adapter;
|
|
6879
|
+
if (!adapter.listGuildEmojis)
|
|
6880
|
+
return { ok: false, error: "this adapter cannot list server emojis" };
|
|
6881
|
+
const cached = this.guildEmojiCache.get(channelId);
|
|
6882
|
+
if (!refresh && cached && Date.now() - cached.fetched_at < FleetManager.GUILD_EMOJI_TTL_MS)
|
|
6883
|
+
return { ok: true, ...cached };
|
|
6884
|
+
let pending = this.guildEmojiFetches.get(channelId);
|
|
6885
|
+
if (!pending) {
|
|
6886
|
+
pending = adapter.listGuildEmojis().then(emojis => {
|
|
6887
|
+
const entry = { fetched_at: Date.now(), emojis };
|
|
6888
|
+
this.guildEmojiCache.set(channelId, entry);
|
|
6889
|
+
return entry;
|
|
6890
|
+
}).finally(() => this.guildEmojiFetches.delete(channelId));
|
|
6891
|
+
this.guildEmojiFetches.set(channelId, pending);
|
|
6892
|
+
}
|
|
6893
|
+
try {
|
|
6894
|
+
return { ok: true, ...(await pending) };
|
|
6895
|
+
}
|
|
6896
|
+
catch (e) {
|
|
6897
|
+
return { ok: false, error: `Discord refused the emoji list: ${e.message}` };
|
|
6898
|
+
}
|
|
6899
|
+
}
|
|
6900
|
+
static GUILD_EMOJI_TTL_MS = 10 * 60_000;
|
|
6901
|
+
guildEmojiCache = new Map();
|
|
6902
|
+
guildEmojiFetches = new Map();
|
|
6903
|
+
/** What the instructions tell `instanceName` not to react with (its own status set). */
|
|
6904
|
+
statusEmojiAvoidList(instanceName) {
|
|
6905
|
+
return statusAvoidList(this.resolveStatusEmojisFor(instanceName));
|
|
6906
|
+
}
|
|
6907
|
+
/** The progress bubble's leading emoji, in the form the channel renders. */
|
|
6908
|
+
progressPrefixFor(instanceName) {
|
|
6909
|
+
const r = this.resolveStatusEmojisFor(instanceName);
|
|
6910
|
+
const builtin = builtinStatusEmojis(r.platform).progress_prefix;
|
|
6911
|
+
return r.progress_prefix === builtin ? undefined : textForm(r.platform, r.progress_prefix);
|
|
6912
|
+
}
|
|
6913
|
+
/**
|
|
6914
|
+
* Is this inbound reaction one of the fleet's own delivery-status stamps?
|
|
6915
|
+
* Those are machine indicators and must not reach any instance as a user
|
|
6916
|
+
* reaction (#1005 point 3). When every bot's user id is known, the check
|
|
6917
|
+
* keys on WHO reacted: a fleet bot's reaction is a status stamp only if it
|
|
6918
|
+
* is in that bot's instances' status set (other bot reactions are agent
|
|
6919
|
+
* signals and pass), and a human's reaction always passes β whatever emoji
|
|
6920
|
+
* a persona picked. Without every id, fall back to the emoji alone.
|
|
6921
|
+
*/
|
|
6922
|
+
isOwnStatusReaction(r) {
|
|
6923
|
+
const key = reactionMatchKey(r.emoji, r.emojiId);
|
|
6924
|
+
const worlds = [...this.worlds.values()];
|
|
6925
|
+
const names = new Set([
|
|
6926
|
+
...Object.keys(this.fleetConfig?.instances ?? {}),
|
|
6927
|
+
...(this.classicChannels?.getAll?.() ?? []).map(c => c.instanceName),
|
|
6928
|
+
...this.instanceWorldBinding.keys(),
|
|
6929
|
+
]);
|
|
6930
|
+
if (worlds.length > 0 && worlds.every(w => !!w.botUserId)) {
|
|
6931
|
+
const reactorWorlds = new Set(worlds.filter(w => w.botUserId === r.userId).map(w => w.id));
|
|
6932
|
+
if (reactorWorlds.size === 0)
|
|
6933
|
+
return false; // a human, or a bot outside this fleet
|
|
6934
|
+
return [...names]
|
|
6935
|
+
.filter(n => reactorWorlds.has(this.getInstanceAdapterId(n) ?? ""))
|
|
6936
|
+
.some(n => statusMatchKeys(this.resolveStatusEmojisFor(n)).includes(key));
|
|
6937
|
+
}
|
|
6938
|
+
// Fallback: the built-in ladder (as before #1005) plus every value an
|
|
6939
|
+
// operator configured. Not the Telegram built-ins β π is AgEnD's failed
|
|
6940
|
+
// stamp there, but also far too common a human reaction to swallow blind.
|
|
6941
|
+
const keys = new Set(statusMatchKeys(builtinStatusEmojis(undefined)));
|
|
6942
|
+
for (const name of names) {
|
|
6943
|
+
const resolved = this.resolveStatusEmojisFor(name);
|
|
6944
|
+
const builtin = builtinStatusEmojis(resolved.platform);
|
|
6945
|
+
for (const k of STATUS_EMOJI_KEYS)
|
|
6946
|
+
if (resolved[k] !== builtin[k])
|
|
6947
|
+
keys.add(statusMatchKey(resolved[k]));
|
|
6948
|
+
}
|
|
6949
|
+
return keys.has(key);
|
|
6950
|
+
}
|
|
6951
|
+
/**
|
|
6952
|
+
* The "received" reaction for paths that react with the inbound message's
|
|
6953
|
+
* own adapter rather than through reactMessageStatus (classic bots).
|
|
6954
|
+
*/
|
|
6955
|
+
receivedReactionFor(instanceName, adapter, adapterId) {
|
|
6956
|
+
const resolved = this.resolveStatusEmojisFor(instanceName, adapterId, adapter);
|
|
6957
|
+
return reactionForm(resolved.platform, resolved.received);
|
|
6958
|
+
}
|
|
6959
|
+
reactMessageStatus(instanceName, chatId, messageId, status, threadId) {
|
|
6813
6960
|
// React via the adapter BOUND to this instance β NOT the first discord world.
|
|
6814
6961
|
// Otherwise, in a same-channel/same-guild multi-bot setup, the inbound π
|
|
6815
6962
|
// (bound bot) and the delivery/confirm reactions (some other bot) come from
|
|
@@ -6817,40 +6964,41 @@ export class FleetManager {
|
|
|
6817
6964
|
const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
6818
6965
|
if (!adapter)
|
|
6819
6966
|
return;
|
|
6820
|
-
const
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
//
|
|
6824
|
-
|
|
6825
|
-
if (!statusEmoji)
|
|
6967
|
+
const resolved = this.resolveStatusEmojisFor(instanceName, undefined, adapter);
|
|
6968
|
+
const configured = resolved[status];
|
|
6969
|
+
// Unknown statuses stay a no-op. Every resolved Telegram value has been
|
|
6970
|
+
// checked against the ReactionTypeEmoji set (status-emojis.ts).
|
|
6971
|
+
if (!configured)
|
|
6826
6972
|
return;
|
|
6973
|
+
const statusEmoji = reactionForm(resolved.platform, configured);
|
|
6827
6974
|
// Bot-scoped: sibling bots reacting on the same message must not clear
|
|
6828
6975
|
// each other's state β every removal below targets this adapter's own
|
|
6829
6976
|
// reactions (@me on Discord, the bot's list on Telegram).
|
|
6830
6977
|
const adapterId = typeof adapter.id === "string"
|
|
6831
6978
|
? adapter.id : "?";
|
|
6832
6979
|
const key = `${adapterId}:${chatId}:${threadId ?? ""}:${messageId}`;
|
|
6833
|
-
this.queueDeliveryStatusReaction(adapter, key, chatId, messageId, statusEmoji, threadId);
|
|
6980
|
+
this.queueDeliveryStatusReaction(adapter, key, chatId, messageId, statusEmoji, threadId, status);
|
|
6834
6981
|
}
|
|
6835
6982
|
/**
|
|
6836
|
-
* Apply a terminal delivery verdict. Telegram
|
|
6837
|
-
*
|
|
6983
|
+
* Apply a terminal delivery verdict. On Telegram the built-in delivered is
|
|
6984
|
+
* π and failed is π; Discord shows β
/β β both configurable (#1005).
|
|
6838
6985
|
*/
|
|
6839
|
-
finishDeliveryStatus(instanceName, chatId, messageId,
|
|
6840
|
-
this.reactMessageStatus(instanceName, chatId, messageId,
|
|
6986
|
+
finishDeliveryStatus(instanceName, chatId, messageId, status, threadId) {
|
|
6987
|
+
this.reactMessageStatus(instanceName, chatId, messageId, status, threadId);
|
|
6841
6988
|
}
|
|
6842
|
-
queueDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId) {
|
|
6989
|
+
queueDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId, status) {
|
|
6843
6990
|
const prev = this.deliveryStatusChains.get(key) ?? Promise.resolve();
|
|
6844
|
-
const run = prev.then(() => this.applyDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId));
|
|
6991
|
+
const run = prev.then(() => this.applyDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId, status));
|
|
6845
6992
|
this.deliveryStatusChains.set(key, run);
|
|
6846
6993
|
void run.then(() => { if (this.deliveryStatusChains.get(key) === run)
|
|
6847
6994
|
this.deliveryStatusChains.delete(key); }, () => { if (this.deliveryStatusChains.get(key) === run)
|
|
6848
6995
|
this.deliveryStatusChains.delete(key); });
|
|
6849
6996
|
}
|
|
6850
|
-
async applyDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId) {
|
|
6997
|
+
async applyDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId, status) {
|
|
6851
6998
|
try {
|
|
6852
6999
|
const target = adapter instanceof TelegramAdapter ? chatId : (threadId ?? chatId);
|
|
6853
|
-
const
|
|
7000
|
+
const last = this.lastStatusEmoji.get(key);
|
|
7001
|
+
const prev = last?.emoji;
|
|
6854
7002
|
if (emoji == null) {
|
|
6855
7003
|
if (prev && adapter.unreact) {
|
|
6856
7004
|
await adapter.unreact(target, messageId, prev, threadId);
|
|
@@ -6865,15 +7013,16 @@ export class FleetManager {
|
|
|
6865
7013
|
return;
|
|
6866
7014
|
// Thread-aware adapters (Discord) react where the thread is; Telegram
|
|
6867
7015
|
// addresses the supergroup chat and ignores the thread part.
|
|
6868
|
-
// #972: add-only by default β only remove
|
|
6869
|
-
// state. π/β³/β
stacking together is harmless; leaving β
|
|
6870
|
-
// after a recovery would be misleading.
|
|
6871
|
-
|
|
7016
|
+
// #972: add-only by default β only remove the failed emoji when leaving
|
|
7017
|
+
// the failed state. π/β³/β
stacking together is harmless; leaving β
|
|
7018
|
+
// visible after a recovery would be misleading. `prev` is the exact
|
|
7019
|
+
// form it was added with, so a custom emoji comes off too (#1005).
|
|
7020
|
+
if (last?.status === "failed" && adapter.unreact && !(adapter instanceof TelegramAdapter)) {
|
|
6872
7021
|
// Best effort: a failed removal must not block the new status.
|
|
6873
|
-
await adapter.unreact(target, messageId,
|
|
7022
|
+
await adapter.unreact(target, messageId, last.emoji, threadId).catch(e => this.logger.debug({ err: e.message }, "Delivery status reaction removal failed"));
|
|
6874
7023
|
}
|
|
6875
7024
|
await adapter.react(target, messageId, emoji, threadId);
|
|
6876
|
-
this.lastStatusEmoji.set(key, emoji);
|
|
7025
|
+
this.lastStatusEmoji.set(key, { emoji, status });
|
|
6877
7026
|
}
|
|
6878
7027
|
catch (e) {
|
|
6879
7028
|
this.logger.debug({ err: e.message }, "Message status react failed");
|
|
@@ -8080,11 +8229,12 @@ export class FleetManager {
|
|
|
8080
8229
|
return;
|
|
8081
8230
|
}
|
|
8082
8231
|
const publication = this.beginCancelButtonPublication(instanceName, correlationId);
|
|
8232
|
+
const initialProgressText = FleetManager.progressText(0, null, 1, this.progressPrefixFor(instanceName));
|
|
8083
8233
|
try {
|
|
8084
8234
|
const sent = await adapter.notifyAlert(chatId, {
|
|
8085
8235
|
type: "cancel",
|
|
8086
8236
|
instanceName,
|
|
8087
|
-
message:
|
|
8237
|
+
message: initialProgressText,
|
|
8088
8238
|
choices: [{ id: `cancel:${instanceName}`, label: t("cancel.button") }],
|
|
8089
8239
|
}, threadId ? { threadId } : undefined);
|
|
8090
8240
|
publication.inFlight = false;
|
|
@@ -8103,7 +8253,7 @@ export class FleetManager {
|
|
|
8103
8253
|
// Matches the text notifyAlert just posted, so the first 60s tick does
|
|
8104
8254
|
// not re-edit identical text β which put a "(edited)" mark on Discord
|
|
8105
8255
|
// with nothing visibly changed.
|
|
8106
|
-
lastProgressText:
|
|
8256
|
+
lastProgressText: initialProgressText,
|
|
8107
8257
|
// A reply can re-post the same in-flight bubble below the reply. Carry
|
|
8108
8258
|
// its current list into that replacement; fresh inbound work must start
|
|
8109
8259
|
// empty even if the daemon's reset broadcast is still in flight.
|
|
@@ -8190,14 +8340,16 @@ export class FleetManager {
|
|
|
8190
8340
|
* progress indicator (#409) β the channel showed nothing at all during long work,
|
|
8191
8341
|
* and once the agent had replied once there was no sign it was still going.
|
|
8192
8342
|
*/
|
|
8193
|
-
static progressText(elapsedMs, activity, minElapsedMs = PROGRESS_MIN_ELAPSED_MS) {
|
|
8343
|
+
static progressText(elapsedMs, activity, minElapsedMs = PROGRESS_MIN_ELAPSED_MS, prefix) {
|
|
8344
|
+
// A configured progress_prefix (#1005) leads both phases; the built-in
|
|
8345
|
+
// switches π β β³ once the elapsed time shows.
|
|
8194
8346
|
if (elapsedMs < minElapsedMs)
|
|
8195
|
-
return "π
|
|
8347
|
+
return `${prefix ?? "π"} θηδΈβ¦`;
|
|
8196
8348
|
const elapsed = FleetManager.formatProgressElapsed(elapsedMs);
|
|
8197
8349
|
const detail = FleetManager.sanitizeActivity(activity);
|
|
8198
8350
|
return detail
|
|
8199
|
-
?
|
|
8200
|
-
:
|
|
8351
|
+
? `${prefix ?? "β³"} θηδΈβ¦ (ε·²ι²θ‘ ${elapsed} Β· ${detail})`
|
|
8352
|
+
: `${prefix ?? "β³"} θηδΈβ¦ (ε·²ι²θ‘ ${elapsed})`;
|
|
8201
8353
|
}
|
|
8202
8354
|
/** Render elapsed time consistently in live and retained progress bubbles. */
|
|
8203
8355
|
static formatProgressElapsed(elapsedMs) {
|
|
@@ -8293,13 +8445,13 @@ export class FleetManager {
|
|
|
8293
8445
|
* wiped by the next elapsed tick (#528 trap 2).
|
|
8294
8446
|
*/
|
|
8295
8447
|
composeBubbleText(entry) {
|
|
8296
|
-
return FleetManager.bubbleText(Date.now() - (entry.startedAt ?? Date.now()), this.instanceActivity.get(entry.instanceName), this.progressMinElapsedMs(), entry.toolProgress);
|
|
8448
|
+
return FleetManager.bubbleText(Date.now() - (entry.startedAt ?? Date.now()), this.instanceActivity.get(entry.instanceName), this.progressMinElapsedMs(), entry.toolProgress, this.progressPrefixFor(entry.instanceName));
|
|
8297
8449
|
}
|
|
8298
8450
|
/** Pure composition of header + tool list, exposed for tests. */
|
|
8299
|
-
static bubbleText(elapsedMs, activity, minElapsedMs, progress) {
|
|
8451
|
+
static bubbleText(elapsedMs, activity, minElapsedMs, progress, prefix) {
|
|
8300
8452
|
const header = FleetManager.progressText(elapsedMs,
|
|
8301
8453
|
// The single-line activity detail is redundant once a tool list exists.
|
|
8302
|
-
progress ? undefined : activity, minElapsedMs);
|
|
8454
|
+
progress ? undefined : activity, minElapsedMs, prefix);
|
|
8303
8455
|
return progress ? `${header}\n${progress}` : header;
|
|
8304
8456
|
}
|
|
8305
8457
|
/** Recompose and edit the bubble in place; skips when nothing changed. */
|
|
@@ -9919,7 +10071,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
9919
10071
|
const classicAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
9920
10072
|
const collabReactChatId = msg.threadId ?? msg.chatId;
|
|
9921
10073
|
if (classicAdapter && collabReactChatId && msg.messageId) {
|
|
9922
|
-
classicAdapter.react(collabReactChatId, msg.messageId,
|
|
10074
|
+
classicAdapter.react(collabReactChatId, msg.messageId, this.receivedReactionFor(instanceName, classicAdapter, msg.adapterId))
|
|
9923
10075
|
.catch(e => this.logger.debug({ err: e.message }, "Auto-react failed"));
|
|
9924
10076
|
}
|
|
9925
10077
|
// Block /raw bypass β check the mention-stripped text so the self-marker
|
|
@@ -10024,7 +10176,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
10024
10176
|
}
|
|
10025
10177
|
if (msg.chatId && msg.messageId) {
|
|
10026
10178
|
const reactChatId = msg.threadId ?? msg.chatId;
|
|
10027
|
-
classicMsgAdapter.react(reactChatId, msg.messageId,
|
|
10179
|
+
classicMsgAdapter.react(reactChatId, msg.messageId, this.receivedReactionFor(instanceName, classicMsgAdapter, msg.adapterId))
|
|
10028
10180
|
.catch(e => this.logger.debug({ err: e.message }, "Auto-react failed"));
|
|
10029
10181
|
if (saved) {
|
|
10030
10182
|
const savedEmoji = msg.source === "telegram"
|
|
@@ -11412,6 +11564,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
11412
11564
|
clearInterval(this.eventLogPruneTimer);
|
|
11413
11565
|
this.eventLogPruneTimer = null;
|
|
11414
11566
|
}
|
|
11567
|
+
if (this.replyObligationTimer) {
|
|
11568
|
+
clearInterval(this.replyObligationTimer);
|
|
11569
|
+
this.replyObligationTimer = null;
|
|
11570
|
+
}
|
|
11415
11571
|
if (this.logRotateTimer) {
|
|
11416
11572
|
clearInterval(this.logRotateTimer);
|
|
11417
11573
|
this.logRotateTimer = null;
|