@songsid/agend 2.2.0-alpha.2 → 2.2.0-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/README.zh-TW.md +4 -4
- package/dist/agent-endpoint.d.ts +4 -1
- package/dist/agent-endpoint.js +14 -2
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/atomic-write.d.ts +21 -0
- package/dist/atomic-write.js +65 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex.d.ts +7 -0
- package/dist/backend/codex.js +7 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/muse.d.ts +7 -0
- package/dist/backend/muse.js +7 -0
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/binary-probe.d.ts +80 -0
- package/dist/binary-probe.js +182 -0
- package/dist/binary-probe.js.map +1 -0
- package/dist/cache-service.d.ts +17 -1
- package/dist/cache-service.js +39 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +5 -1
- package/dist/channel/adapters/discord.js +56 -11
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +38 -1
- package/dist/channel/adapters/telegram.js +82 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +8 -1
- package/dist/channel/tool-router.js +4 -2
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/cli.js +133 -38
- package/dist/cli.js.map +1 -1
- package/dist/connection-secrets.d.ts +5 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/cpu-profile.js +50 -0
- package/dist/cpu-profile.js.map +1 -1
- package/dist/daemon.d.ts +12 -0
- package/dist/daemon.js +166 -26
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +1 -0
- package/dist/delivery-outbox.js +12 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/discord-permissions.d.ts +33 -0
- package/dist/discord-permissions.js +42 -0
- package/dist/discord-permissions.js.map +1 -0
- package/dist/env-file.d.ts +11 -0
- package/dist/env-file.js +27 -0
- package/dist/env-file.js.map +1 -0
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-level-config.d.ts +8 -1
- package/dist/fleet-level-config.js +31 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +76 -7
- package/dist/fleet-manager.js +599 -127
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-stop-budget.d.ts +22 -0
- package/dist/fleet-stop-budget.js +95 -0
- package/dist/fleet-stop-budget.js.map +1 -0
- package/dist/kiro-transcript-lane.d.ts +8 -4
- package/dist/kiro-transcript-lane.js +66 -44
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/locale.js +82 -0
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.d.ts +5 -0
- package/dist/next-steps.js +26 -0
- package/dist/next-steps.js.map +1 -0
- package/dist/outbound-handlers.js +8 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +4 -0
- package/dist/outbound-schemas.js +6 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/outbox-open-error.d.ts +7 -0
- package/dist/outbox-open-error.js +15 -0
- package/dist/outbox-open-error.js.map +1 -0
- package/dist/pane-context-cache.d.ts +25 -0
- package/dist/pane-context-cache.js +73 -0
- package/dist/pane-context-cache.js.map +1 -0
- package/dist/paths.d.ts +0 -7
- package/dist/paths.js +8 -2
- package/dist/paths.js.map +1 -1
- package/dist/provider-probe.d.ts +15 -0
- package/dist/provider-probe.js +20 -0
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-link-progress.d.ts +73 -0
- package/dist/public-link-progress.js +190 -0
- package/dist/public-link-progress.js.map +1 -0
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +6 -1
- package/dist/public-web-link.js +87 -15
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +93 -20
- package/dist/quickstart-api.js +194 -62
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +15 -0
- package/dist/quickstart.js +100 -29
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +98 -0
- package/dist/reply-buttons-controller.js +174 -0
- package/dist/reply-buttons-controller.js.map +1 -0
- package/dist/reply-buttons.d.ts +113 -0
- package/dist/reply-buttons.js +208 -0
- package/dist/reply-buttons.js.map +1 -0
- package/dist/reply-dedup.js +3 -1
- package/dist/reply-dedup.js.map +1 -1
- package/dist/restart-guard.d.ts +1 -0
- package/dist/restart-guard.js +8 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/service-activation.d.ts +15 -2
- package/dist/service-activation.js +81 -4
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.js +19 -1
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +53 -12
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-authority.d.ts +11 -0
- package/dist/settings-authority.js +128 -0
- package/dist/settings-authority.js.map +1 -0
- package/dist/settings-change.d.ts +5 -0
- package/dist/settings-change.js +4 -3
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +3 -0
- package/dist/settings-confirmation.js +7 -1
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +19 -7
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-redaction.d.ts +18 -0
- package/dist/settings-redaction.js +58 -0
- package/dist/settings-redaction.js.map +1 -0
- package/dist/setup-form.d.ts +9 -1
- package/dist/setup-form.js +25 -1
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.js +13 -3
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +26 -19
- package/dist/setup-wizard.js.map +1 -1
- package/dist/systemd-runtime.d.ts +15 -0
- package/dist/systemd-runtime.js +41 -0
- package/dist/systemd-runtime.js.map +1 -0
- package/dist/tmux-control.d.ts +22 -0
- package/dist/tmux-control.js +82 -17
- package/dist/tmux-control.js.map +1 -1
- package/dist/token-env-name.d.ts +37 -0
- package/dist/token-env-name.js +93 -0
- package/dist/token-env-name.js.map +1 -0
- package/dist/tool-permissions.js +5 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +7 -16
- package/dist/topic-commands.js +45 -83
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +3 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +68 -0
- package/dist/transcript-sources.js +336 -29
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +39 -0
- package/dist/transcript-turns.js +58 -0
- package/dist/transcript-turns.js.map +1 -0
- package/dist/tunnel/cloudflared-install.d.ts +18 -0
- package/dist/tunnel/cloudflared-install.js +15 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +3 -0
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/ui/chat-render.js +32 -6
- package/dist/ui/chat-store.js +21 -1
- package/dist/ui/chat-strings.js +8 -0
- package/dist/ui/chat-thread.js +109 -52
- package/dist/ui/first-run.js +64 -0
- package/dist/ui/html-attachment.js +26 -0
- package/dist/ui/panel-chat.js +13 -5
- package/dist/ui/panel-fleet.js +5 -5
- package/dist/ui/panel-settings.js +18 -8
- package/dist/ui/preview-panel.js +38 -3
- package/dist/ui/settings-apply.js +3 -1
- package/dist/ui/settings-dialogs.js +95 -25
- package/dist/ui/settings-discord.js +130 -0
- package/dist/ui/settings-model.js +21 -0
- package/dist/ui/settings-strings.js +46 -6
- package/dist/ui/settings-token.js +54 -0
- package/dist/ui/settings-wizard.js +63 -29
- package/dist/ui/shared/app-shell.js +19 -33
- package/dist/ui/shared/app-store.js +5 -3
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +34 -2
- package/dist/ui/shared/app.js +2 -1
- package/dist/ui/shared/instance-nav.js +310 -0
- package/dist/ui/shared/panel-view.js +12 -190
- package/dist/ui/shared/view-roster-store.js +5 -0
- package/dist/ui/shared/view-strings.js +12 -0
- package/dist/update-dispatch.d.ts +1 -1
- package/dist/update-dispatch.js +1 -1
- package/dist/update-install.d.ts +15 -3
- package/dist/update-install.js +41 -13
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +22 -0
- package/dist/update-launch.js +106 -0
- package/dist/update-launch.js.map +1 -0
- package/dist/update-marker.js +3 -8
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/format-rich.js +7 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +4 -0
- package/dist/usage/providers.js +2 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +48 -2
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +40 -0
- package/dist/view-api.js +28 -10
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +8 -0
- package/dist/web-api.js +50 -16
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +8 -0
- package/dist/web-auth.js +34 -8
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +21 -0
- package/dist/web-chat-history.js +44 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-host-guard.d.ts +18 -2
- package/dist/web-host-guard.js +30 -5
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-session.d.ts +7 -1
- package/dist/web-session.js +23 -2
- package/dist/web-session.js.map +1 -1
- package/launcher/postinstall.cjs +8 -4
- package/package.json +1 -1
- package/templates/systemd.service.ejs +3 -3
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1266: what happens to a reply's buttons after they are sent — a click on a platform or in the web chat, the one
|
|
3
|
+
* choice it makes, the message showing how it ended, and the expiry sweep. The store (reply-buttons.ts) is the truth;
|
|
4
|
+
* this decides who may click and keeps every surface in step with it. FleetManager supplies the fleet's own pieces.
|
|
5
|
+
*/
|
|
6
|
+
import type { ChannelAdapter } from "./channel/types.js";
|
|
7
|
+
import { type ReplyButton, type ReplyButtonSet, type ReplyButtonStore } from "./reply-buttons.js";
|
|
8
|
+
/** A click from a platform adapter (the fleet's callback shape). */
|
|
9
|
+
export interface ReplyButtonClick {
|
|
10
|
+
callbackData: string;
|
|
11
|
+
chatId: string;
|
|
12
|
+
threadId?: string;
|
|
13
|
+
messageId: string;
|
|
14
|
+
userId?: string;
|
|
15
|
+
username?: string;
|
|
16
|
+
ack?: (notice?: string) => void;
|
|
17
|
+
}
|
|
18
|
+
/** What a web page shows for a reply's buttons. */
|
|
19
|
+
export interface ReplyButtonsView {
|
|
20
|
+
id: string;
|
|
21
|
+
labels: string[];
|
|
22
|
+
state: "open" | "chosen" | "expired";
|
|
23
|
+
chosen?: number;
|
|
24
|
+
by?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface ReplyButtonsDeps {
|
|
27
|
+
store: ReplyButtonStore;
|
|
28
|
+
/** Wall clock: an expiry is a calendar instant that must hold across restarts. */
|
|
29
|
+
now(): number;
|
|
30
|
+
adapterFor(adapterId: string): ChannelAdapter | undefined;
|
|
31
|
+
/** May this platform user answer this set — someone who may message its instance there? */
|
|
32
|
+
mayClick(set: ReplyButtonSet, userId: string): boolean;
|
|
33
|
+
/** Deliver the choice to the instance as an inbound message; false when it could not be delivered. */
|
|
34
|
+
deliver(set: ReplyButtonSet, button: ReplyButton, by: {
|
|
35
|
+
userId: string;
|
|
36
|
+
username: string;
|
|
37
|
+
source: "web" | string;
|
|
38
|
+
}): Promise<boolean>;
|
|
39
|
+
/** Tell the web pages a set changed. */
|
|
40
|
+
publish(instance: string, view: ReplyButtonsView): void;
|
|
41
|
+
logger: {
|
|
42
|
+
info(obj: object, msg?: string): void;
|
|
43
|
+
warn(obj: object, msg?: string): void;
|
|
44
|
+
};
|
|
45
|
+
setTimer(fn: () => void, ms: number): unknown;
|
|
46
|
+
clearTimer(handle: unknown): void;
|
|
47
|
+
}
|
|
48
|
+
export declare class ReplyButtonsController {
|
|
49
|
+
private readonly deps;
|
|
50
|
+
private timer;
|
|
51
|
+
private stopped;
|
|
52
|
+
/** Claims this process is delivering right now (a sweep leaves them alone). */
|
|
53
|
+
private readonly inFlight;
|
|
54
|
+
constructor(deps: ReplyButtonsDeps);
|
|
55
|
+
/** Before the send: a set for these buttons, and the callbacks the platform will carry. */
|
|
56
|
+
prepare(where: {
|
|
57
|
+
instance: string;
|
|
58
|
+
adapterId: string;
|
|
59
|
+
chatId: string;
|
|
60
|
+
threadId?: string;
|
|
61
|
+
}, buttons: readonly ReplyButton[]): {
|
|
62
|
+
id: string;
|
|
63
|
+
callbacks: Array<{
|
|
64
|
+
id: string;
|
|
65
|
+
label: string;
|
|
66
|
+
}>;
|
|
67
|
+
};
|
|
68
|
+
/** After a confirmed send: the message the buttons are on. From now on a click there can match. */
|
|
69
|
+
bind(id: string, messageId: string): void;
|
|
70
|
+
/** A send that failed: the set never existed. */
|
|
71
|
+
discard(id: string): void;
|
|
72
|
+
view(set: ReplyButtonSet, now?: number): ReplyButtonsView;
|
|
73
|
+
viewOf(id: string): ReplyButtonsView | null;
|
|
74
|
+
/** A platform click. True when the callback is a reply button's (handled here, whatever the outcome). */
|
|
75
|
+
handleCallback(data: ReplyButtonClick, adapterId: string): Promise<boolean>;
|
|
76
|
+
/** A click in the web chat (the caller has passed the /ui gate: a session, or the public link). */
|
|
77
|
+
clickWeb(instance: string, id: string, index: number): Promise<{
|
|
78
|
+
status: 200 | 400 | 403 | 409;
|
|
79
|
+
error?: string;
|
|
80
|
+
}>;
|
|
81
|
+
/** Consume, deliver, then show it everywhere. A choice the agent could not be given is undone (the set reopens). */
|
|
82
|
+
private choose;
|
|
83
|
+
/**
|
|
84
|
+
* Show how a set ended on its platform message and in the web chat; once. A failed edit is not retried. Ended is: a
|
|
85
|
+
* delivered choice, an expiry with no claim, or (`closeClaim`) a claim no process is delivering any more.
|
|
86
|
+
*/
|
|
87
|
+
settle(set: ReplyButtonSet, closeClaim?: boolean): Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Settle what has ended (an expiry, or a choice whose message was not updated before a restart), then re-arm. A
|
|
90
|
+
* claim no one in this process is delivering was left by a process that stopped mid-delivery: whether the agent got
|
|
91
|
+
* it is unknown, so it is closed as expired ("reply in text") rather than reopened — reopening could deliver a
|
|
92
|
+
* second choice.
|
|
93
|
+
*/
|
|
94
|
+
sweep(): Promise<void>;
|
|
95
|
+
/** One timer, for the next expiry. A wall-clock jump makes it fire early or late; the sweep re-reads the clock. */
|
|
96
|
+
arm(): void;
|
|
97
|
+
stop(): void;
|
|
98
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { t } from "./locale.js";
|
|
2
|
+
import { REPLY_BUTTON_PREFIX, newReplyButtonsId, parseReplyButtonCallback, replyButtonCallback, replyButtonsClickPlace, } from "./reply-buttons.js";
|
|
3
|
+
/** Settled sets are kept a week (a page may still show the reply), then pruned. */
|
|
4
|
+
const KEEP_SETTLED_MS = 7 * 24 * 60 * 60 * 1000;
|
|
5
|
+
/** setTimeout's ceiling; a later expiry is reached by re-arming. */
|
|
6
|
+
const MAX_TIMER_MS = 2_147_483_647;
|
|
7
|
+
export class ReplyButtonsController {
|
|
8
|
+
deps;
|
|
9
|
+
timer = null;
|
|
10
|
+
stopped = false;
|
|
11
|
+
/** Claims this process is delivering right now (a sweep leaves them alone). */
|
|
12
|
+
inFlight = new Set();
|
|
13
|
+
constructor(deps) {
|
|
14
|
+
this.deps = deps;
|
|
15
|
+
}
|
|
16
|
+
/** Before the send: a set for these buttons, and the callbacks the platform will carry. */
|
|
17
|
+
prepare(where, buttons) {
|
|
18
|
+
const id = newReplyButtonsId();
|
|
19
|
+
this.deps.store.create({ id, instance: where.instance, adapterId: where.adapterId, chatId: where.chatId, threadId: where.threadId ?? "", buttons: [...buttons] }, this.deps.now());
|
|
20
|
+
return { id, callbacks: buttons.map((b, i) => ({ id: replyButtonCallback(id, i), label: b.label })) };
|
|
21
|
+
}
|
|
22
|
+
/** After a confirmed send: the message the buttons are on. From now on a click there can match. */
|
|
23
|
+
bind(id, messageId) {
|
|
24
|
+
this.deps.store.bind(id, messageId);
|
|
25
|
+
this.arm();
|
|
26
|
+
}
|
|
27
|
+
/** A send that failed: the set never existed. */
|
|
28
|
+
discard(id) { this.deps.store.remove(id); }
|
|
29
|
+
view(set, now = this.deps.now()) {
|
|
30
|
+
const labels = set.buttons.map(b => b.label);
|
|
31
|
+
if (set.deliveredAt !== null && set.chosenIndex !== null)
|
|
32
|
+
return { id: set.id, labels, state: "chosen", chosen: set.chosenIndex, by: set.chosenBy ?? "" };
|
|
33
|
+
// Settled without a delivered choice: expired, or a claim a stopped process never delivered (closed as expired).
|
|
34
|
+
if (set.settledAt !== null)
|
|
35
|
+
return { id: set.id, labels, state: "expired" };
|
|
36
|
+
return { id: set.id, labels, state: now >= set.expiresAt && set.consumedAt === null ? "expired" : "open" };
|
|
37
|
+
}
|
|
38
|
+
viewOf(id) {
|
|
39
|
+
const set = this.deps.store.get(id);
|
|
40
|
+
return set ? this.view(set) : null;
|
|
41
|
+
}
|
|
42
|
+
/** A platform click. True when the callback is a reply button's (handled here, whatever the outcome). */
|
|
43
|
+
async handleCallback(data, adapterId) {
|
|
44
|
+
if (!data.callbackData.startsWith(REPLY_BUTTON_PREFIX))
|
|
45
|
+
return false;
|
|
46
|
+
const parsed = parseReplyButtonCallback(data.callbackData);
|
|
47
|
+
const set = parsed ? this.deps.store.get(parsed.id) : null;
|
|
48
|
+
if (!parsed || !set) {
|
|
49
|
+
data.ack?.(t("reply_buttons.closed"));
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (!replyButtonsClickPlace(set, { adapterId, chatId: data.chatId, threadId: data.threadId, messageId: data.messageId })) {
|
|
53
|
+
this.deps.logger.warn({ adapterId, set: set.id }, "Refused reply-button click: not on the message the buttons were sent on");
|
|
54
|
+
data.ack?.(t("buttons.wrong_place"));
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (!data.userId || !this.deps.mayClick(set, data.userId)) {
|
|
58
|
+
this.deps.logger.warn({ adapterId, set: set.id, userId: data.userId }, "Refused reply-button click: not someone who may message this instance here");
|
|
59
|
+
data.ack?.(t("buttons.not_allowed"));
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
await this.choose(set.id, parsed.index, { userId: data.userId, username: data.username || data.userId, source: "platform" }, data.ack);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
/** A click in the web chat (the caller has passed the /ui gate: a session, or the public link). */
|
|
66
|
+
async clickWeb(instance, id, index) {
|
|
67
|
+
if (!/^[0-9a-f]{32}$/.test(id) || !Number.isInteger(index) || index < 0)
|
|
68
|
+
return { status: 400, error: "Malformed button" };
|
|
69
|
+
const set = this.deps.store.get(id);
|
|
70
|
+
if (!set)
|
|
71
|
+
return { status: 409, error: t("reply_buttons.closed") };
|
|
72
|
+
if (set.instance !== instance)
|
|
73
|
+
return { status: 403, error: "This button belongs to another instance" };
|
|
74
|
+
let notice;
|
|
75
|
+
const ok = await this.choose(id, index, { userId: "web-user", username: "web-user", source: "web" }, n => { notice = n; });
|
|
76
|
+
return ok ? { status: 200 } : { status: 409, error: notice ?? t("reply_buttons.closed") };
|
|
77
|
+
}
|
|
78
|
+
/** Consume, deliver, then show it everywhere. A choice the agent could not be given is undone (the set reopens). */
|
|
79
|
+
async choose(id, index, by, ack) {
|
|
80
|
+
const now = this.deps.now();
|
|
81
|
+
const result = this.deps.store.consume(id, index, by.username, now);
|
|
82
|
+
if (!result.ok) {
|
|
83
|
+
ack?.(result.reason === "used" ? t("reply_buttons.answered") : t("reply_buttons.closed"));
|
|
84
|
+
if (result.reason === "expired" && result.set)
|
|
85
|
+
void this.settle(result.set);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
let delivered = false;
|
|
89
|
+
this.inFlight.add(id);
|
|
90
|
+
try {
|
|
91
|
+
delivered = await this.deps.deliver(result.set, result.button, by);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
this.deps.logger.warn({ err: err.message, set: id }, "Reply-button choice could not be delivered");
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
this.inFlight.delete(id);
|
|
98
|
+
}
|
|
99
|
+
if (!delivered) {
|
|
100
|
+
this.deps.store.release(id, now);
|
|
101
|
+
ack?.(t("reply_buttons.closed"));
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
this.deps.store.markDelivered(id, this.deps.now());
|
|
105
|
+
ack?.(t("reply_buttons.sent", result.button.label));
|
|
106
|
+
await this.settle(result.set);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Show how a set ended on its platform message and in the web chat; once. A failed edit is not retried. Ended is: a
|
|
111
|
+
* delivered choice, an expiry with no claim, or (`closeClaim`) a claim no process is delivering any more.
|
|
112
|
+
*/
|
|
113
|
+
async settle(set, closeClaim = false) {
|
|
114
|
+
const fresh = this.deps.store.get(set.id);
|
|
115
|
+
if (!fresh || fresh.settledAt !== null || fresh.messageId === null)
|
|
116
|
+
return;
|
|
117
|
+
const now = this.deps.now();
|
|
118
|
+
// Invariant (#1500 review r2): an orphan close only closes a set that is STILL claimed and not delivered. A claim
|
|
119
|
+
// released while a sweep was awaiting another set's edit is open again — re-read here, it is not an orphan.
|
|
120
|
+
const ended = fresh.deliveredAt !== null || (fresh.consumedAt === null && now >= fresh.expiresAt)
|
|
121
|
+
|| (closeClaim && fresh.consumedAt !== null && fresh.deliveredAt === null && !this.inFlight.has(fresh.id));
|
|
122
|
+
if (!ended)
|
|
123
|
+
return; // open, or a claim being delivered
|
|
124
|
+
this.deps.store.markSettled(fresh.id, now); // first: a concurrent settle does nothing
|
|
125
|
+
const view = this.view({ ...fresh, settledAt: now }, now);
|
|
126
|
+
this.deps.publish(fresh.instance, view);
|
|
127
|
+
const adapter = this.deps.adapterFor(fresh.adapterId);
|
|
128
|
+
if (!adapter?.settleReplyButtons)
|
|
129
|
+
return;
|
|
130
|
+
const outcome = view.state === "chosen" ? { chosenIndex: view.chosen, by: view.by ?? "" } : { expired: true };
|
|
131
|
+
try {
|
|
132
|
+
await adapter.settleReplyButtons(fresh.chatId, fresh.messageId, fresh.threadId || undefined, view.labels, outcome);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
this.deps.logger.warn({ err: err.message, set: fresh.id }, "Could not update a reply's buttons on the platform");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Settle what has ended (an expiry, or a choice whose message was not updated before a restart), then re-arm. A
|
|
140
|
+
* claim no one in this process is delivering was left by a process that stopped mid-delivery: whether the agent got
|
|
141
|
+
* it is unknown, so it is closed as expired ("reply in text") rather than reopened — reopening could deliver a
|
|
142
|
+
* second choice.
|
|
143
|
+
*/
|
|
144
|
+
async sweep() {
|
|
145
|
+
const now = this.deps.now();
|
|
146
|
+
for (const set of this.deps.store.unsettled(now))
|
|
147
|
+
await this.settle(set);
|
|
148
|
+
for (const set of this.deps.store.undeliveredClaims())
|
|
149
|
+
if (!this.inFlight.has(set.id))
|
|
150
|
+
await this.settle(set, true);
|
|
151
|
+
this.deps.store.prune(now - KEEP_SETTLED_MS);
|
|
152
|
+
this.arm();
|
|
153
|
+
}
|
|
154
|
+
/** One timer, for the next expiry. A wall-clock jump makes it fire early or late; the sweep re-reads the clock. */
|
|
155
|
+
arm() {
|
|
156
|
+
if (this.stopped)
|
|
157
|
+
return;
|
|
158
|
+
if (this.timer !== null)
|
|
159
|
+
this.deps.clearTimer(this.timer);
|
|
160
|
+
this.timer = null;
|
|
161
|
+
const next = this.deps.store.nextExpiry(this.deps.now());
|
|
162
|
+
if (next === null)
|
|
163
|
+
return;
|
|
164
|
+
const delay = Math.min(MAX_TIMER_MS, Math.max(1000, next - this.deps.now()));
|
|
165
|
+
this.timer = this.deps.setTimer(() => { this.timer = null; void this.sweep(); }, delay);
|
|
166
|
+
}
|
|
167
|
+
stop() {
|
|
168
|
+
this.stopped = true;
|
|
169
|
+
if (this.timer !== null)
|
|
170
|
+
this.deps.clearTimer(this.timer);
|
|
171
|
+
this.timer = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=reply-buttons-controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reply-buttons-controller.js","sourceRoot":"","sources":["../src/reply-buttons-controller.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
export declare const REPLY_BUTTONS_MAX = 10;
|
|
2
|
+
/** Discord's limit per label (the strictest of the three); counted in UTF-16 units, as Discord counts. */
|
|
3
|
+
export declare const REPLY_BUTTON_LABEL_MAX = 80;
|
|
4
|
+
export declare const REPLY_BUTTON_VALUE_MAX = 200;
|
|
5
|
+
export declare const REPLY_BUTTON_TTL_MS: number;
|
|
6
|
+
export declare const REPLY_BUTTON_PREFIX = "rb:";
|
|
7
|
+
export interface ReplyButton {
|
|
8
|
+
label: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The reply's `buttons`, checked: null when there are none, `{ error }` when they cannot be sent as given.
|
|
13
|
+
* Labels are plain text on every surface; a newline or control character is refused rather than rewritten.
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseReplyButtons(raw: unknown, args: {
|
|
16
|
+
text?: unknown;
|
|
17
|
+
stickers?: unknown;
|
|
18
|
+
}): {
|
|
19
|
+
buttons: ReplyButton[];
|
|
20
|
+
} | {
|
|
21
|
+
error: string;
|
|
22
|
+
} | null;
|
|
23
|
+
export declare function newReplyButtonsId(): string;
|
|
24
|
+
/** What a platform carries for one button: an unguessable set id and the button's index — never its label or value. */
|
|
25
|
+
export declare function replyButtonCallback(id: string, index: number): string;
|
|
26
|
+
export declare function parseReplyButtonCallback(data: string): {
|
|
27
|
+
id: string;
|
|
28
|
+
index: number;
|
|
29
|
+
} | null;
|
|
30
|
+
/** The message the agent receives for a click: an ordinary inbound line, marked as a button. */
|
|
31
|
+
export declare function replyButtonClickText(b: ReplyButton): string;
|
|
32
|
+
/** Where buttons cannot be shown: the choices as text, so the question can still be answered by writing back. */
|
|
33
|
+
export declare function replyButtonsFallbackText(buttons: readonly ReplyButton[]): string;
|
|
34
|
+
/**
|
|
35
|
+
* Is a platform click on this set's own message? Same adapter, same message id, and the click's chat or thread is the
|
|
36
|
+
* set's chat or thread (Discord reports a click as guild + channel, Telegram as chat + topic; a reply recorded its
|
|
37
|
+
* chat_id and thread). A callback copied onto another message, or replayed from another chat, is not.
|
|
38
|
+
*/
|
|
39
|
+
export declare function replyButtonsClickPlace(set: ReplyButtonSet, at: {
|
|
40
|
+
adapterId: string;
|
|
41
|
+
chatId: string;
|
|
42
|
+
threadId?: string;
|
|
43
|
+
messageId: string;
|
|
44
|
+
}): boolean;
|
|
45
|
+
export interface ReplyButtonSet {
|
|
46
|
+
id: string;
|
|
47
|
+
instance: string;
|
|
48
|
+
/** The world (adapter id) it was sent through, or "web" on a web-only fleet. */
|
|
49
|
+
adapterId: string;
|
|
50
|
+
chatId: string;
|
|
51
|
+
threadId: string;
|
|
52
|
+
/** The platform message carrying the buttons; null while the send is in flight. */
|
|
53
|
+
messageId: string | null;
|
|
54
|
+
buttons: ReplyButton[];
|
|
55
|
+
createdAt: number;
|
|
56
|
+
expiresAt: number;
|
|
57
|
+
consumedAt: number | null;
|
|
58
|
+
chosenIndex: number | null;
|
|
59
|
+
chosenBy: string | null;
|
|
60
|
+
/**
|
|
61
|
+
* When the agent was given the choice (#1500 review). A claim (consumedAt) is not a choice until then: only a
|
|
62
|
+
* delivered one is shown as chosen; one that cannot be delivered is released.
|
|
63
|
+
*/
|
|
64
|
+
deliveredAt: number | null;
|
|
65
|
+
/** Set when the platform message was updated to its final state (chosen / expired). */
|
|
66
|
+
settledAt: number | null;
|
|
67
|
+
}
|
|
68
|
+
export type ConsumeResult = {
|
|
69
|
+
ok: true;
|
|
70
|
+
set: ReplyButtonSet;
|
|
71
|
+
button: ReplyButton;
|
|
72
|
+
} | {
|
|
73
|
+
ok: false;
|
|
74
|
+
reason: "missing" | "pending" | "used" | "expired";
|
|
75
|
+
set?: ReplyButtonSet;
|
|
76
|
+
};
|
|
77
|
+
/** The button sets, in `<dataDir>/reply-buttons.db`. Wall-clock instants: an expiry is a calendar time that must hold across restarts. */
|
|
78
|
+
export declare class ReplyButtonStore {
|
|
79
|
+
private db;
|
|
80
|
+
constructor(path: string);
|
|
81
|
+
close(): void;
|
|
82
|
+
/** Before the send: the set exists, unbound (a click cannot match it until the platform message is known). */
|
|
83
|
+
create(set: Pick<ReplyButtonSet, "id" | "instance" | "adapterId" | "chatId" | "threadId" | "buttons">, now: number): ReplyButtonSet;
|
|
84
|
+
/** After the send: the message the buttons are on. Only an unbound set can be bound. */
|
|
85
|
+
bind(id: string, messageId: string): boolean;
|
|
86
|
+
/** A send that failed: the set never existed for anyone. */
|
|
87
|
+
remove(id: string): void;
|
|
88
|
+
get(id: string): ReplyButtonSet | null;
|
|
89
|
+
/**
|
|
90
|
+
* One click, already placed on this set's message by the caller (replyButtonsClickPlace). The first permitted click
|
|
91
|
+
* consumes the whole set, atomically: of two clicks racing here only one changes the row.
|
|
92
|
+
*/
|
|
93
|
+
consume(id: string, index: number, by: string, now: number): ConsumeResult;
|
|
94
|
+
/** The claim reached the agent: from now on it is the choice. */
|
|
95
|
+
markDelivered(id: string, now: number): void;
|
|
96
|
+
/** A consumed click that could not reach the agent: the set is open again (only that click's own claim is undone). */
|
|
97
|
+
release(id: string, consumedAt: number): void;
|
|
98
|
+
/** Claims never delivered nor released — in flight now, or left by a process that stopped mid-delivery. */
|
|
99
|
+
undeliveredClaims(): ReplyButtonSet[];
|
|
100
|
+
/** The platform message shows the final state now (chosen or expired). */
|
|
101
|
+
markSettled(id: string, now: number): void;
|
|
102
|
+
/**
|
|
103
|
+
* Sets that ended and whose message does not show it yet: a delivered choice, or an expiry nobody claimed. A claim
|
|
104
|
+
* still being delivered is neither (#1500 review): it may yet be released.
|
|
105
|
+
*/
|
|
106
|
+
unsettled(now: number): ReplyButtonSet[];
|
|
107
|
+
/** The next expiry still to come, for the one sweep timer. */
|
|
108
|
+
nextExpiry(now: number): number | null;
|
|
109
|
+
/** Sets on these messages, for the web chat's view of a reply's buttons. */
|
|
110
|
+
forMessages(instance: string, messageIds: readonly string[]): ReplyButtonSet[];
|
|
111
|
+
/** Settled sets older than `before` are history the store no longer needs. */
|
|
112
|
+
prune(before: number): number;
|
|
113
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1266: buttons on an agent's reply — the same choice in the web chat, on Telegram (inline keyboard) and on Discord
|
|
3
|
+
* (message components). Design: https://github.com/songsid/AgEnD/issues/1266#issuecomment-6082375631
|
|
4
|
+
*
|
|
5
|
+
* - A reply's `buttons` are checked here before anything is sent (the reply tool's arguments are not validated by
|
|
6
|
+
* the MCP schema at run time).
|
|
7
|
+
* - Each button set is one row of a small SQLite store, so it outlives a restart. The platform sees only
|
|
8
|
+
* `rb:<32 hex>:<index>`; the labels and values stay here.
|
|
9
|
+
* - A click is consumed once for the whole set (`UPDATE … WHERE consumed_at IS NULL`), only on the exact message the
|
|
10
|
+
* set was delivered on, and only before it expires: a replayed or late click changes nothing.
|
|
11
|
+
*/
|
|
12
|
+
import Database from "better-sqlite3";
|
|
13
|
+
import { randomBytes } from "node:crypto";
|
|
14
|
+
export const REPLY_BUTTONS_MAX = 10;
|
|
15
|
+
/** Discord's limit per label (the strictest of the three); counted in UTF-16 units, as Discord counts. */
|
|
16
|
+
export const REPLY_BUTTON_LABEL_MAX = 80;
|
|
17
|
+
export const REPLY_BUTTON_VALUE_MAX = 200;
|
|
18
|
+
export const REPLY_BUTTON_TTL_MS = 24 * 60 * 60 * 1000;
|
|
19
|
+
export const REPLY_BUTTON_PREFIX = "rb:";
|
|
20
|
+
/**
|
|
21
|
+
* The reply's `buttons`, checked: null when there are none, `{ error }` when they cannot be sent as given.
|
|
22
|
+
* Labels are plain text on every surface; a newline or control character is refused rather than rewritten.
|
|
23
|
+
*/
|
|
24
|
+
export function parseReplyButtons(raw, args) {
|
|
25
|
+
if (raw === undefined || raw === null)
|
|
26
|
+
return null;
|
|
27
|
+
if (!Array.isArray(raw))
|
|
28
|
+
return { error: "buttons must be a list of { label, value? }" };
|
|
29
|
+
if (raw.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
if (raw.length > REPLY_BUTTONS_MAX)
|
|
32
|
+
return { error: `at most ${REPLY_BUTTONS_MAX} buttons on one reply (${raw.length} given)` };
|
|
33
|
+
if (typeof args.text !== "string" || !args.text.trim())
|
|
34
|
+
return { error: "buttons need text: they sit on the reply's message" };
|
|
35
|
+
if (Array.isArray(args.stickers) && args.stickers.length)
|
|
36
|
+
return { error: "buttons and stickers cannot go on one reply: send the stickers in another reply" };
|
|
37
|
+
const out = [];
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
for (const [i, b] of raw.entries()) {
|
|
40
|
+
if (!b || typeof b !== "object" || Array.isArray(b))
|
|
41
|
+
return { error: `buttons[${i}] must be { label, value? }` };
|
|
42
|
+
const { label, value } = b;
|
|
43
|
+
if (typeof label !== "string" || !label.trim())
|
|
44
|
+
return { error: `buttons[${i}].label must be non-empty text` };
|
|
45
|
+
if (label.length > REPLY_BUTTON_LABEL_MAX)
|
|
46
|
+
return { error: `buttons[${i}].label is ${label.length} characters; at most ${REPLY_BUTTON_LABEL_MAX}` };
|
|
47
|
+
// eslint-disable-next-line no-control-regex
|
|
48
|
+
if (/[\u0000-\u001f\u007f]/.test(label))
|
|
49
|
+
return { error: `buttons[${i}].label must be one line of text` };
|
|
50
|
+
if (value !== undefined && typeof value !== "string")
|
|
51
|
+
return { error: `buttons[${i}].value must be text` };
|
|
52
|
+
if (typeof value === "string" && value.length > REPLY_BUTTON_VALUE_MAX)
|
|
53
|
+
return { error: `buttons[${i}].value is ${value.length} characters; at most ${REPLY_BUTTON_VALUE_MAX}` };
|
|
54
|
+
const trimmed = label.trim();
|
|
55
|
+
if (seen.has(trimmed))
|
|
56
|
+
return { error: `buttons[${i}].label "${trimmed}" is used twice` };
|
|
57
|
+
seen.add(trimmed);
|
|
58
|
+
out.push({ label: trimmed, value: typeof value === "string" && value !== "" ? value : trimmed });
|
|
59
|
+
}
|
|
60
|
+
return { buttons: out };
|
|
61
|
+
}
|
|
62
|
+
export function newReplyButtonsId() { return randomBytes(16).toString("hex"); }
|
|
63
|
+
/** What a platform carries for one button: an unguessable set id and the button's index — never its label or value. */
|
|
64
|
+
export function replyButtonCallback(id, index) { return `${REPLY_BUTTON_PREFIX}${id}:${index}`; }
|
|
65
|
+
export function parseReplyButtonCallback(data) {
|
|
66
|
+
const m = /^rb:([0-9a-f]{32}):(\d{1,2})$/.exec(data);
|
|
67
|
+
if (!m)
|
|
68
|
+
return null;
|
|
69
|
+
const index = Number(m[2]);
|
|
70
|
+
return index < REPLY_BUTTONS_MAX ? { id: m[1], index } : null;
|
|
71
|
+
}
|
|
72
|
+
/** The message the agent receives for a click: an ordinary inbound line, marked as a button. */
|
|
73
|
+
export function replyButtonClickText(b) {
|
|
74
|
+
return b.value === b.label ? `[button] ${b.label}` : `[button] ${b.label} (value: ${b.value})`;
|
|
75
|
+
}
|
|
76
|
+
/** Where buttons cannot be shown: the choices as text, so the question can still be answered by writing back. */
|
|
77
|
+
export function replyButtonsFallbackText(buttons) {
|
|
78
|
+
return `Options: ${buttons.map((b, i) => `${i + 1}) ${b.label}`).join(" ")} — reply with your choice.`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Is a platform click on this set's own message? Same adapter, same message id, and the click's chat or thread is the
|
|
82
|
+
* set's chat or thread (Discord reports a click as guild + channel, Telegram as chat + topic; a reply recorded its
|
|
83
|
+
* chat_id and thread). A callback copied onto another message, or replayed from another chat, is not.
|
|
84
|
+
*/
|
|
85
|
+
export function replyButtonsClickPlace(set, at) {
|
|
86
|
+
if (set.messageId === null || at.adapterId !== set.adapterId || at.messageId !== set.messageId)
|
|
87
|
+
return false;
|
|
88
|
+
const places = [set.chatId, set.threadId].filter(Boolean);
|
|
89
|
+
return [at.chatId, at.threadId].some(v => !!v && places.includes(v));
|
|
90
|
+
}
|
|
91
|
+
const fromRow = (r) => ({
|
|
92
|
+
id: r.id, instance: r.instance, adapterId: r.adapter_id, chatId: r.chat_id, threadId: r.thread_id, messageId: r.message_id,
|
|
93
|
+
buttons: JSON.parse(r.buttons), createdAt: r.created_at, expiresAt: r.expires_at, consumedAt: r.consumed_at,
|
|
94
|
+
chosenIndex: r.chosen_index, chosenBy: r.chosen_by, deliveredAt: r.delivered_at ?? null, settledAt: r.settled_at,
|
|
95
|
+
});
|
|
96
|
+
/** The button sets, in `<dataDir>/reply-buttons.db`. Wall-clock instants: an expiry is a calendar time that must hold across restarts. */
|
|
97
|
+
export class ReplyButtonStore {
|
|
98
|
+
db;
|
|
99
|
+
constructor(path) {
|
|
100
|
+
this.db = new Database(path);
|
|
101
|
+
this.db.pragma("journal_mode = WAL");
|
|
102
|
+
this.db.pragma("busy_timeout = 5000");
|
|
103
|
+
this.db.exec(`
|
|
104
|
+
CREATE TABLE IF NOT EXISTS reply_buttons (
|
|
105
|
+
id TEXT PRIMARY KEY,
|
|
106
|
+
instance TEXT NOT NULL,
|
|
107
|
+
adapter_id TEXT NOT NULL,
|
|
108
|
+
chat_id TEXT NOT NULL,
|
|
109
|
+
thread_id TEXT NOT NULL DEFAULT '',
|
|
110
|
+
message_id TEXT,
|
|
111
|
+
buttons TEXT NOT NULL,
|
|
112
|
+
created_at INTEGER NOT NULL,
|
|
113
|
+
expires_at INTEGER NOT NULL,
|
|
114
|
+
consumed_at INTEGER,
|
|
115
|
+
chosen_index INTEGER,
|
|
116
|
+
chosen_by TEXT,
|
|
117
|
+
settled_at INTEGER,
|
|
118
|
+
delivered_at INTEGER
|
|
119
|
+
);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_reply_buttons_open ON reply_buttons(settled_at, expires_at);
|
|
121
|
+
`);
|
|
122
|
+
const columns = this.db.prepare("PRAGMA table_info(reply_buttons)").all().map(c => c.name);
|
|
123
|
+
if (!columns.includes("delivered_at"))
|
|
124
|
+
this.db.exec("ALTER TABLE reply_buttons ADD COLUMN delivered_at INTEGER");
|
|
125
|
+
}
|
|
126
|
+
close() { this.db.close(); }
|
|
127
|
+
/** Before the send: the set exists, unbound (a click cannot match it until the platform message is known). */
|
|
128
|
+
create(set, now) {
|
|
129
|
+
this.db.prepare(`INSERT INTO reply_buttons (id, instance, adapter_id, chat_id, thread_id, buttons, created_at, expires_at)
|
|
130
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(set.id, set.instance, set.adapterId, set.chatId, set.threadId ?? "", JSON.stringify(set.buttons), now, now + REPLY_BUTTON_TTL_MS);
|
|
131
|
+
return this.get(set.id);
|
|
132
|
+
}
|
|
133
|
+
/** After the send: the message the buttons are on. Only an unbound set can be bound. */
|
|
134
|
+
bind(id, messageId) {
|
|
135
|
+
return this.db.prepare("UPDATE reply_buttons SET message_id = ? WHERE id = ? AND message_id IS NULL").run(messageId, id).changes === 1;
|
|
136
|
+
}
|
|
137
|
+
/** A send that failed: the set never existed for anyone. */
|
|
138
|
+
remove(id) { this.db.prepare("DELETE FROM reply_buttons WHERE id = ?").run(id); }
|
|
139
|
+
get(id) {
|
|
140
|
+
const r = this.db.prepare("SELECT * FROM reply_buttons WHERE id = ?").get(id);
|
|
141
|
+
return r ? fromRow(r) : null;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* One click, already placed on this set's message by the caller (replyButtonsClickPlace). The first permitted click
|
|
145
|
+
* consumes the whole set, atomically: of two clicks racing here only one changes the row.
|
|
146
|
+
*/
|
|
147
|
+
consume(id, index, by, now) {
|
|
148
|
+
const set = this.get(id);
|
|
149
|
+
if (!set)
|
|
150
|
+
return { ok: false, reason: "missing" };
|
|
151
|
+
if (set.messageId === null)
|
|
152
|
+
return { ok: false, reason: "pending", set };
|
|
153
|
+
if (set.consumedAt !== null)
|
|
154
|
+
return { ok: false, reason: "used", set };
|
|
155
|
+
if (now >= set.expiresAt)
|
|
156
|
+
return { ok: false, reason: "expired", set };
|
|
157
|
+
const button = set.buttons[index];
|
|
158
|
+
if (!button)
|
|
159
|
+
return { ok: false, reason: "missing", set };
|
|
160
|
+
const won = this.db.prepare(`UPDATE reply_buttons SET consumed_at = ?, chosen_index = ?, chosen_by = ?
|
|
161
|
+
WHERE id = ? AND consumed_at IS NULL AND message_id IS NOT NULL AND expires_at > ?`).run(now, index, by, id, now).changes === 1;
|
|
162
|
+
const after = this.get(id);
|
|
163
|
+
return won ? { ok: true, set: after, button } : { ok: false, reason: after.consumedAt !== null ? "used" : "expired", set: after };
|
|
164
|
+
}
|
|
165
|
+
/** The claim reached the agent: from now on it is the choice. */
|
|
166
|
+
markDelivered(id, now) {
|
|
167
|
+
this.db.prepare("UPDATE reply_buttons SET delivered_at = ? WHERE id = ? AND consumed_at IS NOT NULL AND delivered_at IS NULL").run(now, id);
|
|
168
|
+
}
|
|
169
|
+
/** A consumed click that could not reach the agent: the set is open again (only that click's own claim is undone). */
|
|
170
|
+
release(id, consumedAt) {
|
|
171
|
+
this.db.prepare(`UPDATE reply_buttons SET consumed_at = NULL, chosen_index = NULL, chosen_by = NULL
|
|
172
|
+
WHERE id = ? AND consumed_at = ? AND delivered_at IS NULL AND settled_at IS NULL`).run(id, consumedAt);
|
|
173
|
+
}
|
|
174
|
+
/** Claims never delivered nor released — in flight now, or left by a process that stopped mid-delivery. */
|
|
175
|
+
undeliveredClaims() {
|
|
176
|
+
return this.db.prepare(`SELECT * FROM reply_buttons WHERE settled_at IS NULL AND message_id IS NOT NULL
|
|
177
|
+
AND consumed_at IS NOT NULL AND delivered_at IS NULL ORDER BY created_at, id`).all().map(fromRow);
|
|
178
|
+
}
|
|
179
|
+
/** The platform message shows the final state now (chosen or expired). */
|
|
180
|
+
markSettled(id, now) { this.db.prepare("UPDATE reply_buttons SET settled_at = ? WHERE id = ? AND settled_at IS NULL").run(now, id); }
|
|
181
|
+
/**
|
|
182
|
+
* Sets that ended and whose message does not show it yet: a delivered choice, or an expiry nobody claimed. A claim
|
|
183
|
+
* still being delivered is neither (#1500 review): it may yet be released.
|
|
184
|
+
*/
|
|
185
|
+
unsettled(now) {
|
|
186
|
+
return this.db.prepare(`SELECT * FROM reply_buttons WHERE settled_at IS NULL AND message_id IS NOT NULL
|
|
187
|
+
AND (delivered_at IS NOT NULL OR (consumed_at IS NULL AND expires_at <= ?))`).all(now).map(fromRow);
|
|
188
|
+
}
|
|
189
|
+
/** The next expiry still to come, for the one sweep timer. */
|
|
190
|
+
nextExpiry(now) {
|
|
191
|
+
const r = this.db.prepare(`SELECT MIN(expires_at) AS t FROM reply_buttons WHERE settled_at IS NULL AND consumed_at IS NULL
|
|
192
|
+
AND message_id IS NOT NULL AND expires_at > ?`).get(now);
|
|
193
|
+
return r.t ?? null;
|
|
194
|
+
}
|
|
195
|
+
/** Sets on these messages, for the web chat's view of a reply's buttons. */
|
|
196
|
+
forMessages(instance, messageIds) {
|
|
197
|
+
if (!messageIds.length)
|
|
198
|
+
return [];
|
|
199
|
+
const rows = this.db.prepare(`SELECT * FROM reply_buttons WHERE instance = ? AND message_id IN (${messageIds.map(() => "?").join(",")})`)
|
|
200
|
+
.all(instance, ...messageIds);
|
|
201
|
+
return rows.map(fromRow);
|
|
202
|
+
}
|
|
203
|
+
/** Settled sets older than `before` are history the store no longer needs. */
|
|
204
|
+
prune(before) {
|
|
205
|
+
return this.db.prepare("DELETE FROM reply_buttons WHERE settled_at IS NOT NULL AND settled_at < ?").run(before).changes;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=reply-buttons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reply-buttons.js","sourceRoot":"","sources":["../src/reply-buttons.ts"],"names":[],"mappings":"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"}
|
package/dist/reply-dedup.js
CHANGED
|
@@ -28,7 +28,9 @@ import { createHash } from "node:crypto";
|
|
|
28
28
|
export function replyDedupText(args) {
|
|
29
29
|
const text = String(args.text ?? "");
|
|
30
30
|
const stickers = Array.isArray(args.stickers) ? args.stickers.map(String) : [];
|
|
31
|
-
|
|
31
|
+
const keyed = stickers.length ? `${text}\u0000stickers:${stickers.join(",")}` : text;
|
|
32
|
+
// #1266: the same text with other buttons is another reply.
|
|
33
|
+
return Array.isArray(args.buttons) && args.buttons.length ? `${keyed}\u0000buttons:${JSON.stringify(args.buttons)}` : keyed;
|
|
32
34
|
}
|
|
33
35
|
export class ReplyDeduper {
|
|
34
36
|
inFlightTimeoutMs;
|