@songsid/agend 2.2.0-beta.6 → 2.2.0-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/agent-cli-instructions.md +6 -2
- package/dist/agent-cli.js +9 -5
- package/dist/agent-cli.js.map +1 -1
- package/dist/backend/muse.d.ts +26 -0
- package/dist/backend/muse.js +131 -0
- package/dist/backend/muse.js.map +1 -1
- package/dist/channel/adapters/discord.js +12 -1
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +6 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +3 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +3 -0
- package/dist/cross-instance-envelope.js +2 -0
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +6 -0
- package/dist/daemon.js +124 -54
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +27 -0
- package/dist/delivery-outbox.js +175 -6
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/fleet-manager.d.ts +28 -1
- package/dist/fleet-manager.js +203 -40
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +14 -1
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
- package/dist/human-delivery-receipts.d.ts +83 -0
- package/dist/human-delivery-receipts.js +194 -0
- package/dist/human-delivery-receipts.js.map +1 -0
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +1 -1
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +8 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.d.ts +3 -0
- package/dist/outbound-handlers.js +20 -4
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +15 -13
- package/dist/outbound-schemas.js +5 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-link-progress.d.ts +1 -1
- package/dist/public-link-progress.js +8 -2
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-preview.d.ts +2 -1
- package/dist/public-preview.js +8 -5
- package/dist/public-preview.js.map +1 -1
- package/dist/public-web-link.d.ts +4 -1
- package/dist/public-web-link.js +27 -10
- package/dist/public-web-link.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +3 -0
- package/dist/reply-buttons-controller.js +3 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/topic-commands.d.ts +3 -2
- package/dist/topic-commands.js +14 -4
- package/dist/topic-commands.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +7 -15
- package/dist/tunnel/cloudflared.js +36 -1
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/types.d.ts +5 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/ui/chat-strings.js +8 -2
- package/dist/ui/chat-thread.js +24 -4
- package/dist/ui/chat-time.js +80 -0
- package/dist/ui/preview-panel.js +7 -1
- package/dist/ui/preview.js +5 -1
- package/dist/ui/shared/app.css +4 -1
- package/dist/web-api.d.ts +2 -0
- package/dist/web-api.js +8 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-commands.d.ts +2 -2
- package/dist/web-commands.js +3 -2
- package/dist/web-commands.js.map +1 -1
- package/dist/web-reply-files.d.ts +45 -0
- package/dist/web-reply-files.js +223 -0
- package/dist/web-reply-files.js.map +1 -0
- package/dist/web-upload.d.ts +17 -4
- package/dist/web-upload.js +64 -13
- package/dist/web-upload.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type HumanOrigin } from "./human-delivery-receipts.js";
|
|
1
2
|
import { SettingsExecution } from "./settings-transaction.js";
|
|
2
3
|
import type { LoginCodeOwner } from "./web-login.js";
|
|
3
4
|
import { type ProfileTicket } from "./runtime-cpu-profile.js";
|
|
@@ -63,6 +64,7 @@ import { type AgentEndpointContext } from "./agent-endpoint.js";
|
|
|
63
64
|
import { ClassicChannelManager } from "./classic-channel-manager.js";
|
|
64
65
|
import { type ExplicitInstanceRemoval } from "./instance-removal.js";
|
|
65
66
|
import type { InstanceState, InteractionSnapshot } from "./backend/types.js";
|
|
67
|
+
import { type Daemon } from "./daemon.js";
|
|
66
68
|
import { WakeCoordinator } from "./wake-coordinator.js";
|
|
67
69
|
import { TargetQueueWorker } from "./target-queue-worker.js";
|
|
68
70
|
import { resolveDeliveryWorkerMode } from "./types.js";
|
|
@@ -218,7 +220,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
218
220
|
private replyObligationTimer;
|
|
219
221
|
private readonly activeDurableTargets;
|
|
220
222
|
/** Live view of lifecycle.daemons — used throughout; not deprecated. */
|
|
221
|
-
get daemons(): Map<string,
|
|
223
|
+
get daemons(): Map<string, Daemon>;
|
|
222
224
|
fleetConfig: FleetConfig | null;
|
|
223
225
|
private rawFleetConfig;
|
|
224
226
|
private rawFleetDocument;
|
|
@@ -470,6 +472,10 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
470
472
|
private webChatStore;
|
|
471
473
|
/** Uploaded files and the files the dashboard may fetch back (uploads, reply attachments). */
|
|
472
474
|
readonly webFiles: WebFileLedger;
|
|
475
|
+
/** #1770: per instance, its last web reply still copying its files — the next one is shown after it, in order. */
|
|
476
|
+
private readonly webReplyOrder;
|
|
477
|
+
/** #1770: an instance's web-files byte cap (a field, so a test can make it small). */
|
|
478
|
+
private webReplyMaxBytes;
|
|
473
479
|
/** #1682: the people seen on inbound messages (id → name), for the web chat's mention pills; bounded, in memory. */
|
|
474
480
|
readonly seenUsers: SeenUsers;
|
|
475
481
|
private webPrefsStore?;
|
|
@@ -555,10 +561,13 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
555
561
|
kind: string;
|
|
556
562
|
correlationId: string;
|
|
557
563
|
payload: Record<string, unknown>;
|
|
564
|
+
forwards?: readonly string[];
|
|
565
|
+
requestedKind?: string;
|
|
558
566
|
}): {
|
|
559
567
|
deliveryId: string;
|
|
560
568
|
state: string;
|
|
561
569
|
duplicate: boolean;
|
|
570
|
+
kind: string;
|
|
562
571
|
};
|
|
563
572
|
queryDurableDeliveryStatus(callerInstance: string, selector: DeliveryStatusSelector): DeliveryStatusPage;
|
|
564
573
|
/** #1641: why a target is not taking queued work right now, or null when nothing known holds it. */
|
|
@@ -902,6 +911,17 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
902
911
|
hasPendingIdleGatedDelivery(instanceName: string): boolean;
|
|
903
912
|
/** The running launch's own steer capability (#1405); undefined for a backend without one, or no running Daemon. */
|
|
904
913
|
instanceLaunchSupportsSteer(instanceName: string): boolean | undefined;
|
|
914
|
+
private humanReceiptDaemons;
|
|
915
|
+
/** Cached FM + lifecycle + Daemon + tmux owner; unknown ownership grants nothing. */
|
|
916
|
+
private humanReceiptOwner;
|
|
917
|
+
private listenForHumanReceipt;
|
|
918
|
+
/** Evidence-only admission. Bots/system/agent payloads have no trusted origin. */
|
|
919
|
+
private humanReceiptPayload;
|
|
920
|
+
/** Bind at the actual IPC send, after all wake/reconnect waits. No human retry lane. */
|
|
921
|
+
private bindHumanReceiptPayload;
|
|
922
|
+
deliverHumanToInstance(instanceName: string, payload: Record<string, unknown>, origin: HumanOrigin | undefined, originalBody: string, options?: DeliveryOptions): Promise<boolean | void>;
|
|
923
|
+
/** Synchronous steer/btw handoff, using the same evidence and owner binding. */
|
|
924
|
+
sendHumanControl(instanceName: string, payload: Record<string, unknown>, origin: HumanOrigin | undefined, originalBody: string): boolean;
|
|
905
925
|
/** Single delivery facade: wake paused CLIs and serialize non-user work behind idle. */
|
|
906
926
|
deliverToInstance(instanceName: string, payload: Record<string, unknown>, options?: DeliveryOptions): Promise<boolean | void>;
|
|
907
927
|
/**
|
|
@@ -1580,7 +1600,14 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1580
1600
|
private isWebOnlyFleet;
|
|
1581
1601
|
/** On a fleet with no chat platform, the dashboard is where an instance-health prompt is posted (#1307 item 6). */
|
|
1582
1602
|
private webOnlyPromptPlace;
|
|
1603
|
+
/** Resolves when the reply is in the web chat (#1770: after its files are copied); the caller need not wait. */
|
|
1583
1604
|
private afterReplyRouted;
|
|
1605
|
+
/**
|
|
1606
|
+
* #1770: a reply's files as the web chat shows them. Each is copied into the instance's web-files/ and served from
|
|
1607
|
+
* the copy. One the copy refuses (a symlink, not a regular file, over 50 MB, a state file, missing) is not shown;
|
|
1608
|
+
* one that could not be copied (I/O) is served from the agent's own path, as before — until the fleet restarts.
|
|
1609
|
+
*/
|
|
1610
|
+
private webReplyAttachments;
|
|
1584
1611
|
private handleScheduleTrigger;
|
|
1585
1612
|
/**
|
|
1586
1613
|
* The adapter that can actually post into a chat, found by its group.
|
package/dist/fleet-manager.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { humanOwnerObjectId, webHumanOrigin } from "./human-delivery-receipts.js";
|
|
1
2
|
import { SettingsBaselines } from "./settings-baseline.js";
|
|
2
3
|
import { SettingsConfirmationStore } from "./settings-confirmation.js";
|
|
3
4
|
import { SettingsHttpConfirmation } from "./settings-http-confirmation.js";
|
|
@@ -138,6 +139,7 @@ import { WebPrefsStore } from "./web-prefs.js";
|
|
|
138
139
|
import { ReplyButtonStore, parseReplyButtons, replyButtonClickText, replyButtonsFallbackText, REPLY_BUTTON_PREFIX } from "./reply-buttons.js";
|
|
139
140
|
import { ReplyButtonsController } from "./reply-buttons-controller.js";
|
|
140
141
|
import { publicAttachment, sweepOrphanedUploads, WebFileLedger } from "./web-upload.js";
|
|
142
|
+
import { REPLY_FILES_MAX_BYTES, snapshotReplyFile, sweepReplySnapshots, webFilesDir } from "./web-reply-files.js";
|
|
141
143
|
import { handleViewRequest, isViewPath, profileIdentities, resolveInstanceIdentity } from "./view-api.js";
|
|
142
144
|
import { downloadStickerImage } from "./channel/sticker-download.js";
|
|
143
145
|
import { WebImageCache, imageMimeForFile, WEB_IMAGE_MAX_AGE_MS, WEB_IMAGE_MAX_BYTES, WEB_IMAGE_RETRY_MS } from "./web-images.js";
|
|
@@ -789,6 +791,10 @@ export class FleetManager {
|
|
|
789
791
|
webChatStore = null;
|
|
790
792
|
/** Uploaded files and the files the dashboard may fetch back (uploads, reply attachments). */
|
|
791
793
|
webFiles = new WebFileLedger();
|
|
794
|
+
/** #1770: per instance, its last web reply still copying its files — the next one is shown after it, in order. */
|
|
795
|
+
webReplyOrder = new Map();
|
|
796
|
+
/** #1770: an instance's web-files byte cap (a field, so a test can make it small). */
|
|
797
|
+
webReplyMaxBytes = REPLY_FILES_MAX_BYTES;
|
|
792
798
|
/** #1682: the people seen on inbound messages (id → name), for the web chat's mention pills; bounded, in memory. */
|
|
793
799
|
seenUsers = new SeenUsers();
|
|
794
800
|
webPrefsStore;
|
|
@@ -1189,7 +1195,7 @@ export class FleetManager {
|
|
|
1189
1195
|
const reactions = this.pendingReactionsMeta(set.instance);
|
|
1190
1196
|
let accepted = false;
|
|
1191
1197
|
try {
|
|
1192
|
-
accepted = (await this.
|
|
1198
|
+
accepted = (await this.deliverHumanToInstance(set.instance, {
|
|
1193
1199
|
type: "fleet_inbound",
|
|
1194
1200
|
content,
|
|
1195
1201
|
targetSession: set.instance,
|
|
@@ -1198,7 +1204,7 @@ export class FleetManager {
|
|
|
1198
1204
|
thread_id: set.threadId, adapter_id: inbound.adapterId, source: inbound.source,
|
|
1199
1205
|
...reactions.meta,
|
|
1200
1206
|
},
|
|
1201
|
-
})) !== false;
|
|
1207
|
+
}, by.humanOrigin, button.label)) !== false;
|
|
1202
1208
|
}
|
|
1203
1209
|
finally {
|
|
1204
1210
|
// Both false and throw reopen the choice in the controller. Finish the
|
|
@@ -1317,6 +1323,7 @@ export class FleetManager {
|
|
|
1317
1323
|
deliveryId: admitted.delivery.deliveryId,
|
|
1318
1324
|
state: admitted.delivery.state,
|
|
1319
1325
|
duplicate: !admitted.inserted,
|
|
1326
|
+
kind: admitted.delivery.kind,
|
|
1320
1327
|
};
|
|
1321
1328
|
}
|
|
1322
1329
|
queryDurableDeliveryStatus(callerInstance, selector) {
|
|
@@ -2989,7 +2996,7 @@ export class FleetManager {
|
|
|
2989
2996
|
const queued = this.ipcWaitTails.get(instanceName);
|
|
2990
2997
|
if (!queued) {
|
|
2991
2998
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
2992
|
-
if (ipc?.connected && ipc.send(payload))
|
|
2999
|
+
if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
|
|
2993
3000
|
return true;
|
|
2994
3001
|
}
|
|
2995
3002
|
const attempt = (queued ?? Promise.resolve())
|
|
@@ -3024,7 +3031,7 @@ export class FleetManager {
|
|
|
3024
3031
|
// Re-read every round: a reconnect replaces the IpcClient object entirely,
|
|
3025
3032
|
// so a cached reference would stay dead forever.
|
|
3026
3033
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
3027
|
-
if (ipc?.connected && ipc.send(payload))
|
|
3034
|
+
if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
|
|
3028
3035
|
return true;
|
|
3029
3036
|
if (Date.now() >= deadline) {
|
|
3030
3037
|
throw new Error(`Instance '${instanceName}' IPC is unavailable`);
|
|
@@ -3242,6 +3249,101 @@ export class FleetManager {
|
|
|
3242
3249
|
instanceLaunchSupportsSteer(instanceName) {
|
|
3243
3250
|
return this.daemons.get(instanceName)?.launchSupportsSteer();
|
|
3244
3251
|
}
|
|
3252
|
+
humanReceiptDaemons = new WeakSet();
|
|
3253
|
+
/** Cached FM + lifecycle + Daemon + tmux owner; unknown ownership grants nothing. */
|
|
3254
|
+
humanReceiptOwner(instanceName, daemon = this.daemons.get(instanceName)) {
|
|
3255
|
+
try {
|
|
3256
|
+
if (this.shuttingDown || !daemon || this.daemons.get(instanceName) !== daemon)
|
|
3257
|
+
return null;
|
|
3258
|
+
const epoch = this.lifecycle.epochOf(instanceName);
|
|
3259
|
+
const owner = daemon.captureHumanReceiptOwner();
|
|
3260
|
+
if (!Number.isSafeInteger(epoch) || !owner)
|
|
3261
|
+
return null;
|
|
3262
|
+
return JSON.stringify([this.managerBootId, epoch, this.getDeliveryEpoch(instanceName), humanOwnerObjectId(daemon), owner]);
|
|
3263
|
+
}
|
|
3264
|
+
catch {
|
|
3265
|
+
return null;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
listenForHumanReceipt(instanceName, daemon) {
|
|
3269
|
+
// Lazy initialization also supports narrow prototype-based integration fixtures.
|
|
3270
|
+
this.humanReceiptDaemons ??= new WeakSet();
|
|
3271
|
+
if (this.humanReceiptDaemons.has(daemon))
|
|
3272
|
+
return;
|
|
3273
|
+
this.humanReceiptDaemons.add(daemon);
|
|
3274
|
+
daemon.on("human_receipt_result", (result) => {
|
|
3275
|
+
try {
|
|
3276
|
+
this.deliveryOutbox?.settleHumanReceipt(result.receipt_id, instanceName, result.owner_key, result.proof, this.humanReceiptOwner(instanceName, daemon));
|
|
3277
|
+
}
|
|
3278
|
+
catch (err) {
|
|
3279
|
+
this.logger.error({ err, instanceName }, "Could not persist human delivery evidence");
|
|
3280
|
+
}
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
3283
|
+
/** Evidence-only admission. Bots/system/agent payloads have no trusted origin. */
|
|
3284
|
+
humanReceiptPayload(instanceName, payload, origin, originalBody) {
|
|
3285
|
+
if (!origin || !this.deliveryOutbox)
|
|
3286
|
+
return payload;
|
|
3287
|
+
const row = this.deliveryOutbox.createHumanReceipt({ targetInstance: instanceName, originalBody,
|
|
3288
|
+
origin: { ...origin }, inputKind: String(payload.type), managerBootId: this.managerBootId });
|
|
3289
|
+
return { ...payload, meta: { ...(payload.meta ?? {}),
|
|
3290
|
+
human_receipt_id: row.receipt_id, verification_id: row.message_id } };
|
|
3291
|
+
}
|
|
3292
|
+
/** Bind at the actual IPC send, after all wake/reconnect waits. No human retry lane. */
|
|
3293
|
+
bindHumanReceiptPayload(instanceName, payload) {
|
|
3294
|
+
const meta = payload.meta;
|
|
3295
|
+
if (!meta?.human_receipt_id || !this.deliveryOutbox)
|
|
3296
|
+
return payload;
|
|
3297
|
+
const daemon = this.daemons.get(instanceName);
|
|
3298
|
+
const owner = this.humanReceiptOwner(instanceName, daemon);
|
|
3299
|
+
if (!daemon || !owner) {
|
|
3300
|
+
this.deliveryOutbox.markHumanReceiptUncertain(meta.human_receipt_id, instanceName);
|
|
3301
|
+
return payload;
|
|
3302
|
+
}
|
|
3303
|
+
this.listenForHumanReceipt(instanceName, daemon);
|
|
3304
|
+
const daemonOwner = daemon.captureHumanReceiptOwner();
|
|
3305
|
+
const wire = daemon.humanReceiptWire(String(payload.content ?? ""), meta, String(payload.type));
|
|
3306
|
+
const row = this.deliveryOutbox.bindHumanReceipt(meta.human_receipt_id, instanceName, owner, wire, this.managerBootId);
|
|
3307
|
+
if (!daemonOwner || this.humanReceiptOwner(instanceName, daemon) !== owner)
|
|
3308
|
+
throw new Error("Human delivery ownership changed");
|
|
3309
|
+
// /raw retains exact bytes and never acquires a positive verification capability.
|
|
3310
|
+
if (payload.type === "raw_paste" || String(payload.content ?? "").startsWith("/raw ")) {
|
|
3311
|
+
this.deliveryOutbox.markHumanReceiptUncertain(row.receipt_id, instanceName);
|
|
3312
|
+
return payload;
|
|
3313
|
+
}
|
|
3314
|
+
return { ...payload, meta: { ...meta, human_owner_key: owner, human_daemon_owner: daemonOwner, human_wire_sha256: row.wire_sha256 } };
|
|
3315
|
+
}
|
|
3316
|
+
async deliverHumanToInstance(instanceName, payload, origin, originalBody, options) {
|
|
3317
|
+
const admitted = this.humanReceiptPayload(instanceName, payload, origin, originalBody);
|
|
3318
|
+
const id = admitted.meta?.human_receipt_id;
|
|
3319
|
+
let sent = false;
|
|
3320
|
+
try {
|
|
3321
|
+
const result = options ? await this.deliverToInstance(instanceName, admitted, options) : await this.deliverToInstance(instanceName, admitted);
|
|
3322
|
+
sent = result !== false;
|
|
3323
|
+
return result;
|
|
3324
|
+
}
|
|
3325
|
+
finally {
|
|
3326
|
+
if (!sent && id)
|
|
3327
|
+
this.deliveryOutbox?.markHumanReceiptUncertain(id, instanceName);
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
/** Synchronous steer/btw handoff, using the same evidence and owner binding. */
|
|
3331
|
+
sendHumanControl(instanceName, payload, origin, originalBody) {
|
|
3332
|
+
const ipc = this.instanceIpcClients.get(instanceName);
|
|
3333
|
+
if (this.shuttingDown || !ipc?.connected)
|
|
3334
|
+
return false;
|
|
3335
|
+
const admitted = this.humanReceiptPayload(instanceName, payload, origin, originalBody);
|
|
3336
|
+
const id = admitted.meta?.human_receipt_id;
|
|
3337
|
+
let sent = false;
|
|
3338
|
+
try {
|
|
3339
|
+
sent = ipc.send(this.bindHumanReceiptPayload(instanceName, admitted));
|
|
3340
|
+
return sent;
|
|
3341
|
+
}
|
|
3342
|
+
finally {
|
|
3343
|
+
if (!sent && id)
|
|
3344
|
+
this.deliveryOutbox?.markHumanReceiptUncertain(id, instanceName);
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3245
3347
|
/** Single delivery facade: wake paused CLIs and serialize non-user work behind idle. */
|
|
3246
3348
|
async deliverToInstance(instanceName, payload, options = {}) {
|
|
3247
3349
|
const deliveryEpoch = this.getDeliveryEpoch(instanceName);
|
|
@@ -3434,7 +3536,7 @@ export class FleetManager {
|
|
|
3434
3536
|
userId: data.userId,
|
|
3435
3537
|
username,
|
|
3436
3538
|
source: "discord",
|
|
3437
|
-
timestamp: new Date(),
|
|
3539
|
+
timestamp: new Date(), adapterId, humanOrigin: data.humanOrigin, humanOriginalBody: text,
|
|
3438
3540
|
});
|
|
3439
3541
|
}
|
|
3440
3542
|
else if (data.command === "save") {
|
|
@@ -3579,7 +3681,8 @@ export class FleetManager {
|
|
|
3579
3681
|
await data.respond(t("classic.no_agent"));
|
|
3580
3682
|
return;
|
|
3581
3683
|
}
|
|
3582
|
-
const
|
|
3684
|
+
const originalSteerText = String(data.options?.message ?? "");
|
|
3685
|
+
const steerText = originalSteerText.trim();
|
|
3583
3686
|
if (!steerText) {
|
|
3584
3687
|
await data.respond(t("steer.usage"));
|
|
3585
3688
|
return;
|
|
@@ -3589,7 +3692,7 @@ export class FleetManager {
|
|
|
3589
3692
|
// rerouting the instance's replies to the slash context.
|
|
3590
3693
|
const result = this.topicCommands.sendSteer(name, steerText, {
|
|
3591
3694
|
chatId: "", messageId: "", username: data.username ?? "user",
|
|
3592
|
-
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord",
|
|
3695
|
+
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord", humanOrigin: data.humanOrigin, humanOriginalBody: originalSteerText,
|
|
3593
3696
|
});
|
|
3594
3697
|
await data.respond(result);
|
|
3595
3698
|
}
|
|
@@ -3599,14 +3702,15 @@ export class FleetManager {
|
|
|
3599
3702
|
await data.respond(t("classic.no_agent"));
|
|
3600
3703
|
return;
|
|
3601
3704
|
}
|
|
3602
|
-
const
|
|
3705
|
+
const originalBtwText = String(data.options?.message ?? "");
|
|
3706
|
+
const btwText = originalBtwText.trim();
|
|
3603
3707
|
if (!btwText) {
|
|
3604
3708
|
await data.respond(t("btw.usage"));
|
|
3605
3709
|
return;
|
|
3606
3710
|
}
|
|
3607
3711
|
const result = this.topicCommands.sendBtw(name, btwText, {
|
|
3608
3712
|
chatId: "", messageId: "", username: data.username ?? "user",
|
|
3609
|
-
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord",
|
|
3713
|
+
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord", humanOrigin: data.humanOrigin, humanOriginalBody: originalBtwText,
|
|
3610
3714
|
});
|
|
3611
3715
|
await data.respond(result);
|
|
3612
3716
|
}
|
|
@@ -4959,13 +5063,13 @@ export class FleetManager {
|
|
|
4959
5063
|
if (result && !result.durable)
|
|
4960
5064
|
this.logger.warn("Public session revocation could not be saved; exposure remains closed");
|
|
4961
5065
|
},
|
|
4962
|
-
log: (event, exposureId) => this.logger.info({ event, exposureId }, "Public web link"),
|
|
5066
|
+
log: (event, exposureId, detail) => this.logger.info({ event, exposureId, ...(detail ? { detail } : {}) }, "Public web link"),
|
|
4963
5067
|
createPreview: ({ id, expiresAt, binary, protocol, dashboardOrigin, isCurrent }) => {
|
|
4964
5068
|
if (!publicPreviewWanted(this.fleetConfig?.web))
|
|
4965
5069
|
return null;
|
|
4966
5070
|
return new PublicPreview({ exposureId: id, expiresAt, dashboardOrigin, isCurrent, manager: this.getPreviewTunnel(),
|
|
4967
5071
|
provider: () => new CloudflaredProvider({ binaryName: binary, protocol }),
|
|
4968
|
-
log: event => this.logger.info({ event, exposureId: id }, "Public web link") });
|
|
5072
|
+
log: (event, detail) => this.logger.info({ event, exposureId: id, ...(detail ? { detail } : {}) }, "Public web link") });
|
|
4969
5073
|
},
|
|
4970
5074
|
onCleanupUnconfirmed: (result, owner) => {
|
|
4971
5075
|
this.logger.warn({ pid: result.confirmed ? null : result.pid }, "Public web access closed; tunnel cleanup unconfirmed");
|
|
@@ -7509,6 +7613,7 @@ export class FleetManager {
|
|
|
7509
7613
|
});
|
|
7510
7614
|
}
|
|
7511
7615
|
async handleInboundMessage(msg) {
|
|
7616
|
+
msg = { ...msg, humanOriginalBody: msg.humanOriginalBody ?? msg.text, humanOrigin: msg.isBotMessage ? undefined : msg.humanOrigin && { ...msg.humanOrigin } };
|
|
7512
7617
|
if (msg.isBotMessage)
|
|
7513
7618
|
return withSpawnHoldReply(undefined, () => this.handleInboundMessageCore(msg));
|
|
7514
7619
|
return withSpawnHoldReply(text => this.inboundNoticeAdapter(msg)?.sendText(msg.chatId, text, { threadId: msg.threadId }) ?? Promise.resolve(), () => this.handleInboundMessageCore(msg));
|
|
@@ -7967,7 +8072,7 @@ export class FleetManager {
|
|
|
7967
8072
|
const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, generalInstance);
|
|
7968
8073
|
const generalReactions = this.pendingReactionsMeta(generalInstance);
|
|
7969
8074
|
try {
|
|
7970
|
-
await this.
|
|
8075
|
+
await this.deliverHumanToInstance(generalInstance, {
|
|
7971
8076
|
type: "fleet_inbound",
|
|
7972
8077
|
content: text,
|
|
7973
8078
|
targetSession: generalInstance,
|
|
@@ -7988,7 +8093,7 @@ export class FleetManager {
|
|
|
7988
8093
|
...generalReactions.meta,
|
|
7989
8094
|
...extraMeta,
|
|
7990
8095
|
},
|
|
7991
|
-
});
|
|
8096
|
+
}, msg.humanOrigin, msg.humanOriginalBody ?? msg.text);
|
|
7992
8097
|
generalReactions.consume();
|
|
7993
8098
|
this.afterUserInboundDelivered(generalInstance, msg);
|
|
7994
8099
|
this.logger.info(`${msg.username} → ${generalInstance}: ${(text ?? "").slice(0, 100)}`);
|
|
@@ -8068,7 +8173,7 @@ export class FleetManager {
|
|
|
8068
8173
|
const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, instanceName);
|
|
8069
8174
|
const reactions = this.pendingReactionsMeta(instanceName);
|
|
8070
8175
|
try {
|
|
8071
|
-
await this.
|
|
8176
|
+
await this.deliverHumanToInstance(instanceName, {
|
|
8072
8177
|
type: "fleet_inbound",
|
|
8073
8178
|
content: text,
|
|
8074
8179
|
targetSession: instanceName, // Channel messages → instance's own session
|
|
@@ -8087,7 +8192,7 @@ export class FleetManager {
|
|
|
8087
8192
|
...reactions.meta,
|
|
8088
8193
|
...extraMeta,
|
|
8089
8194
|
},
|
|
8090
|
-
});
|
|
8195
|
+
}, msg.humanOrigin, msg.humanOriginalBody ?? msg.text);
|
|
8091
8196
|
// Only after the message actually went out. A failed delivery keeps the
|
|
8092
8197
|
// reactions queued for the retry / the next message.
|
|
8093
8198
|
reactions.consume();
|
|
@@ -8395,7 +8500,7 @@ export class FleetManager {
|
|
|
8395
8500
|
// an agent cannot invent it to suppress the normal completion marker.
|
|
8396
8501
|
if (!error && result != null && msg.statusOnly !== true) {
|
|
8397
8502
|
try {
|
|
8398
|
-
this.afterReplyRouted(instanceName, args, senderSessionName, buttonsView);
|
|
8503
|
+
void this.afterReplyRouted(instanceName, args, senderSessionName, buttonsView);
|
|
8399
8504
|
}
|
|
8400
8505
|
catch (err) {
|
|
8401
8506
|
this.logger.warn({ err, instanceName }, "Reply delivered but post-delivery bookkeeping failed");
|
|
@@ -8448,6 +8553,7 @@ export class FleetManager {
|
|
|
8448
8553
|
webOnlyPromptPlace() {
|
|
8449
8554
|
return this.worlds.size === 0 && this.isWebOnlyFleet() ? { adapter: WEB_ONLY_PROMPT_SINK, adapterId: "web", chatId: "web" } : null;
|
|
8450
8555
|
}
|
|
8556
|
+
/** Resolves when the reply is in the web chat (#1770: after its files are copied); the caller need not wait. */
|
|
8451
8557
|
afterReplyRouted(instanceName, args, senderSessionName, buttons = null) {
|
|
8452
8558
|
// A reply is NOT proof the turn is over (#410) — but it is not proof of
|
|
8453
8559
|
// more work either. Split the difference: an instance that is clearly
|
|
@@ -8465,27 +8571,74 @@ export class FleetManager {
|
|
|
8465
8571
|
this.reactDone(instanceName);
|
|
8466
8572
|
const replyTo = this.lastInboundUser.get(instanceName) ?? "user";
|
|
8467
8573
|
this.logger.info(`${instanceName} → ${replyTo}: ${(args.text ?? "").slice(0, 100)}`);
|
|
8468
|
-
// Files the agent attached are shown in the web chat too: registered for fetching by id (the path
|
|
8469
|
-
// already passed the reply tool's sendability check, and is resolved once more here), never by path.
|
|
8470
|
-
const replyFiles = Array.isArray(args.files) ? args.files.filter((f) => typeof f === "string") : [];
|
|
8471
|
-
const attachments = replyFiles
|
|
8472
|
-
.map(path => this.webFiles.registerServed({ path, instance: instanceName }))
|
|
8473
|
-
.filter((f) => f !== null)
|
|
8474
|
-
.map(publicAttachment);
|
|
8475
|
-
// The one place a delivered agent reply reaches the web chat: the server marks it `agent` (#1306) — the only
|
|
8476
|
-
// role that may get HTML preview cards. Never inferred from the sender name or the text.
|
|
8477
|
-
this.emitSseEvent("message", {
|
|
8478
|
-
instance: instanceName, sender: senderSessionName ?? instanceName, role: "agent",
|
|
8479
|
-
text: (args.text ?? "").slice(0, WEB_CHAT_TEXT_MAX),
|
|
8480
|
-
ts: new Date().toISOString(),
|
|
8481
|
-
...(attachments.length ? { attachments } : {}),
|
|
8482
|
-
...(buttons ? { buttons } : {}), // #1266
|
|
8483
|
-
});
|
|
8484
8574
|
// Log bot reply to classic instance chat-log
|
|
8485
8575
|
const isClassic = this.classicChannels?.getChannelIdByInstance(instanceName) !== undefined;
|
|
8486
8576
|
if (isClassic) {
|
|
8487
8577
|
ClassicChannelManager.logMessage(instanceName, "bot", args.text ?? "", new Date());
|
|
8488
8578
|
}
|
|
8579
|
+
// Files the agent attached are shown in the web chat too, fetched by id, never by path. #1770: each is copied
|
|
8580
|
+
// first (web-reply-files.ts), and the copy is what the chat serves and keeps — so the message is shown once the
|
|
8581
|
+
// copies are done, and an instance's replies wait for the one before them: they reach the chat in order.
|
|
8582
|
+
const replyFiles = Array.isArray(args.files) ? args.files.filter((f) => typeof f === "string") : [];
|
|
8583
|
+
const ts = new Date().toISOString();
|
|
8584
|
+
const show = (attachments) => {
|
|
8585
|
+
// The one place a delivered agent reply reaches the web chat: the server marks it `agent` (#1306) — the only
|
|
8586
|
+
// role that may get HTML preview cards. Never inferred from the sender name or the text.
|
|
8587
|
+
this.emitSseEvent("message", {
|
|
8588
|
+
instance: instanceName, sender: senderSessionName ?? instanceName, role: "agent",
|
|
8589
|
+
text: (args.text ?? "").slice(0, WEB_CHAT_TEXT_MAX),
|
|
8590
|
+
ts,
|
|
8591
|
+
...(attachments.length ? { attachments } : {}),
|
|
8592
|
+
...(buttons ? { buttons } : {}), // #1266
|
|
8593
|
+
});
|
|
8594
|
+
};
|
|
8595
|
+
const before = this.webReplyOrder.get(instanceName);
|
|
8596
|
+
if (!replyFiles.length && !before) {
|
|
8597
|
+
show([]);
|
|
8598
|
+
return Promise.resolve();
|
|
8599
|
+
}
|
|
8600
|
+
const done = (before ?? Promise.resolve())
|
|
8601
|
+
.then(() => this.webReplyAttachments(instanceName, replyFiles))
|
|
8602
|
+
.catch((err) => { this.logger.warn({ err, instanceName }, "Reply files could not be prepared for the web chat"); return []; })
|
|
8603
|
+
.then((attachments) => { try {
|
|
8604
|
+
show(attachments);
|
|
8605
|
+
}
|
|
8606
|
+
catch (err) {
|
|
8607
|
+
this.logger.warn({ err, instanceName }, "Reply could not be shown in the web chat");
|
|
8608
|
+
} })
|
|
8609
|
+
.finally(() => { if (this.webReplyOrder.get(instanceName) === done)
|
|
8610
|
+
this.webReplyOrder.delete(instanceName); });
|
|
8611
|
+
this.webReplyOrder.set(instanceName, done);
|
|
8612
|
+
return done;
|
|
8613
|
+
}
|
|
8614
|
+
/**
|
|
8615
|
+
* #1770: a reply's files as the web chat shows them. Each is copied into the instance's web-files/ and served from
|
|
8616
|
+
* the copy. One the copy refuses (a symlink, not a regular file, over 50 MB, a state file, missing) is not shown;
|
|
8617
|
+
* one that could not be copied (I/O) is served from the agent's own path, as before — until the fleet restarts.
|
|
8618
|
+
*/
|
|
8619
|
+
async webReplyAttachments(instanceName, paths) {
|
|
8620
|
+
const home = getAgendHome();
|
|
8621
|
+
const dir = webFilesDir(home, instanceName);
|
|
8622
|
+
const out = [];
|
|
8623
|
+
for (const path of paths) {
|
|
8624
|
+
const id = randomBytes(16).toString("hex");
|
|
8625
|
+
const got = dir ? await snapshotReplyFile(path, dir, id, { stateDir: home, maxDirBytes: this.webReplyMaxBytes }) : null;
|
|
8626
|
+
if (got && "evicted" in got && got.evicted.length)
|
|
8627
|
+
this.webFiles.dropServed(got.evicted);
|
|
8628
|
+
let file = null;
|
|
8629
|
+
if (got?.ok)
|
|
8630
|
+
file = this.webFiles.registerServed({ id, path: got.path, name: basename(path), instance: instanceName, snapshot: true });
|
|
8631
|
+
else if (got?.refused)
|
|
8632
|
+
this.logger.info({ instanceName, file: basename(path), reason: got.reason }, "Reply file not shown in the web chat");
|
|
8633
|
+
else {
|
|
8634
|
+
if (got)
|
|
8635
|
+
this.logger.warn({ instanceName, file: basename(path), reason: got.reason }, "Reply file could not be copied for the web chat; served from its own path until a restart");
|
|
8636
|
+
file = this.webFiles.registerServed({ path, instance: instanceName });
|
|
8637
|
+
}
|
|
8638
|
+
if (file)
|
|
8639
|
+
out.push(publicAttachment(file));
|
|
8640
|
+
}
|
|
8641
|
+
return out;
|
|
8489
8642
|
}
|
|
8490
8643
|
// ===================== Scheduler =====================
|
|
8491
8644
|
async handleScheduleTrigger(schedule, stableRunId = randomUUID(), retry) {
|
|
@@ -12254,7 +12407,7 @@ export class FleetManager {
|
|
|
12254
12407
|
const topicId = this.fleetConfig?.instances[instance]?.topic_id;
|
|
12255
12408
|
return {
|
|
12256
12409
|
chatId: this.getGroupIdForInstance(instance) || "", messageId: newWebMessageId(), username: "web-user", userId: "web-user",
|
|
12257
|
-
threadId: topicId != null ? String(topicId) : undefined, adapterId: this.getAdapterForInstance(instance)?.id ?? "", source: "web",
|
|
12410
|
+
threadId: topicId != null ? String(topicId) : undefined, adapterId: this.getAdapterForInstance(instance)?.id ?? "", source: "web", humanOrigin: webHumanOrigin(true),
|
|
12258
12411
|
};
|
|
12259
12412
|
};
|
|
12260
12413
|
const choicesFromCache = (instance) => {
|
|
@@ -12285,8 +12438,8 @@ export class FleetManager {
|
|
|
12285
12438
|
return { current, options: levels.map(l => ({ id: l, label: this.effortChoiceLabel(l, current) })) };
|
|
12286
12439
|
},
|
|
12287
12440
|
cancel: (instance) => this.cancelInstance(instance),
|
|
12288
|
-
steer: (instance, text) => this.topicCommands.sendSteer(instance, text, webMeta(instance)),
|
|
12289
|
-
btw: (instance, text) => this.topicCommands.sendBtw(instance, text, webMeta(instance)),
|
|
12441
|
+
steer: (instance, text, originalBody) => this.topicCommands.sendSteer(instance, text, { ...webMeta(instance), humanOriginalBody: originalBody }),
|
|
12442
|
+
btw: (instance, text, originalBody) => this.topicCommands.sendBtw(instance, text, { ...webMeta(instance), humanOriginalBody: originalBody }),
|
|
12290
12443
|
pauseWake: (instance, action) => this.topicCommands.runPauseWake(instance, action),
|
|
12291
12444
|
save: (instance, filename) => this.topicCommands.sendSave(instance, filename),
|
|
12292
12445
|
clearAsk: (instance) => {
|
|
@@ -14082,16 +14235,26 @@ export class FleetManager {
|
|
|
14082
14235
|
try {
|
|
14083
14236
|
const store = new WebChatDiskStore({ home: getAgendHome(), logger: this.logger, ownerOf: (name) => this.webChatOwner(name) });
|
|
14084
14237
|
const names = new Set([...Object.keys(fleet.instances ?? {}), ...(this.classicChannels?.getAll().map(c => c.instanceName) ?? [])]);
|
|
14085
|
-
// A kept message's files are served again under their ids only from the instance's own inbox
|
|
14086
|
-
// checks (web-upload.ts restoreServed); they leave the ledger with their message.
|
|
14238
|
+
// A kept message's files are served again under their ids only from the instance's own inbox or (#1770) its reply
|
|
14239
|
+
// snapshots, by the ledger's checks (web-upload.ts restoreServed); they leave the ledger with their message.
|
|
14087
14240
|
this.webChatHistory.setFiles({
|
|
14088
14241
|
storablePath: (id) => this.webFiles.storablePath(id),
|
|
14089
|
-
restore: (entry, instance) => this.webFiles.restoreServed(entry, instance, join(getAgendHome(), "workspaces", instance, "inbox")),
|
|
14242
|
+
restore: (entry, instance) => this.webFiles.restoreServed(entry, instance, join(getAgendHome(), "workspaces", instance, "inbox"), webFilesDir(getAgendHome(), instance) ?? undefined),
|
|
14090
14243
|
drop: (ids) => this.webFiles.dropServed(ids),
|
|
14091
14244
|
});
|
|
14092
14245
|
let restored = 0;
|
|
14093
14246
|
for (const name of names)
|
|
14094
14247
|
restored += this.webChatHistory.restore(name, store.load(name));
|
|
14248
|
+
// #1770: before any instance replies, the snapshots no restored message holds (it left the history while the
|
|
14249
|
+
// fleet was down, or a copy stopped midway) are removed.
|
|
14250
|
+
let swept = 0;
|
|
14251
|
+
for (const name of names) {
|
|
14252
|
+
const dir = webFilesDir(getAgendHome(), name);
|
|
14253
|
+
if (dir)
|
|
14254
|
+
swept += sweepReplySnapshots(dir, this.webFiles.heldSnapshots(name));
|
|
14255
|
+
}
|
|
14256
|
+
if (swept)
|
|
14257
|
+
this.logger.info({ swept }, "Removed web reply snapshots no message holds");
|
|
14095
14258
|
this.webChatStore = store;
|
|
14096
14259
|
this.webChatHistory.setStore(store);
|
|
14097
14260
|
if (restored)
|
|
@@ -14882,12 +15045,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
14882
15045
|
const reactions = this.pendingReactionsMeta(instanceName);
|
|
14883
15046
|
Object.assign(meta, reactions.meta);
|
|
14884
15047
|
try {
|
|
14885
|
-
await this.
|
|
15048
|
+
await this.deliverHumanToInstance(instanceName, {
|
|
14886
15049
|
type: "fleet_inbound",
|
|
14887
15050
|
content: fullText,
|
|
14888
15051
|
targetSession: instanceName,
|
|
14889
15052
|
meta,
|
|
14890
|
-
});
|
|
15053
|
+
}, msg.humanOrigin, msg.humanOriginalBody ?? text);
|
|
14891
15054
|
reactions.consume();
|
|
14892
15055
|
}
|
|
14893
15056
|
catch (err) {
|