@songsid/agend 2.1.3 → 2.1.4-beta.2
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/LICENSE +21 -0
- package/README.md +2 -2
- package/README.zh-TW.md +2 -2
- package/dist/backend/antigravity.d.ts +1 -0
- package/dist/backend/antigravity.js +1 -0
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +1 -0
- package/dist/backend/claude-code.js +1 -0
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +1 -0
- package/dist/backend/codex.js +1 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/gemini-cli.d.ts +1 -0
- package/dist/backend/gemini-cli.js +1 -0
- package/dist/backend/gemini-cli.js.map +1 -1
- package/dist/backend/grok.d.ts +1 -0
- package/dist/backend/grok.js +1 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +1 -0
- package/dist/backend/kiro.js +1 -0
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/mock.d.ts +1 -0
- package/dist/backend/mock.js +1 -0
- package/dist/backend/mock.js.map +1 -1
- package/dist/backend/opencode.d.ts +24 -0
- package/dist/backend/opencode.js +72 -5
- package/dist/backend/opencode.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.js +5 -1
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/types.d.ts +1 -1
- package/dist/cli.js +9 -10
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +0 -9
- package/dist/daemon.js +0 -53
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-manager.d.ts +17 -2
- package/dist/fleet-manager.js +211 -32
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +2 -0
- package/dist/instance-lifecycle.js +4 -8
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +30 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -1
- package/dist/quickstart.js +4 -2
- package/dist/quickstart.js.map +1 -1
- package/dist/setup-wizard.d.ts +4 -2
- package/dist/setup-wizard.js +2 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/topic-commands.d.ts +9 -0
- package/dist/topic-commands.js +49 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/dashboard.html +5 -4
- package/dist/web-api.js +2 -2
- package/dist/web-api.js.map +1 -1
- package/package.json +2 -2
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -119,6 +119,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
119
119
|
/** In-flight /effort selections, same coordinator shape as pendingModelSelects. */
|
|
120
120
|
private pendingEffortSelects;
|
|
121
121
|
private pendingModelSelects;
|
|
122
|
+
/** One-shot General assistance buttons for stable interactive terminal prompts. */
|
|
123
|
+
private pendingInteractivePromptAssists;
|
|
122
124
|
private failoverActive;
|
|
123
125
|
/** instance → when a click with no live button entry last fired a cancel. */
|
|
124
126
|
private staleCancelClickAt;
|
|
@@ -216,6 +218,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
216
218
|
*/
|
|
217
219
|
private isModelAdmin;
|
|
218
220
|
private handlePauseWakeSlash;
|
|
221
|
+
/** Admin-only full conversation reset for fleet-topic and Classic instances. */
|
|
222
|
+
private handleClearSlash;
|
|
219
223
|
/** Get the adapter bound to an instance, falling back to primary adapter */
|
|
220
224
|
getAdapterForInstance(name: string): ChannelAdapter | null;
|
|
221
225
|
/** Get the world for an instance */
|
|
@@ -426,8 +430,6 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
426
430
|
private handleOutboundFromInstance;
|
|
427
431
|
/** Side effects of a routed reply: cancel-button lifecycle, logs, SSE, chat log. */
|
|
428
432
|
private afterReplyRouted;
|
|
429
|
-
/** Handle tool status update from a daemon instance */
|
|
430
|
-
private handleToolStatusFromInstance;
|
|
431
433
|
private handleScheduleTrigger;
|
|
432
434
|
private notifySourceTopic;
|
|
433
435
|
private notifyScheduleFailure;
|
|
@@ -530,6 +532,19 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
530
532
|
private static readonly FLEET_ERROR_THROTTLE_MS;
|
|
531
533
|
private fleetErrorNotices;
|
|
532
534
|
notifyInstanceTopic(instanceName: string, text: string, extraOpts?: import("./channel/types.js").SendOpts): void;
|
|
535
|
+
private interactivePromptLabel;
|
|
536
|
+
/**
|
|
537
|
+
* Surface a stable terminal prompt in both places that matter:
|
|
538
|
+
* - the blocked instance gets a plain pointer to General;
|
|
539
|
+
* - General gets one-shot Confirm/Cancel controls on its own bound adapter.
|
|
540
|
+
*
|
|
541
|
+
* Only the prompt category crosses channels. The captured terminal line can
|
|
542
|
+
* contain usernames or secret-adjacent text and stays in daemon.log.
|
|
543
|
+
*/
|
|
544
|
+
notifyInteractivePrompt(instanceName: string, kind: string): Promise<void>;
|
|
545
|
+
private retireInteractivePromptButtons;
|
|
546
|
+
/** Consume a General assist button exactly once. */
|
|
547
|
+
private handleInteractivePromptAssist;
|
|
533
548
|
/** Send a "🛑 Cancel" button to the instance's topic/channel after delivery. */
|
|
534
549
|
/**
|
|
535
550
|
* Handle the DC `/save` slash command for both classic AND fleet-topic targets.
|
package/dist/fleet-manager.js
CHANGED
|
@@ -165,6 +165,8 @@ const CLASSIC_BACKEND_SELECTION_TIMEOUT_MS = 60_000;
|
|
|
165
165
|
const CLASSIC_BACKEND_CALLBACK_PREFIX = "classic-backend:";
|
|
166
166
|
const MODEL_SELECT_CALLBACK_PREFIX = "model-select:";
|
|
167
167
|
const EFFORT_SELECT_CALLBACK_PREFIX = "effort-select:";
|
|
168
|
+
const INTERACTIVE_ASSIST_CALLBACK_PREFIX = "interactive-assist:";
|
|
169
|
+
const INTERACTIVE_ASSIST_TIMEOUT_MS = 15 * 60_000;
|
|
168
170
|
const CLI_ENV_TTL_MS = 24 * 60 * 60 * 1000; // /model reads cached CLI env within 24h
|
|
169
171
|
export class FleetManager {
|
|
170
172
|
dataDir;
|
|
@@ -253,6 +255,8 @@ export class FleetManager {
|
|
|
253
255
|
/** In-flight /effort selections, same coordinator shape as pendingModelSelects. */
|
|
254
256
|
pendingEffortSelects = new Map();
|
|
255
257
|
pendingModelSelects = new Map();
|
|
258
|
+
/** One-shot General assistance buttons for stable interactive terminal prompts. */
|
|
259
|
+
pendingInteractivePromptAssists = new Map();
|
|
256
260
|
// Model failover state
|
|
257
261
|
failoverActive = new Map(); // instance → current failover model
|
|
258
262
|
// IPC reconnect: tracks instances being intentionally stopped (skip reconnect)
|
|
@@ -553,6 +557,19 @@ export class FleetManager {
|
|
|
553
557
|
}
|
|
554
558
|
await data.respond(await this.topicCommands.runPauseWake(target, action));
|
|
555
559
|
}
|
|
560
|
+
/** Admin-only full conversation reset for fleet-topic and Classic instances. */
|
|
561
|
+
async handleClearSlash(data, adapterId) {
|
|
562
|
+
if (!this.isModelAdmin(data.userId, data.channelId, adapterId)) {
|
|
563
|
+
await data.respond(t("permission.denied"));
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
567
|
+
if (!name) {
|
|
568
|
+
await data.respond(t("classic.no_agent"));
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
await data.respond(await this.topicCommands.sendClear(name));
|
|
572
|
+
}
|
|
556
573
|
/** Get the adapter bound to an instance, falling back to primary adapter */
|
|
557
574
|
getAdapterForInstance(name) {
|
|
558
575
|
const worldId = this.getInstanceAdapterId(name);
|
|
@@ -1980,6 +1997,8 @@ export class FleetManager {
|
|
|
1980
1997
|
await this.handleInboundReaction(r);
|
|
1981
1998
|
}, this.logger, "adapter.reaction"));
|
|
1982
1999
|
this.adapter.on("callback_query", safeHandler(async (data) => {
|
|
2000
|
+
if (await this.handleInteractivePromptAssist(data, adapterId))
|
|
2001
|
+
return;
|
|
1983
2002
|
if (await this.handleClassicBackendSelection(data))
|
|
1984
2003
|
return;
|
|
1985
2004
|
if (await this.handleModelSelection(data))
|
|
@@ -2083,6 +2102,9 @@ export class FleetManager {
|
|
|
2083
2102
|
const result = await this.topicCommands.sendCompact(name);
|
|
2084
2103
|
await data.respond(result);
|
|
2085
2104
|
}
|
|
2105
|
+
else if (data.command === "clear") {
|
|
2106
|
+
await this.handleClearSlash(data, adapterId);
|
|
2107
|
+
}
|
|
2086
2108
|
else if (data.command === "model") {
|
|
2087
2109
|
await this.handleModelSlash(data, adapterId);
|
|
2088
2110
|
}
|
|
@@ -2290,6 +2312,8 @@ export class FleetManager {
|
|
|
2290
2312
|
await this.handleInboundReaction(r);
|
|
2291
2313
|
}, this.logger, `adapter[${adapterId}].reaction`));
|
|
2292
2314
|
adapter.on("callback_query", safeHandler(async (data) => {
|
|
2315
|
+
if (await this.handleInteractivePromptAssist(data, adapterId))
|
|
2316
|
+
return;
|
|
2293
2317
|
if (await this.handleClassicBackendSelection(data))
|
|
2294
2318
|
return;
|
|
2295
2319
|
if (await this.handleModelSelection(data))
|
|
@@ -2385,6 +2409,9 @@ export class FleetManager {
|
|
|
2385
2409
|
else if (data.command === "model") {
|
|
2386
2410
|
await this.handleModelSlash(data, adapterId);
|
|
2387
2411
|
}
|
|
2412
|
+
else if (data.command === "clear") {
|
|
2413
|
+
await this.handleClearSlash(data, adapterId);
|
|
2414
|
+
}
|
|
2388
2415
|
else if (data.command === "effort") {
|
|
2389
2416
|
await this.handleEffortSlash(data, adapterId);
|
|
2390
2417
|
}
|
|
@@ -2607,9 +2634,6 @@ export class FleetManager {
|
|
|
2607
2634
|
}
|
|
2608
2635
|
await this.handleOutboundFromInstance(name, msg);
|
|
2609
2636
|
}
|
|
2610
|
-
else if (msg.type === "fleet_tool_status") {
|
|
2611
|
-
this.handleToolStatusFromInstance(name, msg);
|
|
2612
|
-
}
|
|
2613
2637
|
else if (msg.type === "fleet_schedule_create" || msg.type === "fleet_schedule_list" ||
|
|
2614
2638
|
msg.type === "fleet_schedule_update" || msg.type === "fleet_schedule_delete") {
|
|
2615
2639
|
this.handleScheduleCrud(name, msg);
|
|
@@ -3076,6 +3100,21 @@ export class FleetManager {
|
|
|
3076
3100
|
await msgAdapter?.sendText(chatId, result);
|
|
3077
3101
|
return;
|
|
3078
3102
|
}
|
|
3103
|
+
// Handle /clear command (admin only) — unlike /compact this starts a
|
|
3104
|
+
// fresh conversation and intentionally discards the current history.
|
|
3105
|
+
if (text === "/clear" || text.startsWith("/clear@")) {
|
|
3106
|
+
if (!this.isModelAdmin(msg.userId, chatId, msg.adapterId)) {
|
|
3107
|
+
await msgAdapter?.sendText(chatId, t("cmd.admin_required", "/clear"));
|
|
3108
|
+
return;
|
|
3109
|
+
}
|
|
3110
|
+
const clearName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
3111
|
+
if (!clearName) {
|
|
3112
|
+
await msgAdapter?.sendText(chatId, t("classic.no_agent_start"));
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
await msgAdapter?.sendText(chatId, await this.topicCommands.sendClear(clearName));
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3079
3118
|
// Handle /cancel command
|
|
3080
3119
|
if (text === "/cancel" || text.startsWith("/cancel@")) {
|
|
3081
3120
|
const cancelName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
@@ -3482,34 +3521,6 @@ export class FleetManager {
|
|
|
3482
3521
|
ClassicChannelManager.logMessage(instanceName, "bot", args.text ?? "", new Date());
|
|
3483
3522
|
}
|
|
3484
3523
|
}
|
|
3485
|
-
/** Handle tool status update from a daemon instance */
|
|
3486
|
-
handleToolStatusFromInstance(instanceName, msg) {
|
|
3487
|
-
const statusAdapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
3488
|
-
if (!statusAdapter)
|
|
3489
|
-
return;
|
|
3490
|
-
const text = msg.text;
|
|
3491
|
-
const editMessageId = msg.editMessageId;
|
|
3492
|
-
const senderSessionName = msg.senderSessionName;
|
|
3493
|
-
const senderInstanceName = senderSessionName && this.fleetConfig?.instances[senderSessionName]
|
|
3494
|
-
? senderSessionName
|
|
3495
|
-
: null;
|
|
3496
|
-
const routingConfig = senderInstanceName
|
|
3497
|
-
? this.fleetConfig?.instances[senderInstanceName]
|
|
3498
|
-
: (senderSessionName ? undefined : this.fleetConfig?.instances[instanceName]);
|
|
3499
|
-
const threadId = routingConfig?.topic_id ? String(routingConfig.topic_id) : undefined;
|
|
3500
|
-
const chatId = statusAdapter.getChatId();
|
|
3501
|
-
if (!chatId)
|
|
3502
|
-
return;
|
|
3503
|
-
if (editMessageId) {
|
|
3504
|
-
statusAdapter.editMessage(chatId, editMessageId, text, threadId).catch(e => this.logger.debug({ err: e }, "Failed to edit tool status message"));
|
|
3505
|
-
}
|
|
3506
|
-
else {
|
|
3507
|
-
statusAdapter.sendText(chatId, text, { threadId }).then((sent) => {
|
|
3508
|
-
const ipc = this.instanceIpcClients.get(instanceName);
|
|
3509
|
-
ipc?.send({ type: "fleet_tool_status_ack", messageId: sent.messageId });
|
|
3510
|
-
}).catch(e => this.logger.warn({ err: e }, "Failed to send tool status message"));
|
|
3511
|
-
}
|
|
3512
|
-
}
|
|
3513
3524
|
// ===================== Scheduler =====================
|
|
3514
3525
|
async handleScheduleTrigger(schedule) {
|
|
3515
3526
|
const { target, reply_chat_id, reply_thread_id, message, label, id, source, silent } = schedule;
|
|
@@ -4243,7 +4254,13 @@ export class FleetManager {
|
|
|
4243
4254
|
}
|
|
4244
4255
|
catch (err) {
|
|
4245
4256
|
const e = err;
|
|
4246
|
-
|
|
4257
|
+
const output = stripAnsi(e.stdout ?? "").trim();
|
|
4258
|
+
if (output)
|
|
4259
|
+
return output;
|
|
4260
|
+
if (e.code === "ENOENT") {
|
|
4261
|
+
return "Backend doctor unavailable — agend CLI not found in PATH";
|
|
4262
|
+
}
|
|
4263
|
+
return stripAnsi(e.message ?? "").trim() || "Doctor failed";
|
|
4247
4264
|
}
|
|
4248
4265
|
}
|
|
4249
4266
|
/** Drop event/activity rows older than the retention window. Best-effort. */
|
|
@@ -4395,6 +4412,163 @@ export class FleetManager {
|
|
|
4395
4412
|
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send notification (no topic)"));
|
|
4396
4413
|
}
|
|
4397
4414
|
}
|
|
4415
|
+
interactivePromptLabel(kind) {
|
|
4416
|
+
const key = `interactive.kind.${kind}`;
|
|
4417
|
+
const translated = t(key);
|
|
4418
|
+
return translated === key ? kind.replaceAll("_", " ") : translated;
|
|
4419
|
+
}
|
|
4420
|
+
/**
|
|
4421
|
+
* Surface a stable terminal prompt in both places that matter:
|
|
4422
|
+
* - the blocked instance gets a plain pointer to General;
|
|
4423
|
+
* - General gets one-shot Confirm/Cancel controls on its own bound adapter.
|
|
4424
|
+
*
|
|
4425
|
+
* Only the prompt category crosses channels. The captured terminal line can
|
|
4426
|
+
* contain usernames or secret-adjacent text and stays in daemon.log.
|
|
4427
|
+
*/
|
|
4428
|
+
async notifyInteractivePrompt(instanceName, kind) {
|
|
4429
|
+
this.notifyInstanceTopic(instanceName, t("interactive.instance_notice", instanceName));
|
|
4430
|
+
// Multi-world fleets can have one General per bot/platform. Route the assist
|
|
4431
|
+
// request to the General bound to the same adapter as the blocked instance.
|
|
4432
|
+
const generalName = this.findGeneralInstance(this.getInstanceAdapterId(instanceName));
|
|
4433
|
+
if (!generalName) {
|
|
4434
|
+
this.logger.warn({ instanceName }, "Interactive prompt has no General topic notification target");
|
|
4435
|
+
return;
|
|
4436
|
+
}
|
|
4437
|
+
const adapterId = this.getInstanceAdapterId(generalName);
|
|
4438
|
+
const adapter = this.getAdapterForInstance(generalName);
|
|
4439
|
+
const chatId = this.getGroupIdForInstance(generalName);
|
|
4440
|
+
const topicId = this.fleetConfig?.instances[generalName]?.topic_id;
|
|
4441
|
+
const threadId = topicId != null ? String(topicId) : undefined;
|
|
4442
|
+
if (!adapter || !adapterId || !chatId) {
|
|
4443
|
+
this.logger.warn({ instanceName, generalName, adapterId, chatId }, "Cannot address interactive prompt assistance controls");
|
|
4444
|
+
return;
|
|
4445
|
+
}
|
|
4446
|
+
const nonce = randomBytes(8).toString("hex");
|
|
4447
|
+
const label = this.interactivePromptLabel(kind);
|
|
4448
|
+
const entry = {
|
|
4449
|
+
instanceName,
|
|
4450
|
+
generalName,
|
|
4451
|
+
kind,
|
|
4452
|
+
adapterId,
|
|
4453
|
+
adapter,
|
|
4454
|
+
chatId,
|
|
4455
|
+
threadId,
|
|
4456
|
+
};
|
|
4457
|
+
entry.timer = setTimeout(() => {
|
|
4458
|
+
const pending = this.pendingInteractivePromptAssists.get(nonce);
|
|
4459
|
+
if (pending !== entry)
|
|
4460
|
+
return;
|
|
4461
|
+
this.pendingInteractivePromptAssists.delete(nonce);
|
|
4462
|
+
if (entry.messageId && entry.adapter.editMessageRemoveButtons) {
|
|
4463
|
+
entry.adapter.editMessageRemoveButtons(entry.chatId, entry.messageId, t("interactive.expired", entry.instanceName), entry.threadId).catch(err => this.logger.debug({ err, instanceName }, "Failed to expire interactive prompt controls"));
|
|
4464
|
+
}
|
|
4465
|
+
}, INTERACTIVE_ASSIST_TIMEOUT_MS);
|
|
4466
|
+
entry.timer.unref?.();
|
|
4467
|
+
this.pendingInteractivePromptAssists.set(nonce, entry);
|
|
4468
|
+
try {
|
|
4469
|
+
const sent = await adapter.notifyAlert(chatId, {
|
|
4470
|
+
type: "interactive_prompt",
|
|
4471
|
+
instanceName,
|
|
4472
|
+
message: t("interactive.general_notice", instanceName, label),
|
|
4473
|
+
choices: [
|
|
4474
|
+
{ id: `${INTERACTIVE_ASSIST_CALLBACK_PREFIX}${nonce}:confirm`, label: t("interactive.confirm") },
|
|
4475
|
+
{ id: `${INTERACTIVE_ASSIST_CALLBACK_PREFIX}${nonce}:cancel`, label: t("interactive.cancel") },
|
|
4476
|
+
],
|
|
4477
|
+
}, threadId ? { threadId } : undefined);
|
|
4478
|
+
entry.messageId = sent.messageId;
|
|
4479
|
+
}
|
|
4480
|
+
catch (err) {
|
|
4481
|
+
this.pendingInteractivePromptAssists.delete(nonce);
|
|
4482
|
+
if (entry.timer)
|
|
4483
|
+
clearTimeout(entry.timer);
|
|
4484
|
+
this.logger.warn({ err, instanceName, generalName }, "Failed to send interactive prompt assistance controls");
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
async retireInteractivePromptButtons(pending, messageId, text) {
|
|
4488
|
+
if (!pending.adapter.editMessageRemoveButtons) {
|
|
4489
|
+
this.logger.warn({ instanceName: pending.instanceName, adapterId: pending.adapterId }, "Adapter cannot remove interactive prompt controls");
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
try {
|
|
4493
|
+
await pending.adapter.editMessageRemoveButtons(pending.chatId, messageId, text, pending.threadId);
|
|
4494
|
+
}
|
|
4495
|
+
catch (err) {
|
|
4496
|
+
// The action was already atomically consumed. An edit failure must not
|
|
4497
|
+
// prevent Confirm from reaching General or make Cancel actionable again.
|
|
4498
|
+
this.logger.warn({ err, instanceName: pending.instanceName }, "Failed to retire interactive prompt controls");
|
|
4499
|
+
}
|
|
4500
|
+
}
|
|
4501
|
+
/** Consume a General assist button exactly once. */
|
|
4502
|
+
async handleInteractivePromptAssist(data, callbackAdapterId) {
|
|
4503
|
+
if (!data.callbackData.startsWith(INTERACTIVE_ASSIST_CALLBACK_PREFIX))
|
|
4504
|
+
return false;
|
|
4505
|
+
const match = data.callbackData.match(/^interactive-assist:([0-9a-f]+):(confirm|cancel)$/);
|
|
4506
|
+
if (!match)
|
|
4507
|
+
return true;
|
|
4508
|
+
const pending = this.pendingInteractivePromptAssists.get(match[1]);
|
|
4509
|
+
if (!pending)
|
|
4510
|
+
return true;
|
|
4511
|
+
// Bind the capability to the exact message/world that created it. Telegram
|
|
4512
|
+
// keyboards are visible to everyone, so the click also requires fleet admin.
|
|
4513
|
+
if (pending.adapterId !== callbackAdapterId
|
|
4514
|
+
|| data.chatId !== pending.chatId
|
|
4515
|
+
|| (pending.threadId != null && data.threadId !== pending.threadId)
|
|
4516
|
+
|| (pending.messageId != null && data.messageId !== pending.messageId)
|
|
4517
|
+
|| !data.userId
|
|
4518
|
+
|| !this.isFleetAdmin(data.userId, callbackAdapterId)) {
|
|
4519
|
+
this.logger.warn({ instanceName: pending.instanceName, userId: data.userId }, "Rejected unauthorized or mismatched interactive prompt callback");
|
|
4520
|
+
return true;
|
|
4521
|
+
}
|
|
4522
|
+
// Claim before any await: double clicks and duplicate callback delivery can
|
|
4523
|
+
// never inject two messages into General.
|
|
4524
|
+
this.pendingInteractivePromptAssists.delete(match[1]);
|
|
4525
|
+
if (pending.timer)
|
|
4526
|
+
clearTimeout(pending.timer);
|
|
4527
|
+
const action = match[2];
|
|
4528
|
+
this.eventLog?.insert(pending.instanceName, "interactive_prompt_assist", {
|
|
4529
|
+
action,
|
|
4530
|
+
userId: data.userId,
|
|
4531
|
+
generalName: pending.generalName,
|
|
4532
|
+
});
|
|
4533
|
+
if (action === "cancel") {
|
|
4534
|
+
await this.retireInteractivePromptButtons(pending, pending.messageId ?? data.messageId, t("interactive.ignored", pending.instanceName));
|
|
4535
|
+
return true;
|
|
4536
|
+
}
|
|
4537
|
+
// Injecting into General while General itself is blocked would type into the
|
|
4538
|
+
// terminal prompt instead of the agent input. Fail safe and require attach.
|
|
4539
|
+
if (pending.instanceName === pending.generalName) {
|
|
4540
|
+
await this.retireInteractivePromptButtons(pending, pending.messageId ?? data.messageId, t("interactive.self_assist", pending.instanceName));
|
|
4541
|
+
return true;
|
|
4542
|
+
}
|
|
4543
|
+
await this.retireInteractivePromptButtons(pending, pending.messageId ?? data.messageId, t("interactive.confirmed", pending.instanceName));
|
|
4544
|
+
try {
|
|
4545
|
+
await this.deliverToInstance(pending.generalName, {
|
|
4546
|
+
type: "fleet_inbound",
|
|
4547
|
+
content: t("interactive.assist_request", pending.instanceName, this.interactivePromptLabel(pending.kind)),
|
|
4548
|
+
targetSession: pending.generalName,
|
|
4549
|
+
meta: {
|
|
4550
|
+
chat_id: pending.chatId,
|
|
4551
|
+
message_id: pending.messageId ?? data.messageId,
|
|
4552
|
+
user: "AgEnD",
|
|
4553
|
+
user_id: data.userId,
|
|
4554
|
+
ts: new Date().toISOString(),
|
|
4555
|
+
thread_id: pending.threadId ?? "",
|
|
4556
|
+
adapter_id: pending.adapterId,
|
|
4557
|
+
source: pending.adapter.type,
|
|
4558
|
+
},
|
|
4559
|
+
}, {
|
|
4560
|
+
// This is system-generated work, not a live user message. Queue behind
|
|
4561
|
+
// General's current turn so Kiro/Claude do not lose it while busy.
|
|
4562
|
+
isCrossInstance: true,
|
|
4563
|
+
});
|
|
4564
|
+
this.logger.info({ instanceName: pending.instanceName, generalName: pending.generalName }, "Interactive prompt assistance delivered to General");
|
|
4565
|
+
}
|
|
4566
|
+
catch (err) {
|
|
4567
|
+
this.logger.warn({ err, instanceName: pending.instanceName, generalName: pending.generalName }, "Failed to deliver interactive prompt assistance to General");
|
|
4568
|
+
await pending.adapter.editMessage(pending.chatId, pending.messageId ?? data.messageId, t("interactive.delivery_failed", pending.instanceName), pending.threadId);
|
|
4569
|
+
}
|
|
4570
|
+
return true;
|
|
4571
|
+
}
|
|
4398
4572
|
// ── Cancel button ────────────────────────────────────────────────────
|
|
4399
4573
|
// Sent after delivering a user message to an instance; clicking it (or
|
|
4400
4574
|
// /cancel) sends Escape to the instance's pane to interrupt generation.
|
|
@@ -6432,6 +6606,11 @@ When users create specialized instances, suggest these configurations:
|
|
|
6432
6606
|
for (const pending of this.pendingClassicStarts.values())
|
|
6433
6607
|
clearTimeout(pending.timer);
|
|
6434
6608
|
this.pendingClassicStarts.clear();
|
|
6609
|
+
for (const pending of this.pendingInteractivePromptAssists.values()) {
|
|
6610
|
+
if (pending.timer)
|
|
6611
|
+
clearTimeout(pending.timer);
|
|
6612
|
+
}
|
|
6613
|
+
this.pendingInteractivePromptAssists.clear();
|
|
6435
6614
|
this.topicArchiver.stop();
|
|
6436
6615
|
this.scheduler?.shutdown();
|
|
6437
6616
|
// Stop instances in parallel batches to avoid long sequential waits.
|