@songsid/agend 2.2.0-beta.6 → 2.2.0-beta.8
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/agent-whats-new.js +9 -3
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/backend/kiro.d.ts +9 -0
- package/dist/backend/kiro.js +23 -0
- package/dist/backend/kiro.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/config-validator.js +3 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-envelope.js +2 -0
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.js +126 -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 +40 -1
- package/dist/fleet-manager.js +248 -40
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +16 -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/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- 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/log-service.d.ts +99 -0
- package/dist/log-service.js +235 -0
- package/dist/log-service.js.map +1 -0
- package/dist/log-steps.d.ts +143 -0
- package/dist/log-steps.js +646 -0
- package/dist/log-steps.js.map +1 -0
- 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-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +4 -1
- package/dist/public-web-link.js +42 -13
- 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/settings-api.js +4 -0
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +2 -0
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +1 -1
- package/dist/tool-progress.js +3 -2
- package/dist/tool-progress.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/transcript-jsonl.d.ts +80 -3
- package/dist/transcript-jsonl.js +147 -10
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-monitor.d.ts +11 -0
- package/dist/transcript-monitor.js +61 -13
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +24 -0
- package/dist/transcript-sources.js +205 -55
- package/dist/transcript-sources.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/types.d.ts +6 -0
- 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/panel-log.js +388 -0
- package/dist/ui/panel-settings.js +6 -1
- package/dist/ui/preview-panel.js +7 -1
- package/dist/ui/preview.js +5 -1
- package/dist/ui/settings-strings.js +2 -0
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-route.js +6 -1
- package/dist/ui/shared/app-shell.js +3 -3
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +44 -6
- package/dist/ui/shared/app.js +5 -0
- package/dist/ui/shared/fonts/README.md +1 -1
- package/dist/ui/shared/instance-nav.js +9 -6
- package/dist/ui/shared/instance-switch.js +4 -4
- package/dist/ui/shared/tokens.css +1 -1
- package/dist/web-api.d.ts +5 -0
- package/dist/web-api.js +49 -1
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +2 -1
- package/dist/web-auth.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-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -0
- package/dist/web-shell-routes.js +9 -0
- package/dist/web-shell-routes.js.map +1 -1
- 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";
|
|
@@ -52,6 +53,7 @@ export declare function applyTaskListCap<T extends {
|
|
|
52
53
|
hint: string;
|
|
53
54
|
} | T[];
|
|
54
55
|
import { type PublicPreviewForUi } from "./web-api.js";
|
|
56
|
+
import { type LogAnswer, type LogQuery } from "./log-service.js";
|
|
55
57
|
import { WebChatHistory } from "./web-chat-history.js";
|
|
56
58
|
import { SeenUsers, type MentionNames, type MentionQuery } from "./web-mentions.js";
|
|
57
59
|
import { WebPrefsStore } from "./web-prefs.js";
|
|
@@ -63,6 +65,7 @@ import { type AgentEndpointContext } from "./agent-endpoint.js";
|
|
|
63
65
|
import { ClassicChannelManager } from "./classic-channel-manager.js";
|
|
64
66
|
import { type ExplicitInstanceRemoval } from "./instance-removal.js";
|
|
65
67
|
import type { InstanceState, InteractionSnapshot } from "./backend/types.js";
|
|
68
|
+
import { type Daemon } from "./daemon.js";
|
|
66
69
|
import { WakeCoordinator } from "./wake-coordinator.js";
|
|
67
70
|
import { TargetQueueWorker } from "./target-queue-worker.js";
|
|
68
71
|
import { resolveDeliveryWorkerMode } from "./types.js";
|
|
@@ -218,7 +221,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
218
221
|
private replyObligationTimer;
|
|
219
222
|
private readonly activeDurableTargets;
|
|
220
223
|
/** Live view of lifecycle.daemons — used throughout; not deprecated. */
|
|
221
|
-
get daemons(): Map<string,
|
|
224
|
+
get daemons(): Map<string, Daemon>;
|
|
222
225
|
fleetConfig: FleetConfig | null;
|
|
223
226
|
private rawFleetConfig;
|
|
224
227
|
private rawFleetDocument;
|
|
@@ -470,6 +473,10 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
470
473
|
private webChatStore;
|
|
471
474
|
/** Uploaded files and the files the dashboard may fetch back (uploads, reply attachments). */
|
|
472
475
|
readonly webFiles: WebFileLedger;
|
|
476
|
+
/** #1770: per instance, its last web reply still copying its files — the next one is shown after it, in order. */
|
|
477
|
+
private readonly webReplyOrder;
|
|
478
|
+
/** #1770: an instance's web-files byte cap (a field, so a test can make it small). */
|
|
479
|
+
private webReplyMaxBytes;
|
|
473
480
|
/** #1682: the people seen on inbound messages (id → name), for the web chat's mention pills; bounded, in memory. */
|
|
474
481
|
readonly seenUsers: SeenUsers;
|
|
475
482
|
private webPrefsStore?;
|
|
@@ -555,10 +562,13 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
555
562
|
kind: string;
|
|
556
563
|
correlationId: string;
|
|
557
564
|
payload: Record<string, unknown>;
|
|
565
|
+
forwards?: readonly string[];
|
|
566
|
+
requestedKind?: string;
|
|
558
567
|
}): {
|
|
559
568
|
deliveryId: string;
|
|
560
569
|
state: string;
|
|
561
570
|
duplicate: boolean;
|
|
571
|
+
kind: string;
|
|
562
572
|
};
|
|
563
573
|
queryDurableDeliveryStatus(callerInstance: string, selector: DeliveryStatusSelector): DeliveryStatusPage;
|
|
564
574
|
/** #1641: why a target is not taking queued work right now, or null when nothing known holds it. */
|
|
@@ -805,6 +815,17 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
805
815
|
getActiveBackendIds(): ReadonlySet<string>;
|
|
806
816
|
isClassicInstance(name: string): boolean;
|
|
807
817
|
/** #1523 N2: a ClassicBot room's own summary for its Details page (classicBot.yaml), or null. */
|
|
818
|
+
/** #1218: the Log tab's on-demand service — made on first use; with `web.log_tab` off it answers off and does nothing. */
|
|
819
|
+
private logService;
|
|
820
|
+
private get log();
|
|
821
|
+
/** #1218: GET /ui/steps — an instance's steps, read on demand (null: no such instance). */
|
|
822
|
+
logSteps(name: string, q: LogQuery): Promise<LogAnswer | null>;
|
|
823
|
+
/**
|
|
824
|
+
* #1218: where an instance's steps are read from, resolved only when its Log asks: the transcript its daemon follows
|
|
825
|
+
* now; for claude-code, also the path its statusline last named (so a stopped instance's Log still shows its last
|
|
826
|
+
* work). kiro comes with its own reader later in phase 1; other backends have no transcript AgEnD can read.
|
|
827
|
+
*/
|
|
828
|
+
private logSource;
|
|
808
829
|
classicRoomFor(name: string): {
|
|
809
830
|
name: string;
|
|
810
831
|
channel_id: string;
|
|
@@ -902,6 +923,17 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
902
923
|
hasPendingIdleGatedDelivery(instanceName: string): boolean;
|
|
903
924
|
/** The running launch's own steer capability (#1405); undefined for a backend without one, or no running Daemon. */
|
|
904
925
|
instanceLaunchSupportsSteer(instanceName: string): boolean | undefined;
|
|
926
|
+
private humanReceiptDaemons;
|
|
927
|
+
/** Cached FM + lifecycle + Daemon + tmux owner; unknown ownership grants nothing. */
|
|
928
|
+
private humanReceiptOwner;
|
|
929
|
+
private listenForHumanReceipt;
|
|
930
|
+
/** Evidence-only admission. Bots/system/agent payloads have no trusted origin. */
|
|
931
|
+
private humanReceiptPayload;
|
|
932
|
+
/** Bind at the actual IPC send, after all wake/reconnect waits. No human retry lane. */
|
|
933
|
+
private bindHumanReceiptPayload;
|
|
934
|
+
deliverHumanToInstance(instanceName: string, payload: Record<string, unknown>, origin: HumanOrigin | undefined, originalBody: string, options?: DeliveryOptions): Promise<boolean | void>;
|
|
935
|
+
/** Synchronous steer/btw handoff, using the same evidence and owner binding. */
|
|
936
|
+
sendHumanControl(instanceName: string, payload: Record<string, unknown>, origin: HumanOrigin | undefined, originalBody: string): boolean;
|
|
905
937
|
/** Single delivery facade: wake paused CLIs and serialize non-user work behind idle. */
|
|
906
938
|
deliverToInstance(instanceName: string, payload: Record<string, unknown>, options?: DeliveryOptions): Promise<boolean | void>;
|
|
907
939
|
/**
|
|
@@ -1580,7 +1612,14 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1580
1612
|
private isWebOnlyFleet;
|
|
1581
1613
|
/** On a fleet with no chat platform, the dashboard is where an instance-health prompt is posted (#1307 item 6). */
|
|
1582
1614
|
private webOnlyPromptPlace;
|
|
1615
|
+
/** Resolves when the reply is in the web chat (#1770: after its files are copied); the caller need not wait. */
|
|
1583
1616
|
private afterReplyRouted;
|
|
1617
|
+
/**
|
|
1618
|
+
* #1770: a reply's files as the web chat shows them. Each is copied into the instance's web-files/ and served from
|
|
1619
|
+
* the copy. One the copy refuses (a symlink, not a regular file, over 50 MB, a state file, missing) is not shown;
|
|
1620
|
+
* one that could not be copied (I/O) is served from the agent's own path, as before — until the fleet restarts.
|
|
1621
|
+
*/
|
|
1622
|
+
private webReplyAttachments;
|
|
1584
1623
|
private handleScheduleTrigger;
|
|
1585
1624
|
/**
|
|
1586
1625
|
* 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";
|
|
@@ -131,6 +132,7 @@ export function applyTaskListCap(tasks, filterAssignee, filterStatus) {
|
|
|
131
132
|
return tasks;
|
|
132
133
|
}
|
|
133
134
|
import { handleWebRequest, broadcastSseEvent, SSE_HEARTBEAT_MS } from "./web-api.js";
|
|
135
|
+
import { LogService } from "./log-service.js";
|
|
134
136
|
import { WebChatHistory, WEB_CHAT_TEXT_MAX, isWebMessageId, newWebMessageId } from "./web-chat-history.js";
|
|
135
137
|
import { WebChatDiskStore } from "./web-chat-store.js";
|
|
136
138
|
import { SeenUsers, resolveMentions } from "./web-mentions.js";
|
|
@@ -138,6 +140,7 @@ import { WebPrefsStore } from "./web-prefs.js";
|
|
|
138
140
|
import { ReplyButtonStore, parseReplyButtons, replyButtonClickText, replyButtonsFallbackText, REPLY_BUTTON_PREFIX } from "./reply-buttons.js";
|
|
139
141
|
import { ReplyButtonsController } from "./reply-buttons-controller.js";
|
|
140
142
|
import { publicAttachment, sweepOrphanedUploads, WebFileLedger } from "./web-upload.js";
|
|
143
|
+
import { REPLY_FILES_MAX_BYTES, snapshotReplyFile, sweepReplySnapshots, webFilesDir } from "./web-reply-files.js";
|
|
141
144
|
import { handleViewRequest, isViewPath, profileIdentities, resolveInstanceIdentity } from "./view-api.js";
|
|
142
145
|
import { downloadStickerImage } from "./channel/sticker-download.js";
|
|
143
146
|
import { WebImageCache, imageMimeForFile, WEB_IMAGE_MAX_AGE_MS, WEB_IMAGE_MAX_BYTES, WEB_IMAGE_RETRY_MS } from "./web-images.js";
|
|
@@ -789,6 +792,10 @@ export class FleetManager {
|
|
|
789
792
|
webChatStore = null;
|
|
790
793
|
/** Uploaded files and the files the dashboard may fetch back (uploads, reply attachments). */
|
|
791
794
|
webFiles = new WebFileLedger();
|
|
795
|
+
/** #1770: per instance, its last web reply still copying its files — the next one is shown after it, in order. */
|
|
796
|
+
webReplyOrder = new Map();
|
|
797
|
+
/** #1770: an instance's web-files byte cap (a field, so a test can make it small). */
|
|
798
|
+
webReplyMaxBytes = REPLY_FILES_MAX_BYTES;
|
|
792
799
|
/** #1682: the people seen on inbound messages (id → name), for the web chat's mention pills; bounded, in memory. */
|
|
793
800
|
seenUsers = new SeenUsers();
|
|
794
801
|
webPrefsStore;
|
|
@@ -1189,7 +1196,7 @@ export class FleetManager {
|
|
|
1189
1196
|
const reactions = this.pendingReactionsMeta(set.instance);
|
|
1190
1197
|
let accepted = false;
|
|
1191
1198
|
try {
|
|
1192
|
-
accepted = (await this.
|
|
1199
|
+
accepted = (await this.deliverHumanToInstance(set.instance, {
|
|
1193
1200
|
type: "fleet_inbound",
|
|
1194
1201
|
content,
|
|
1195
1202
|
targetSession: set.instance,
|
|
@@ -1198,7 +1205,7 @@ export class FleetManager {
|
|
|
1198
1205
|
thread_id: set.threadId, adapter_id: inbound.adapterId, source: inbound.source,
|
|
1199
1206
|
...reactions.meta,
|
|
1200
1207
|
},
|
|
1201
|
-
})) !== false;
|
|
1208
|
+
}, by.humanOrigin, button.label)) !== false;
|
|
1202
1209
|
}
|
|
1203
1210
|
finally {
|
|
1204
1211
|
// Both false and throw reopen the choice in the controller. Finish the
|
|
@@ -1317,6 +1324,7 @@ export class FleetManager {
|
|
|
1317
1324
|
deliveryId: admitted.delivery.deliveryId,
|
|
1318
1325
|
state: admitted.delivery.state,
|
|
1319
1326
|
duplicate: !admitted.inserted,
|
|
1327
|
+
kind: admitted.delivery.kind,
|
|
1320
1328
|
};
|
|
1321
1329
|
}
|
|
1322
1330
|
queryDurableDeliveryStatus(callerInstance, selector) {
|
|
@@ -2688,6 +2696,49 @@ export class FleetManager {
|
|
|
2688
2696
|
return this.classicChannels?.getAll().some(channel => channel.instanceName === name) ?? false;
|
|
2689
2697
|
}
|
|
2690
2698
|
/** #1523 N2: a ClassicBot room's own summary for its Details page (classicBot.yaml), or null. */
|
|
2699
|
+
/** #1218: the Log tab's on-demand service — made on first use; with `web.log_tab` off it answers off and does nothing. */
|
|
2700
|
+
logService = null;
|
|
2701
|
+
get log() {
|
|
2702
|
+
return this.logService ??= new LogService({
|
|
2703
|
+
enabled: () => this.fleetConfig?.web?.log_tab === true,
|
|
2704
|
+
known: (name) => Object.prototype.hasOwnProperty.call(this.fleetConfig?.instances ?? {}, name) || this.classicRoomFor(name) !== null,
|
|
2705
|
+
source: (name) => this.logSource(name),
|
|
2706
|
+
events: (name, limit) => (this.eventLog?.query({ instance: name, limit }) ?? []),
|
|
2707
|
+
emit: (event, data) => this.emitSseEvent(event, data),
|
|
2708
|
+
});
|
|
2709
|
+
}
|
|
2710
|
+
/** #1218: GET /ui/steps — an instance's steps, read on demand (null: no such instance). */
|
|
2711
|
+
logSteps(name, q) { return this.log.steps(name, q); }
|
|
2712
|
+
/**
|
|
2713
|
+
* #1218: where an instance's steps are read from, resolved only when its Log asks: the transcript its daemon follows
|
|
2714
|
+
* now; for claude-code, also the path its statusline last named (so a stopped instance's Log still shows its last
|
|
2715
|
+
* work). kiro comes with its own reader later in phase 1; other backends have no transcript AgEnD can read.
|
|
2716
|
+
*/
|
|
2717
|
+
logSource(name) {
|
|
2718
|
+
const instances = this.fleetConfig?.instances ?? {};
|
|
2719
|
+
const config = Object.prototype.hasOwnProperty.call(instances, name) ? instances[name] : undefined;
|
|
2720
|
+
const backend = config?.backend ?? (config ? null : this.classicRoomFor(name)?.backend) ?? this.fleetConfig?.defaults?.backend ?? "claude-code";
|
|
2721
|
+
const live = this.lifecycle.daemons.get(name)?.logTranscriptPath() ?? null;
|
|
2722
|
+
if (backend === "claude-code") {
|
|
2723
|
+
let path = live;
|
|
2724
|
+
if (!path) {
|
|
2725
|
+
try {
|
|
2726
|
+
path = JSON.parse(readFileSync(join(this.getInstanceDir(name), "statusline.json"), "utf-8")).transcript_path ?? null;
|
|
2727
|
+
}
|
|
2728
|
+
catch {
|
|
2729
|
+
path = null;
|
|
2730
|
+
}
|
|
2731
|
+
if (typeof path !== "string" || !path)
|
|
2732
|
+
path = null;
|
|
2733
|
+
}
|
|
2734
|
+
return { backend, path, format: "claude", ...(path ? {} : { reason: "no_transcript" }) };
|
|
2735
|
+
}
|
|
2736
|
+
if (backend === "codex")
|
|
2737
|
+
return { backend, path: live, format: "codex", ...(live ? {} : { reason: "not_running" }) };
|
|
2738
|
+
if (backend === "kiro-cli")
|
|
2739
|
+
return { backend, path: null, format: null, reason: "coming" };
|
|
2740
|
+
return { backend, path: null, format: null, reason: "unsupported" };
|
|
2741
|
+
}
|
|
2691
2742
|
classicRoomFor(name) {
|
|
2692
2743
|
const ch = this.classicChannels?.getAll().find(channel => channel.instanceName === name);
|
|
2693
2744
|
if (!ch)
|
|
@@ -2989,7 +3040,7 @@ export class FleetManager {
|
|
|
2989
3040
|
const queued = this.ipcWaitTails.get(instanceName);
|
|
2990
3041
|
if (!queued) {
|
|
2991
3042
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
2992
|
-
if (ipc?.connected && ipc.send(payload))
|
|
3043
|
+
if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
|
|
2993
3044
|
return true;
|
|
2994
3045
|
}
|
|
2995
3046
|
const attempt = (queued ?? Promise.resolve())
|
|
@@ -3024,7 +3075,7 @@ export class FleetManager {
|
|
|
3024
3075
|
// Re-read every round: a reconnect replaces the IpcClient object entirely,
|
|
3025
3076
|
// so a cached reference would stay dead forever.
|
|
3026
3077
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
3027
|
-
if (ipc?.connected && ipc.send(payload))
|
|
3078
|
+
if (ipc?.connected && ipc.send(this.bindHumanReceiptPayload(instanceName, payload)))
|
|
3028
3079
|
return true;
|
|
3029
3080
|
if (Date.now() >= deadline) {
|
|
3030
3081
|
throw new Error(`Instance '${instanceName}' IPC is unavailable`);
|
|
@@ -3242,6 +3293,101 @@ export class FleetManager {
|
|
|
3242
3293
|
instanceLaunchSupportsSteer(instanceName) {
|
|
3243
3294
|
return this.daemons.get(instanceName)?.launchSupportsSteer();
|
|
3244
3295
|
}
|
|
3296
|
+
humanReceiptDaemons = new WeakSet();
|
|
3297
|
+
/** Cached FM + lifecycle + Daemon + tmux owner; unknown ownership grants nothing. */
|
|
3298
|
+
humanReceiptOwner(instanceName, daemon = this.daemons.get(instanceName)) {
|
|
3299
|
+
try {
|
|
3300
|
+
if (this.shuttingDown || !daemon || this.daemons.get(instanceName) !== daemon)
|
|
3301
|
+
return null;
|
|
3302
|
+
const epoch = this.lifecycle.epochOf(instanceName);
|
|
3303
|
+
const owner = daemon.captureHumanReceiptOwner();
|
|
3304
|
+
if (!Number.isSafeInteger(epoch) || !owner)
|
|
3305
|
+
return null;
|
|
3306
|
+
return JSON.stringify([this.managerBootId, epoch, this.getDeliveryEpoch(instanceName), humanOwnerObjectId(daemon), owner]);
|
|
3307
|
+
}
|
|
3308
|
+
catch {
|
|
3309
|
+
return null;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
listenForHumanReceipt(instanceName, daemon) {
|
|
3313
|
+
// Lazy initialization also supports narrow prototype-based integration fixtures.
|
|
3314
|
+
this.humanReceiptDaemons ??= new WeakSet();
|
|
3315
|
+
if (this.humanReceiptDaemons.has(daemon))
|
|
3316
|
+
return;
|
|
3317
|
+
this.humanReceiptDaemons.add(daemon);
|
|
3318
|
+
daemon.on("human_receipt_result", (result) => {
|
|
3319
|
+
try {
|
|
3320
|
+
this.deliveryOutbox?.settleHumanReceipt(result.receipt_id, instanceName, result.owner_key, result.proof, this.humanReceiptOwner(instanceName, daemon));
|
|
3321
|
+
}
|
|
3322
|
+
catch (err) {
|
|
3323
|
+
this.logger.error({ err, instanceName }, "Could not persist human delivery evidence");
|
|
3324
|
+
}
|
|
3325
|
+
});
|
|
3326
|
+
}
|
|
3327
|
+
/** Evidence-only admission. Bots/system/agent payloads have no trusted origin. */
|
|
3328
|
+
humanReceiptPayload(instanceName, payload, origin, originalBody) {
|
|
3329
|
+
if (!origin || !this.deliveryOutbox)
|
|
3330
|
+
return payload;
|
|
3331
|
+
const row = this.deliveryOutbox.createHumanReceipt({ targetInstance: instanceName, originalBody,
|
|
3332
|
+
origin: { ...origin }, inputKind: String(payload.type), managerBootId: this.managerBootId });
|
|
3333
|
+
return { ...payload, meta: { ...(payload.meta ?? {}),
|
|
3334
|
+
human_receipt_id: row.receipt_id, verification_id: row.message_id } };
|
|
3335
|
+
}
|
|
3336
|
+
/** Bind at the actual IPC send, after all wake/reconnect waits. No human retry lane. */
|
|
3337
|
+
bindHumanReceiptPayload(instanceName, payload) {
|
|
3338
|
+
const meta = payload.meta;
|
|
3339
|
+
if (!meta?.human_receipt_id || !this.deliveryOutbox)
|
|
3340
|
+
return payload;
|
|
3341
|
+
const daemon = this.daemons.get(instanceName);
|
|
3342
|
+
const owner = this.humanReceiptOwner(instanceName, daemon);
|
|
3343
|
+
if (!daemon || !owner) {
|
|
3344
|
+
this.deliveryOutbox.markHumanReceiptUncertain(meta.human_receipt_id, instanceName);
|
|
3345
|
+
return payload;
|
|
3346
|
+
}
|
|
3347
|
+
this.listenForHumanReceipt(instanceName, daemon);
|
|
3348
|
+
const daemonOwner = daemon.captureHumanReceiptOwner();
|
|
3349
|
+
const wire = daemon.humanReceiptWire(String(payload.content ?? ""), meta, String(payload.type));
|
|
3350
|
+
const row = this.deliveryOutbox.bindHumanReceipt(meta.human_receipt_id, instanceName, owner, wire, this.managerBootId);
|
|
3351
|
+
if (!daemonOwner || this.humanReceiptOwner(instanceName, daemon) !== owner)
|
|
3352
|
+
throw new Error("Human delivery ownership changed");
|
|
3353
|
+
// /raw retains exact bytes and never acquires a positive verification capability.
|
|
3354
|
+
if (payload.type === "raw_paste" || String(payload.content ?? "").startsWith("/raw ")) {
|
|
3355
|
+
this.deliveryOutbox.markHumanReceiptUncertain(row.receipt_id, instanceName);
|
|
3356
|
+
return payload;
|
|
3357
|
+
}
|
|
3358
|
+
return { ...payload, meta: { ...meta, human_owner_key: owner, human_daemon_owner: daemonOwner, human_wire_sha256: row.wire_sha256 } };
|
|
3359
|
+
}
|
|
3360
|
+
async deliverHumanToInstance(instanceName, payload, origin, originalBody, options) {
|
|
3361
|
+
const admitted = this.humanReceiptPayload(instanceName, payload, origin, originalBody);
|
|
3362
|
+
const id = admitted.meta?.human_receipt_id;
|
|
3363
|
+
let sent = false;
|
|
3364
|
+
try {
|
|
3365
|
+
const result = options ? await this.deliverToInstance(instanceName, admitted, options) : await this.deliverToInstance(instanceName, admitted);
|
|
3366
|
+
sent = result !== false;
|
|
3367
|
+
return result;
|
|
3368
|
+
}
|
|
3369
|
+
finally {
|
|
3370
|
+
if (!sent && id)
|
|
3371
|
+
this.deliveryOutbox?.markHumanReceiptUncertain(id, instanceName);
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
/** Synchronous steer/btw handoff, using the same evidence and owner binding. */
|
|
3375
|
+
sendHumanControl(instanceName, payload, origin, originalBody) {
|
|
3376
|
+
const ipc = this.instanceIpcClients.get(instanceName);
|
|
3377
|
+
if (this.shuttingDown || !ipc?.connected)
|
|
3378
|
+
return false;
|
|
3379
|
+
const admitted = this.humanReceiptPayload(instanceName, payload, origin, originalBody);
|
|
3380
|
+
const id = admitted.meta?.human_receipt_id;
|
|
3381
|
+
let sent = false;
|
|
3382
|
+
try {
|
|
3383
|
+
sent = ipc.send(this.bindHumanReceiptPayload(instanceName, admitted));
|
|
3384
|
+
return sent;
|
|
3385
|
+
}
|
|
3386
|
+
finally {
|
|
3387
|
+
if (!sent && id)
|
|
3388
|
+
this.deliveryOutbox?.markHumanReceiptUncertain(id, instanceName);
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3245
3391
|
/** Single delivery facade: wake paused CLIs and serialize non-user work behind idle. */
|
|
3246
3392
|
async deliverToInstance(instanceName, payload, options = {}) {
|
|
3247
3393
|
const deliveryEpoch = this.getDeliveryEpoch(instanceName);
|
|
@@ -3434,7 +3580,7 @@ export class FleetManager {
|
|
|
3434
3580
|
userId: data.userId,
|
|
3435
3581
|
username,
|
|
3436
3582
|
source: "discord",
|
|
3437
|
-
timestamp: new Date(),
|
|
3583
|
+
timestamp: new Date(), adapterId, humanOrigin: data.humanOrigin, humanOriginalBody: text,
|
|
3438
3584
|
});
|
|
3439
3585
|
}
|
|
3440
3586
|
else if (data.command === "save") {
|
|
@@ -3579,7 +3725,8 @@ export class FleetManager {
|
|
|
3579
3725
|
await data.respond(t("classic.no_agent"));
|
|
3580
3726
|
return;
|
|
3581
3727
|
}
|
|
3582
|
-
const
|
|
3728
|
+
const originalSteerText = String(data.options?.message ?? "");
|
|
3729
|
+
const steerText = originalSteerText.trim();
|
|
3583
3730
|
if (!steerText) {
|
|
3584
3731
|
await data.respond(t("steer.usage"));
|
|
3585
3732
|
return;
|
|
@@ -3589,7 +3736,7 @@ export class FleetManager {
|
|
|
3589
3736
|
// rerouting the instance's replies to the slash context.
|
|
3590
3737
|
const result = this.topicCommands.sendSteer(name, steerText, {
|
|
3591
3738
|
chatId: "", messageId: "", username: data.username ?? "user",
|
|
3592
|
-
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord",
|
|
3739
|
+
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord", humanOrigin: data.humanOrigin, humanOriginalBody: originalSteerText,
|
|
3593
3740
|
});
|
|
3594
3741
|
await data.respond(result);
|
|
3595
3742
|
}
|
|
@@ -3599,14 +3746,15 @@ export class FleetManager {
|
|
|
3599
3746
|
await data.respond(t("classic.no_agent"));
|
|
3600
3747
|
return;
|
|
3601
3748
|
}
|
|
3602
|
-
const
|
|
3749
|
+
const originalBtwText = String(data.options?.message ?? "");
|
|
3750
|
+
const btwText = originalBtwText.trim();
|
|
3603
3751
|
if (!btwText) {
|
|
3604
3752
|
await data.respond(t("btw.usage"));
|
|
3605
3753
|
return;
|
|
3606
3754
|
}
|
|
3607
3755
|
const result = this.topicCommands.sendBtw(name, btwText, {
|
|
3608
3756
|
chatId: "", messageId: "", username: data.username ?? "user",
|
|
3609
|
-
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord",
|
|
3757
|
+
userId: data.userId ?? "", threadId: undefined, adapterId, source: "discord", humanOrigin: data.humanOrigin, humanOriginalBody: originalBtwText,
|
|
3610
3758
|
});
|
|
3611
3759
|
await data.respond(result);
|
|
3612
3760
|
}
|
|
@@ -4959,13 +5107,13 @@ export class FleetManager {
|
|
|
4959
5107
|
if (result && !result.durable)
|
|
4960
5108
|
this.logger.warn("Public session revocation could not be saved; exposure remains closed");
|
|
4961
5109
|
},
|
|
4962
|
-
log: (event, exposureId) => this.logger.info({ event, exposureId }, "Public web link"),
|
|
5110
|
+
log: (event, exposureId, detail) => this.logger.info({ event, exposureId, ...(detail ? { detail } : {}) }, "Public web link"),
|
|
4963
5111
|
createPreview: ({ id, expiresAt, binary, protocol, dashboardOrigin, isCurrent }) => {
|
|
4964
5112
|
if (!publicPreviewWanted(this.fleetConfig?.web))
|
|
4965
5113
|
return null;
|
|
4966
5114
|
return new PublicPreview({ exposureId: id, expiresAt, dashboardOrigin, isCurrent, manager: this.getPreviewTunnel(),
|
|
4967
5115
|
provider: () => new CloudflaredProvider({ binaryName: binary, protocol }),
|
|
4968
|
-
log: event => this.logger.info({ event, exposureId: id }, "Public web link") });
|
|
5116
|
+
log: (event, detail) => this.logger.info({ event, exposureId: id, ...(detail ? { detail } : {}) }, "Public web link") });
|
|
4969
5117
|
},
|
|
4970
5118
|
onCleanupUnconfirmed: (result, owner) => {
|
|
4971
5119
|
this.logger.warn({ pid: result.confirmed ? null : result.pid }, "Public web access closed; tunnel cleanup unconfirmed");
|
|
@@ -7509,6 +7657,7 @@ export class FleetManager {
|
|
|
7509
7657
|
});
|
|
7510
7658
|
}
|
|
7511
7659
|
async handleInboundMessage(msg) {
|
|
7660
|
+
msg = { ...msg, humanOriginalBody: msg.humanOriginalBody ?? msg.text, humanOrigin: msg.isBotMessage ? undefined : msg.humanOrigin && { ...msg.humanOrigin } };
|
|
7512
7661
|
if (msg.isBotMessage)
|
|
7513
7662
|
return withSpawnHoldReply(undefined, () => this.handleInboundMessageCore(msg));
|
|
7514
7663
|
return withSpawnHoldReply(text => this.inboundNoticeAdapter(msg)?.sendText(msg.chatId, text, { threadId: msg.threadId }) ?? Promise.resolve(), () => this.handleInboundMessageCore(msg));
|
|
@@ -7967,7 +8116,7 @@ export class FleetManager {
|
|
|
7967
8116
|
const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, generalInstance);
|
|
7968
8117
|
const generalReactions = this.pendingReactionsMeta(generalInstance);
|
|
7969
8118
|
try {
|
|
7970
|
-
await this.
|
|
8119
|
+
await this.deliverHumanToInstance(generalInstance, {
|
|
7971
8120
|
type: "fleet_inbound",
|
|
7972
8121
|
content: text,
|
|
7973
8122
|
targetSession: generalInstance,
|
|
@@ -7988,7 +8137,7 @@ export class FleetManager {
|
|
|
7988
8137
|
...generalReactions.meta,
|
|
7989
8138
|
...extraMeta,
|
|
7990
8139
|
},
|
|
7991
|
-
});
|
|
8140
|
+
}, msg.humanOrigin, msg.humanOriginalBody ?? msg.text);
|
|
7992
8141
|
generalReactions.consume();
|
|
7993
8142
|
this.afterUserInboundDelivered(generalInstance, msg);
|
|
7994
8143
|
this.logger.info(`${msg.username} → ${generalInstance}: ${(text ?? "").slice(0, 100)}`);
|
|
@@ -8068,7 +8217,7 @@ export class FleetManager {
|
|
|
8068
8217
|
const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, instanceName);
|
|
8069
8218
|
const reactions = this.pendingReactionsMeta(instanceName);
|
|
8070
8219
|
try {
|
|
8071
|
-
await this.
|
|
8220
|
+
await this.deliverHumanToInstance(instanceName, {
|
|
8072
8221
|
type: "fleet_inbound",
|
|
8073
8222
|
content: text,
|
|
8074
8223
|
targetSession: instanceName, // Channel messages → instance's own session
|
|
@@ -8087,7 +8236,7 @@ export class FleetManager {
|
|
|
8087
8236
|
...reactions.meta,
|
|
8088
8237
|
...extraMeta,
|
|
8089
8238
|
},
|
|
8090
|
-
});
|
|
8239
|
+
}, msg.humanOrigin, msg.humanOriginalBody ?? msg.text);
|
|
8091
8240
|
// Only after the message actually went out. A failed delivery keeps the
|
|
8092
8241
|
// reactions queued for the retry / the next message.
|
|
8093
8242
|
reactions.consume();
|
|
@@ -8395,7 +8544,7 @@ export class FleetManager {
|
|
|
8395
8544
|
// an agent cannot invent it to suppress the normal completion marker.
|
|
8396
8545
|
if (!error && result != null && msg.statusOnly !== true) {
|
|
8397
8546
|
try {
|
|
8398
|
-
this.afterReplyRouted(instanceName, args, senderSessionName, buttonsView);
|
|
8547
|
+
void this.afterReplyRouted(instanceName, args, senderSessionName, buttonsView);
|
|
8399
8548
|
}
|
|
8400
8549
|
catch (err) {
|
|
8401
8550
|
this.logger.warn({ err, instanceName }, "Reply delivered but post-delivery bookkeeping failed");
|
|
@@ -8448,6 +8597,7 @@ export class FleetManager {
|
|
|
8448
8597
|
webOnlyPromptPlace() {
|
|
8449
8598
|
return this.worlds.size === 0 && this.isWebOnlyFleet() ? { adapter: WEB_ONLY_PROMPT_SINK, adapterId: "web", chatId: "web" } : null;
|
|
8450
8599
|
}
|
|
8600
|
+
/** Resolves when the reply is in the web chat (#1770: after its files are copied); the caller need not wait. */
|
|
8451
8601
|
afterReplyRouted(instanceName, args, senderSessionName, buttons = null) {
|
|
8452
8602
|
// A reply is NOT proof the turn is over (#410) — but it is not proof of
|
|
8453
8603
|
// more work either. Split the difference: an instance that is clearly
|
|
@@ -8465,27 +8615,74 @@ export class FleetManager {
|
|
|
8465
8615
|
this.reactDone(instanceName);
|
|
8466
8616
|
const replyTo = this.lastInboundUser.get(instanceName) ?? "user";
|
|
8467
8617
|
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
8618
|
// Log bot reply to classic instance chat-log
|
|
8485
8619
|
const isClassic = this.classicChannels?.getChannelIdByInstance(instanceName) !== undefined;
|
|
8486
8620
|
if (isClassic) {
|
|
8487
8621
|
ClassicChannelManager.logMessage(instanceName, "bot", args.text ?? "", new Date());
|
|
8488
8622
|
}
|
|
8623
|
+
// Files the agent attached are shown in the web chat too, fetched by id, never by path. #1770: each is copied
|
|
8624
|
+
// first (web-reply-files.ts), and the copy is what the chat serves and keeps — so the message is shown once the
|
|
8625
|
+
// copies are done, and an instance's replies wait for the one before them: they reach the chat in order.
|
|
8626
|
+
const replyFiles = Array.isArray(args.files) ? args.files.filter((f) => typeof f === "string") : [];
|
|
8627
|
+
const ts = new Date().toISOString();
|
|
8628
|
+
const show = (attachments) => {
|
|
8629
|
+
// The one place a delivered agent reply reaches the web chat: the server marks it `agent` (#1306) — the only
|
|
8630
|
+
// role that may get HTML preview cards. Never inferred from the sender name or the text.
|
|
8631
|
+
this.emitSseEvent("message", {
|
|
8632
|
+
instance: instanceName, sender: senderSessionName ?? instanceName, role: "agent",
|
|
8633
|
+
text: (args.text ?? "").slice(0, WEB_CHAT_TEXT_MAX),
|
|
8634
|
+
ts,
|
|
8635
|
+
...(attachments.length ? { attachments } : {}),
|
|
8636
|
+
...(buttons ? { buttons } : {}), // #1266
|
|
8637
|
+
});
|
|
8638
|
+
};
|
|
8639
|
+
const before = this.webReplyOrder.get(instanceName);
|
|
8640
|
+
if (!replyFiles.length && !before) {
|
|
8641
|
+
show([]);
|
|
8642
|
+
return Promise.resolve();
|
|
8643
|
+
}
|
|
8644
|
+
const done = (before ?? Promise.resolve())
|
|
8645
|
+
.then(() => this.webReplyAttachments(instanceName, replyFiles))
|
|
8646
|
+
.catch((err) => { this.logger.warn({ err, instanceName }, "Reply files could not be prepared for the web chat"); return []; })
|
|
8647
|
+
.then((attachments) => { try {
|
|
8648
|
+
show(attachments);
|
|
8649
|
+
}
|
|
8650
|
+
catch (err) {
|
|
8651
|
+
this.logger.warn({ err, instanceName }, "Reply could not be shown in the web chat");
|
|
8652
|
+
} })
|
|
8653
|
+
.finally(() => { if (this.webReplyOrder.get(instanceName) === done)
|
|
8654
|
+
this.webReplyOrder.delete(instanceName); });
|
|
8655
|
+
this.webReplyOrder.set(instanceName, done);
|
|
8656
|
+
return done;
|
|
8657
|
+
}
|
|
8658
|
+
/**
|
|
8659
|
+
* #1770: a reply's files as the web chat shows them. Each is copied into the instance's web-files/ and served from
|
|
8660
|
+
* the copy. One the copy refuses (a symlink, not a regular file, over 50 MB, a state file, missing) is not shown;
|
|
8661
|
+
* one that could not be copied (I/O) is served from the agent's own path, as before — until the fleet restarts.
|
|
8662
|
+
*/
|
|
8663
|
+
async webReplyAttachments(instanceName, paths) {
|
|
8664
|
+
const home = getAgendHome();
|
|
8665
|
+
const dir = webFilesDir(home, instanceName);
|
|
8666
|
+
const out = [];
|
|
8667
|
+
for (const path of paths) {
|
|
8668
|
+
const id = randomBytes(16).toString("hex");
|
|
8669
|
+
const got = dir ? await snapshotReplyFile(path, dir, id, { stateDir: home, maxDirBytes: this.webReplyMaxBytes }) : null;
|
|
8670
|
+
if (got && "evicted" in got && got.evicted.length)
|
|
8671
|
+
this.webFiles.dropServed(got.evicted);
|
|
8672
|
+
let file = null;
|
|
8673
|
+
if (got?.ok)
|
|
8674
|
+
file = this.webFiles.registerServed({ id, path: got.path, name: basename(path), instance: instanceName, snapshot: true });
|
|
8675
|
+
else if (got?.refused)
|
|
8676
|
+
this.logger.info({ instanceName, file: basename(path), reason: got.reason }, "Reply file not shown in the web chat");
|
|
8677
|
+
else {
|
|
8678
|
+
if (got)
|
|
8679
|
+
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");
|
|
8680
|
+
file = this.webFiles.registerServed({ path, instance: instanceName });
|
|
8681
|
+
}
|
|
8682
|
+
if (file)
|
|
8683
|
+
out.push(publicAttachment(file));
|
|
8684
|
+
}
|
|
8685
|
+
return out;
|
|
8489
8686
|
}
|
|
8490
8687
|
// ===================== Scheduler =====================
|
|
8491
8688
|
async handleScheduleTrigger(schedule, stableRunId = randomUUID(), retry) {
|
|
@@ -12254,7 +12451,7 @@ export class FleetManager {
|
|
|
12254
12451
|
const topicId = this.fleetConfig?.instances[instance]?.topic_id;
|
|
12255
12452
|
return {
|
|
12256
12453
|
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",
|
|
12454
|
+
threadId: topicId != null ? String(topicId) : undefined, adapterId: this.getAdapterForInstance(instance)?.id ?? "", source: "web", humanOrigin: webHumanOrigin(true),
|
|
12258
12455
|
};
|
|
12259
12456
|
};
|
|
12260
12457
|
const choicesFromCache = (instance) => {
|
|
@@ -12285,8 +12482,8 @@ export class FleetManager {
|
|
|
12285
12482
|
return { current, options: levels.map(l => ({ id: l, label: this.effortChoiceLabel(l, current) })) };
|
|
12286
12483
|
},
|
|
12287
12484
|
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)),
|
|
12485
|
+
steer: (instance, text, originalBody) => this.topicCommands.sendSteer(instance, text, { ...webMeta(instance), humanOriginalBody: originalBody }),
|
|
12486
|
+
btw: (instance, text, originalBody) => this.topicCommands.sendBtw(instance, text, { ...webMeta(instance), humanOriginalBody: originalBody }),
|
|
12290
12487
|
pauseWake: (instance, action) => this.topicCommands.runPauseWake(instance, action),
|
|
12291
12488
|
save: (instance, filename) => this.topicCommands.sendSave(instance, filename),
|
|
12292
12489
|
clearAsk: (instance) => {
|
|
@@ -14082,16 +14279,26 @@ export class FleetManager {
|
|
|
14082
14279
|
try {
|
|
14083
14280
|
const store = new WebChatDiskStore({ home: getAgendHome(), logger: this.logger, ownerOf: (name) => this.webChatOwner(name) });
|
|
14084
14281
|
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.
|
|
14282
|
+
// A kept message's files are served again under their ids only from the instance's own inbox or (#1770) its reply
|
|
14283
|
+
// snapshots, by the ledger's checks (web-upload.ts restoreServed); they leave the ledger with their message.
|
|
14087
14284
|
this.webChatHistory.setFiles({
|
|
14088
14285
|
storablePath: (id) => this.webFiles.storablePath(id),
|
|
14089
|
-
restore: (entry, instance) => this.webFiles.restoreServed(entry, instance, join(getAgendHome(), "workspaces", instance, "inbox")),
|
|
14286
|
+
restore: (entry, instance) => this.webFiles.restoreServed(entry, instance, join(getAgendHome(), "workspaces", instance, "inbox"), webFilesDir(getAgendHome(), instance) ?? undefined),
|
|
14090
14287
|
drop: (ids) => this.webFiles.dropServed(ids),
|
|
14091
14288
|
});
|
|
14092
14289
|
let restored = 0;
|
|
14093
14290
|
for (const name of names)
|
|
14094
14291
|
restored += this.webChatHistory.restore(name, store.load(name));
|
|
14292
|
+
// #1770: before any instance replies, the snapshots no restored message holds (it left the history while the
|
|
14293
|
+
// fleet was down, or a copy stopped midway) are removed.
|
|
14294
|
+
let swept = 0;
|
|
14295
|
+
for (const name of names) {
|
|
14296
|
+
const dir = webFilesDir(getAgendHome(), name);
|
|
14297
|
+
if (dir)
|
|
14298
|
+
swept += sweepReplySnapshots(dir, this.webFiles.heldSnapshots(name));
|
|
14299
|
+
}
|
|
14300
|
+
if (swept)
|
|
14301
|
+
this.logger.info({ swept }, "Removed web reply snapshots no message holds");
|
|
14095
14302
|
this.webChatStore = store;
|
|
14096
14303
|
this.webChatHistory.setStore(store);
|
|
14097
14304
|
if (restored)
|
|
@@ -14882,12 +15089,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
14882
15089
|
const reactions = this.pendingReactionsMeta(instanceName);
|
|
14883
15090
|
Object.assign(meta, reactions.meta);
|
|
14884
15091
|
try {
|
|
14885
|
-
await this.
|
|
15092
|
+
await this.deliverHumanToInstance(instanceName, {
|
|
14886
15093
|
type: "fleet_inbound",
|
|
14887
15094
|
content: fullText,
|
|
14888
15095
|
targetSession: instanceName,
|
|
14889
15096
|
meta,
|
|
14890
|
-
});
|
|
15097
|
+
}, msg.humanOrigin, msg.humanOriginalBody ?? text);
|
|
14891
15098
|
reactions.consume();
|
|
14892
15099
|
}
|
|
14893
15100
|
catch (err) {
|
|
@@ -16496,6 +16703,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
16496
16703
|
clearInterval(this.eventLogPruneTimer);
|
|
16497
16704
|
this.eventLogPruneTimer = null;
|
|
16498
16705
|
}
|
|
16706
|
+
this.logService?.unwatchAll(); // #1218: a Log's watch timers end with the fleet
|
|
16499
16707
|
if (this.outboxPruneTimer) {
|
|
16500
16708
|
clearInterval(this.outboxPruneTimer);
|
|
16501
16709
|
this.outboxPruneTimer = null;
|