@songsid/agend 2.2.0-alpha.6 → 2.2.0-alpha.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/ansi-faint.d.ts +25 -0
- package/dist/ansi-faint.js +90 -0
- package/dist/ansi-faint.js.map +1 -0
- package/dist/fleet-manager.d.ts +12 -0
- package/dist/fleet-manager.js +116 -28
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +11 -2
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +2 -1
- package/dist/locale.js +4 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.js +8 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/tz-utils.d.ts +6 -0
- package/dist/tz-utils.js +23 -0
- package/dist/tz-utils.js.map +1 -1
- package/dist/ui/chat-render.js +29 -1
- package/dist/ui/chat-strings.js +2 -0
- package/dist/ui/chat-thread.js +128 -3
- package/dist/ui/panel-chat.js +1 -0
- package/dist/ui/shared/app-i18n.js +2 -0
- package/dist/ui/shared/app-shell.js +9 -1
- package/dist/ui/shared/app-stream.js +186 -43
- package/dist/ui/shared/app.css +16 -0
- package/dist/ui/shared/app.js +6 -0
- package/dist/ui/shared/panel-view.js +21 -8
- package/dist/ui/shared/usage-dialog.js +16 -1
- package/dist/ui/shared/view-strings.js +10 -0
- package/dist/usage/usage-api.d.ts +8 -1
- package/dist/usage/usage-api.js +100 -31
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/web-api.d.ts +1 -1
- package/dist/web-api.js +67 -4
- package/dist/web-api.js.map +1 -1
- package/dist/web-chat-history.d.ts +7 -0
- package/dist/web-chat-history.js +19 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-host-guard.d.ts +2 -0
- package/dist/web-host-guard.js +7 -1
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-upload.d.ts +17 -0
- package/dist/web-upload.js +38 -7
- package/dist/web-upload.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1582 (suzuke/agend-terminal#3744): text a terminal shows FAINT (SGR 2) is not input. Claude Code paints its prompt
|
|
3
|
+
* suggestion and its empty-box placeholder in the composer exactly like typed text, only faint; an agent reading
|
|
4
|
+
* another instance's raw stream (`get_instance_logs`) gets that attribute only as `ESC[2m` buried among cursor moves,
|
|
5
|
+
* and would read the suggestion as something the operator typed.
|
|
6
|
+
*
|
|
7
|
+
* `markFaintRuns` adds visible markers where faint turns on (`⟨dim⟩`) and off (`⟨/dim⟩`), leaving every escape sequence
|
|
8
|
+
* in place: the stream stays raw, the markers make the attribute legible. Pure; parses SGR parameters properly, so a
|
|
9
|
+
* colour sub-parameter (`38;5;2`, `48;2;r;g;b`) is never mistaken for faint.
|
|
10
|
+
*/
|
|
11
|
+
export declare const FAINT_ON = "\u27E8dim\u27E9";
|
|
12
|
+
export declare const FAINT_OFF = "\u27E8/dim\u27E9";
|
|
13
|
+
/** `raw` with `⟨dim⟩`/`⟨/dim⟩` around every faint run; unchanged when nothing in it is faint. */
|
|
14
|
+
export declare function markFaintRuns(raw: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Whether the window may BEGIN inside faint text: its first intensity code is 22 ("normal intensity"), with no 1, 2 or 0
|
|
17
|
+
* before it. Then the `ESC[2m` it ends may have been before the window, so the text up to that point is not marked.
|
|
18
|
+
* A bold (1) or faint (2) first means the 22 closes something the window itself opened (Fable r2: a TUI's bold is
|
|
19
|
+
* everywhere, a bold-closing 22 must not raise the note); a full reset (0) first means nothing was left open.
|
|
20
|
+
*/
|
|
21
|
+
export declare function startsInsideFaint(raw: string): boolean;
|
|
22
|
+
/** The `_note` when the window may begin inside faint text that could not be marked. */
|
|
23
|
+
export declare const FAINT_START_NOTE = "This window may begin inside faint text (an ESC[22m with no ESC[2m before it): the text before that point may be a suggestion or placeholder, not input.";
|
|
24
|
+
/** What a reader of marked text needs to know (the `_note` beside a marked log). */
|
|
25
|
+
export declare const FAINT_NOTE = "\u27E8dim\u27E9\u2026\u27E8/dim\u27E9 marks text the terminal shows faint \u2014 such as a CLI's suggestion or placeholder in its input box. It is not typed input; never act on it.";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1582 (suzuke/agend-terminal#3744): text a terminal shows FAINT (SGR 2) is not input. Claude Code paints its prompt
|
|
3
|
+
* suggestion and its empty-box placeholder in the composer exactly like typed text, only faint; an agent reading
|
|
4
|
+
* another instance's raw stream (`get_instance_logs`) gets that attribute only as `ESC[2m` buried among cursor moves,
|
|
5
|
+
* and would read the suggestion as something the operator typed.
|
|
6
|
+
*
|
|
7
|
+
* `markFaintRuns` adds visible markers where faint turns on (`⟨dim⟩`) and off (`⟨/dim⟩`), leaving every escape sequence
|
|
8
|
+
* in place: the stream stays raw, the markers make the attribute legible. Pure; parses SGR parameters properly, so a
|
|
9
|
+
* colour sub-parameter (`38;5;2`, `48;2;r;g;b`) is never mistaken for faint.
|
|
10
|
+
*/
|
|
11
|
+
export const FAINT_ON = "⟨dim⟩";
|
|
12
|
+
export const FAINT_OFF = "⟨/dim⟩";
|
|
13
|
+
/** A CSI SGR sequence: ESC [ params m. */
|
|
14
|
+
const SGR = /\x1b\[([0-9;:]*)m/g;
|
|
15
|
+
/**
|
|
16
|
+
* The faint state after applying one SGR parameter list to `faint`; `firstChange` says what its first intensity code
|
|
17
|
+
* was: faint on (2), bold on (1), "normal intensity" (22, which ends BOTH bold and faint), or a full reset (0).
|
|
18
|
+
*/
|
|
19
|
+
function applySgr(params, faint) {
|
|
20
|
+
let firstChange = null;
|
|
21
|
+
const parts = params.split(";");
|
|
22
|
+
for (let i = 0; i < parts.length; i++) {
|
|
23
|
+
const part = parts[i];
|
|
24
|
+
// An empty parameter is 0: ESC[m, like ESC[0m, is a full reset.
|
|
25
|
+
const c = part === "" ? 0 : Number(part.split(":")[0]);
|
|
26
|
+
if (c === 0 || c === 22) {
|
|
27
|
+
faint = false;
|
|
28
|
+
firstChange ??= c === 22 ? "off22" : "off0";
|
|
29
|
+
}
|
|
30
|
+
else if (c === 2) {
|
|
31
|
+
faint = true;
|
|
32
|
+
firstChange ??= "faint";
|
|
33
|
+
}
|
|
34
|
+
else if (c === 1)
|
|
35
|
+
firstChange ??= "bold";
|
|
36
|
+
else if ((c === 38 || c === 48 || c === 58) && !part.includes(":")) {
|
|
37
|
+
// Extended colour, semicolon form: 5;N (256 colours) or 2;R;G;B (truecolour) belong to this code, not to the
|
|
38
|
+
// attribute list. The colon form (ITU T.416: `38:5:N`, `38:2::R:G:B`) carries them inside this one element.
|
|
39
|
+
if (parts[i + 1] === "5")
|
|
40
|
+
i += 2;
|
|
41
|
+
else if (parts[i + 1] === "2")
|
|
42
|
+
i += 4;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return { faint, firstChange };
|
|
46
|
+
}
|
|
47
|
+
/** `raw` with `⟨dim⟩`/`⟨/dim⟩` around every faint run; unchanged when nothing in it is faint. */
|
|
48
|
+
export function markFaintRuns(raw) {
|
|
49
|
+
let out = "", last = 0, faint = false, marked = false;
|
|
50
|
+
for (const m of raw.matchAll(SGR)) {
|
|
51
|
+
const end = m.index + m[0].length;
|
|
52
|
+
const next = applySgr(m[1], faint).faint;
|
|
53
|
+
out += raw.slice(last, m.index);
|
|
54
|
+
if (next && !faint) {
|
|
55
|
+
out += FAINT_ON + m[0];
|
|
56
|
+
marked = true;
|
|
57
|
+
}
|
|
58
|
+
else if (!next && faint)
|
|
59
|
+
out += FAINT_OFF + m[0];
|
|
60
|
+
else
|
|
61
|
+
out += m[0];
|
|
62
|
+
faint = next;
|
|
63
|
+
last = end;
|
|
64
|
+
}
|
|
65
|
+
if (!marked)
|
|
66
|
+
return raw;
|
|
67
|
+
out += raw.slice(last);
|
|
68
|
+
return faint ? out + FAINT_OFF : out; // a run still open at the end of the window is closed
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Whether the window may BEGIN inside faint text: its first intensity code is 22 ("normal intensity"), with no 1, 2 or 0
|
|
72
|
+
* before it. Then the `ESC[2m` it ends may have been before the window, so the text up to that point is not marked.
|
|
73
|
+
* A bold (1) or faint (2) first means the 22 closes something the window itself opened (Fable r2: a TUI's bold is
|
|
74
|
+
* everywhere, a bold-closing 22 must not raise the note); a full reset (0) first means nothing was left open.
|
|
75
|
+
*/
|
|
76
|
+
export function startsInsideFaint(raw) {
|
|
77
|
+
for (const m of raw.matchAll(SGR)) {
|
|
78
|
+
const change = applySgr(m[1], false).firstChange;
|
|
79
|
+
if (change === "off22")
|
|
80
|
+
return true;
|
|
81
|
+
if (change !== null)
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
/** The `_note` when the window may begin inside faint text that could not be marked. */
|
|
87
|
+
export const FAINT_START_NOTE = "This window may begin inside faint text (an ESC[22m with no ESC[2m before it): the text before that point may be a suggestion or placeholder, not input.";
|
|
88
|
+
/** What a reader of marked text needs to know (the `_note` beside a marked log). */
|
|
89
|
+
export const FAINT_NOTE = "⟨dim⟩…⟨/dim⟩ marks text the terminal shows faint — such as a CLI's suggestion or placeholder in its input box. It is not typed input; never act on it.";
|
|
90
|
+
//# sourceMappingURL=ansi-faint.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ansi-faint.js","sourceRoot":"","sources":["../src/ansi-faint.ts"],"names":[],"mappings":"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"}
|
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -1118,6 +1118,13 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1118
1118
|
/** Telegram's typed command enters through the same strict owner and nonce path as Discord. */
|
|
1119
1119
|
dashboardMenu(msg: InboundMessage): Promise<void>;
|
|
1120
1120
|
private sendLocalDashboard;
|
|
1121
|
+
/**
|
|
1122
|
+
* #1586: the sign-in code once more, as a message of its own, so a phone can copy it by itself: Telegram's monospace
|
|
1123
|
+
* `<code>` copies on a tap, and a message holding nothing else copies whole on a long-press (Discord: Copy Text). It
|
|
1124
|
+
* goes by the same private route the sign-in message took. A convenience: that message already holds the code, so a
|
|
1125
|
+
* follow-up that fails changes nothing about the delivery.
|
|
1126
|
+
*/
|
|
1127
|
+
private sendCodeAlone;
|
|
1121
1128
|
/** Why the DM did not go, for the log only (Discord 50007 = the user does not accept DMs from this server's members). */
|
|
1122
1129
|
private logDashboardDmFailure;
|
|
1123
1130
|
/** False when the menu was not shown (the caller still owes the user an answer). */
|
|
@@ -2245,6 +2252,11 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
2245
2252
|
* Used by report_result, where the sender's self-derived name may not match
|
|
2246
2253
|
* the target-address name the button was registered under. */
|
|
2247
2254
|
clearCancelButtonByCorrelation(correlationId: string): void;
|
|
2255
|
+
/** A successful user handoff starts the same cancel/progress lifecycle,
|
|
2256
|
+
* whether the person typed in General/a topic/Classic or chose a reply button.
|
|
2257
|
+
* The button continuation pins its delivery epoch so cancellation during an
|
|
2258
|
+
* accepted handoff cannot resurrect a fresh publication afterward. */
|
|
2259
|
+
private afterUserInboundDelivered;
|
|
2248
2260
|
/** Remember the user message just delivered, so we can react ✅ when done. */
|
|
2249
2261
|
private trackInboundMsg;
|
|
2250
2262
|
/** Clear the tracked last-inbound message after the agent replies. The ✅
|
package/dist/fleet-manager.js
CHANGED
|
@@ -51,6 +51,7 @@ import { startEventLoopWatch } from "./event-loop-watch.js";
|
|
|
51
51
|
import { TmuxManager } from "./tmux-manager.js";
|
|
52
52
|
import { AccessManager } from "./channel/access-manager.js";
|
|
53
53
|
import { IpcClient } from "./channel/ipc-bridge.js";
|
|
54
|
+
import { formatFleetTime } from "./tz-utils.js";
|
|
54
55
|
import { createAdapter } from "./channel/factory.js";
|
|
55
56
|
import { isWebChannelEcho, WEB_ECHO_PREFIX } from "./web-channel-echo.js";
|
|
56
57
|
import { TelegramAdapter } from "./channel/adapters/telegram.js";
|
|
@@ -1123,21 +1124,52 @@ export class FleetManager {
|
|
|
1123
1124
|
async deliverReplyButtonChoice(set, button, by) {
|
|
1124
1125
|
const content = replyButtonClickText(button);
|
|
1125
1126
|
const web = by.source === "web";
|
|
1126
|
-
const
|
|
1127
|
+
const timestamp = new Date();
|
|
1128
|
+
const ts = timestamp.toISOString();
|
|
1127
1129
|
const messageId = web ? newWebMessageId() : (set.messageId ?? "");
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
if (
|
|
1130
|
+
const deliveryEpoch = this.getDeliveryEpoch(set.instance);
|
|
1131
|
+
const inbound = {
|
|
1132
|
+
chatId: set.chatId, threadId: set.threadId || undefined, messageId,
|
|
1133
|
+
username: by.username, adapterId: set.adapterId === "web" ? undefined : set.adapterId,
|
|
1134
|
+
source: web ? "web" : (this.worlds.get(set.adapterId)?.adapter.type ?? "web"),
|
|
1135
|
+
};
|
|
1136
|
+
// A platform click has no new human message: its receipt and subsequent
|
|
1137
|
+
// daemon delivery statuses belong on the reply carrying these buttons.
|
|
1138
|
+
// Telegram needs the click time to establish status-slot ownership. Web
|
|
1139
|
+
// ids go only to the dashboard's delivery lane, never a platform react API.
|
|
1140
|
+
if (inbound.chatId && messageId) {
|
|
1141
|
+
this.reactMessageStatus(set.instance, inbound.chatId, messageId, "received", inbound.threadId, timestamp.getTime());
|
|
1142
|
+
}
|
|
1143
|
+
if (this.fleetConfig?.instances[set.instance] && set.threadId) {
|
|
1144
|
+
this.touchActivity(set.instance);
|
|
1145
|
+
this.setTopicIcon(set.instance, "blue");
|
|
1146
|
+
}
|
|
1147
|
+
const reactions = this.pendingReactionsMeta(set.instance);
|
|
1148
|
+
let accepted = false;
|
|
1149
|
+
try {
|
|
1150
|
+
accepted = (await this.deliverToInstance(set.instance, {
|
|
1151
|
+
type: "fleet_inbound",
|
|
1152
|
+
content,
|
|
1153
|
+
targetSession: set.instance,
|
|
1154
|
+
meta: {
|
|
1155
|
+
chat_id: set.chatId, message_id: messageId, user: by.username, user_id: by.userId, ts,
|
|
1156
|
+
thread_id: set.threadId, adapter_id: inbound.adapterId, source: inbound.source,
|
|
1157
|
+
...reactions.meta,
|
|
1158
|
+
},
|
|
1159
|
+
})) !== false;
|
|
1160
|
+
}
|
|
1161
|
+
finally {
|
|
1162
|
+
// Both false and throw reopen the choice in the controller. Finish the
|
|
1163
|
+
// receipt through the same reconciler as typed input, rather than leaving
|
|
1164
|
+
// a received reaction on a message that never reached the agent.
|
|
1165
|
+
if (!accepted && inbound.chatId && messageId) {
|
|
1166
|
+
this.finishDeliveryStatus(set.instance, inbound.chatId, messageId, "failed", inbound.threadId);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
if (!accepted)
|
|
1139
1170
|
return false;
|
|
1140
|
-
|
|
1171
|
+
reactions.consume();
|
|
1172
|
+
this.afterUserInboundDelivered(set.instance, inbound, deliveryEpoch);
|
|
1141
1173
|
this.emitSseEvent("message", {
|
|
1142
1174
|
instance: set.instance, sender: by.username, role: "user", text: content, ts, ...(web ? { messageId } : {}),
|
|
1143
1175
|
});
|
|
@@ -4654,7 +4686,7 @@ export class FleetManager {
|
|
|
4654
4686
|
try {
|
|
4655
4687
|
if (!adapter.sendDirect)
|
|
4656
4688
|
throw new Error("DM unavailable");
|
|
4657
|
-
await withinBudget(adapter.sendDirect(data.userId, privateText
|
|
4689
|
+
await withinBudget(adapter.sendDirect(data.userId, `${privateText}\n${t("dashboard.code_next")}`, { disablePreview: true }), Math.min(deadline, performance.now() + 5_000));
|
|
4658
4690
|
deliveredByDm = true;
|
|
4659
4691
|
}
|
|
4660
4692
|
catch (err) {
|
|
@@ -4667,8 +4699,15 @@ export class FleetManager {
|
|
|
4667
4699
|
}
|
|
4668
4700
|
if (!current())
|
|
4669
4701
|
throw new Error("owner changed");
|
|
4670
|
-
|
|
4671
|
-
|
|
4702
|
+
// The slash command's own reply (editReply) is ONE message: only the DM gets the code on its own (#1586). Its
|
|
4703
|
+
// wait has its own budget, so the notice below never runs out of time because of it; and the owner is asked
|
|
4704
|
+
// again after it — an owner gone during that wait takes the code with them.
|
|
4705
|
+
if (deliveredByDm) {
|
|
4706
|
+
await this.sendCodeAlone((body, opts) => adapter.sendDirect(data.userId, body, opts), adapter.type, login.display);
|
|
4707
|
+
if (!current())
|
|
4708
|
+
throw new Error("owner changed");
|
|
4709
|
+
await withinBudget(data.respond(t("dashboard.private_sent_dm")), performance.now() + 5_000);
|
|
4710
|
+
}
|
|
4672
4711
|
}
|
|
4673
4712
|
catch (err) {
|
|
4674
4713
|
this.webLoginCodes.revokeIfCurrent(login.issuanceId);
|
|
@@ -4676,6 +4715,23 @@ export class FleetManager {
|
|
|
4676
4715
|
await data.respond(t("dashboard.private_failed")).catch(() => undefined);
|
|
4677
4716
|
}
|
|
4678
4717
|
}
|
|
4718
|
+
/**
|
|
4719
|
+
* #1586: the sign-in code once more, as a message of its own, so a phone can copy it by itself: Telegram's monospace
|
|
4720
|
+
* `<code>` copies on a tap, and a message holding nothing else copies whole on a long-press (Discord: Copy Text). It
|
|
4721
|
+
* goes by the same private route the sign-in message took. A convenience: that message already holds the code, so a
|
|
4722
|
+
* follow-up that fails changes nothing about the delivery.
|
|
4723
|
+
*/
|
|
4724
|
+
async sendCodeAlone(send, platform, display) {
|
|
4725
|
+
const telegram = platform === "telegram";
|
|
4726
|
+
const body = telegram ? `<code>${display.replace(/[&<>]/g, c => (c === "&" ? "&" : c === "<" ? "<" : ">"))}</code>` : display;
|
|
4727
|
+
try {
|
|
4728
|
+
// Its own 5 s, never the sign-in delivery's budget: a slow convenience must not fail a delivery already confirmed.
|
|
4729
|
+
await withinBudget(send(body, telegram ? { format: "html", disablePreview: true } : { disablePreview: true }), performance.now() + 5_000);
|
|
4730
|
+
}
|
|
4731
|
+
catch (err) {
|
|
4732
|
+
this.logger.info({ err: err?.message }, "The code-only follow-up was not sent; the sign-in message holds the code");
|
|
4733
|
+
}
|
|
4734
|
+
}
|
|
4679
4735
|
/** Why the DM did not go, for the log only (Discord 50007 = the user does not accept DMs from this server's members). */
|
|
4680
4736
|
logDashboardDmFailure(err) {
|
|
4681
4737
|
const code = err?.code;
|
|
@@ -4692,7 +4748,8 @@ export class FleetManager {
|
|
|
4692
4748
|
const publicAllowed = publicLinkSettings(this.fleetConfig?.web).allowed;
|
|
4693
4749
|
const menuText = this.topicCommands.getDashboardText(false, !publicAllowed)
|
|
4694
4750
|
+ (publicAllowed ? "\n\n" + t("dashboard.public_risk") : "")
|
|
4695
|
-
+ "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
|
|
4751
|
+
+ "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
|
|
4752
|
+
+ (status.expiresAt ? t("dashboard.public_until", formatFleetTime(status.expiresAt)) : "");
|
|
4696
4753
|
const choices = [{ action: "local", label: t("dashboard.local") },
|
|
4697
4754
|
...(publicLinkSettings(this.fleetConfig?.web).allowed ? [{ action: "public", label: t("dashboard.public_open") }] : []),
|
|
4698
4755
|
...(status.state !== "closed" ? [{ action: "close", label: t("dashboard.public_close") }] : [])];
|
|
@@ -4755,9 +4812,13 @@ export class FleetManager {
|
|
|
4755
4812
|
return false;
|
|
4756
4813
|
this.initializeWebSessions();
|
|
4757
4814
|
const issued = this.webLoginCodes.issue({ epoch: tokenEpoch(token), audience: exposureId, owner });
|
|
4758
|
-
|
|
4815
|
+
// #1586: the expiry in the fleet's zone with its offset, never a bare UTC ISO string.
|
|
4816
|
+
const text = t("dashboard.private_link", url, issued.display, Math.round(LOGIN_CODE_TTL_MS / 60_000), expiresAt ? formatFleetTime(expiresAt) : t("dashboard.local"))
|
|
4817
|
+
+ "\n" + t("dashboard.code_next");
|
|
4759
4818
|
const deadline = performance.now() + 10_000;
|
|
4760
4819
|
const direct = owner.binding;
|
|
4820
|
+
// The route the sign-in message took: the code-only follow-up takes the same one (#1586).
|
|
4821
|
+
let codeAlone = null;
|
|
4761
4822
|
try {
|
|
4762
4823
|
const deliver = async (c = []) => {
|
|
4763
4824
|
try {
|
|
@@ -4765,6 +4826,7 @@ export class FleetManager {
|
|
|
4765
4826
|
throw new Error("DM unavailable");
|
|
4766
4827
|
const sent = await withinBudget(direct.sendDirect(owner.userId, text, { disablePreview: true, choices: c }), Math.min(deadline, performance.now() + 5_000));
|
|
4767
4828
|
deliveredByDm = true;
|
|
4829
|
+
codeAlone = (body, opts) => direct.sendDirect(owner.userId, body, opts);
|
|
4768
4830
|
return sent;
|
|
4769
4831
|
}
|
|
4770
4832
|
catch (err) {
|
|
@@ -4772,6 +4834,7 @@ export class FleetManager {
|
|
|
4772
4834
|
if (direct.type !== "discord" || !data.respondPrivate || !current())
|
|
4773
4835
|
throw new Error("private delivery unavailable");
|
|
4774
4836
|
const sent = await withinBudget(data.respondPrivate(text, c), deadline);
|
|
4837
|
+
codeAlone = body => data.respondPrivate(body);
|
|
4775
4838
|
return { ...sent, chatId: owner.chatId, threadId: owner.threadId };
|
|
4776
4839
|
}
|
|
4777
4840
|
};
|
|
@@ -4785,6 +4848,12 @@ export class FleetManager {
|
|
|
4785
4848
|
await deliver();
|
|
4786
4849
|
if (!current() || this.webToken !== token)
|
|
4787
4850
|
throw new Error("owner changed");
|
|
4851
|
+
if (codeAlone) {
|
|
4852
|
+
await this.sendCodeAlone(codeAlone, direct.type, issued.display);
|
|
4853
|
+
// The owner (and a public link's exposure, through `current`) and the token are asked again after that wait.
|
|
4854
|
+
if (!current() || this.webToken !== token)
|
|
4855
|
+
throw new Error("owner changed");
|
|
4856
|
+
}
|
|
4788
4857
|
return true;
|
|
4789
4858
|
}
|
|
4790
4859
|
catch (err) {
|
|
@@ -4878,7 +4947,7 @@ export class FleetManager {
|
|
|
4878
4947
|
else if (input.exposureId !== undefined)
|
|
4879
4948
|
return { kind: "refused" };
|
|
4880
4949
|
const issued = this.webLoginCodes.issue({ epoch: tokenEpoch(token), audience: input.exposureId ?? "local", owner });
|
|
4881
|
-
const text = t("web.resend_code", issued.display, Math.round(LOGIN_CODE_TTL_MS / 60_000), input.label || t("web.resend_unknown_browser"), t(input.surface === "gateway" ? "web.resend_public" : "web.resend_local"));
|
|
4950
|
+
const text = t("web.resend_code", issued.display, Math.round(LOGIN_CODE_TTL_MS / 60_000), input.label || t("web.resend_unknown_browser"), t(input.surface === "gateway" ? "web.resend_public" : "web.resend_local")) + "\n" + t("dashboard.code_next");
|
|
4882
4951
|
try {
|
|
4883
4952
|
await withinBudget(adapter.sendDirect(userId, text, { disablePreview: true }), performance.now() + 5_000);
|
|
4884
4953
|
}
|
|
@@ -4891,6 +4960,14 @@ export class FleetManager {
|
|
|
4891
4960
|
this.webLoginCodes.revokeIfCurrent(issued.issuanceId);
|
|
4892
4961
|
return { kind: "refused" };
|
|
4893
4962
|
}
|
|
4963
|
+
await this.sendCodeAlone((body, opts) => adapter.sendDirect(userId, body, opts), adapter.type, issued.display);
|
|
4964
|
+
// Asked again after the follow-up's wait: the owner, the token, shutdown, and a public request's exposure.
|
|
4965
|
+
const exposureGone = input.surface === "gateway"
|
|
4966
|
+
&& (this.publicWebLink?.exposureId !== input.exposureId || this.publicWebLink?.status().state !== "open");
|
|
4967
|
+
if (this.shuttingDown || this.webToken !== token || !this.publicOwnerCurrent(owner) || exposureGone) {
|
|
4968
|
+
this.webLoginCodes.revokeIfCurrent(issued.issuanceId);
|
|
4969
|
+
return { kind: "refused" };
|
|
4970
|
+
}
|
|
4894
4971
|
this.logger.info({ adapterId, surface: input.surface }, "Web sign-in code sent privately to a returning device's owner");
|
|
4895
4972
|
return { kind: "sent" };
|
|
4896
4973
|
}
|
|
@@ -7287,15 +7364,13 @@ export class FleetManager {
|
|
|
7287
7364
|
},
|
|
7288
7365
|
});
|
|
7289
7366
|
generalReactions.consume();
|
|
7290
|
-
this.
|
|
7367
|
+
this.afterUserInboundDelivered(generalInstance, msg);
|
|
7291
7368
|
this.logger.info(`${msg.username} → ${generalInstance}: ${(text ?? "").slice(0, 100)}`);
|
|
7292
7369
|
this.eventLog?.logActivity("message", msg.username, (text ?? "").slice(0, 200), generalInstance);
|
|
7293
7370
|
this.emitSseEvent("message", {
|
|
7294
7371
|
instance: generalInstance, sender: msg.username, role: "user",
|
|
7295
7372
|
text: (text ?? "").slice(0, WEB_CHAT_TEXT_MAX), ts: new Date().toISOString(),
|
|
7296
7373
|
});
|
|
7297
|
-
this.trackInboundMsg(generalInstance, msg);
|
|
7298
|
-
void this.sendCancelButton(generalInstance);
|
|
7299
7374
|
}
|
|
7300
7375
|
catch (err) {
|
|
7301
7376
|
this.logger.warn({ err: err.message, instanceName: generalInstance }, "General wake/delivery failed");
|
|
@@ -7401,15 +7476,13 @@ export class FleetManager {
|
|
|
7401
7476
|
}
|
|
7402
7477
|
return;
|
|
7403
7478
|
}
|
|
7404
|
-
this.
|
|
7479
|
+
this.afterUserInboundDelivered(instanceName, msg);
|
|
7405
7480
|
this.logger.info(`${msg.username} → ${instanceName}: ${(text ?? "").slice(0, 100)}`);
|
|
7406
7481
|
this.eventLog?.logActivity("message", msg.username, (text ?? "").slice(0, 200), instanceName);
|
|
7407
7482
|
this.emitSseEvent("message", {
|
|
7408
7483
|
instance: instanceName, sender: msg.username, role: "user",
|
|
7409
7484
|
text: (text ?? "").slice(0, WEB_CHAT_TEXT_MAX), ts: new Date().toISOString(),
|
|
7410
7485
|
});
|
|
7411
|
-
this.trackInboundMsg(instanceName, msg);
|
|
7412
|
-
void this.sendCancelButton(instanceName);
|
|
7413
7486
|
}
|
|
7414
7487
|
/** Handle outbound tool calls from a daemon instance */
|
|
7415
7488
|
/** Warn (but don't block) when rate limits are high. 30-min debounce per instance. */
|
|
@@ -12365,6 +12438,17 @@ export class FleetManager {
|
|
|
12365
12438
|
this.retireButton(e);
|
|
12366
12439
|
}
|
|
12367
12440
|
}
|
|
12441
|
+
/** A successful user handoff starts the same cancel/progress lifecycle,
|
|
12442
|
+
* whether the person typed in General/a topic/Classic or chose a reply button.
|
|
12443
|
+
* The button continuation pins its delivery epoch so cancellation during an
|
|
12444
|
+
* accepted handoff cannot resurrect a fresh publication afterward. */
|
|
12445
|
+
afterUserInboundDelivered(instanceName, msg, deliveryEpoch) {
|
|
12446
|
+
if (this.shuttingDown || (deliveryEpoch !== undefined && !this.isDeliveryEpochCurrent(instanceName, deliveryEpoch)))
|
|
12447
|
+
return;
|
|
12448
|
+
this.lastInboundUser.set(instanceName, msg.username);
|
|
12449
|
+
this.trackInboundMsg(instanceName, msg);
|
|
12450
|
+
void this.sendCancelButton(instanceName);
|
|
12451
|
+
}
|
|
12368
12452
|
/** Remember the user message just delivered, so we can react ✅ when done. */
|
|
12369
12453
|
trackInboundMsg(instanceName, msg) {
|
|
12370
12454
|
if (!msg.chatId || !msg.messageId)
|
|
@@ -13904,10 +13988,14 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
13904
13988
|
this.logger.warn({ err: err.message, instanceName }, "Classic wake/delivery failed");
|
|
13905
13989
|
return;
|
|
13906
13990
|
}
|
|
13907
|
-
this.
|
|
13991
|
+
this.afterUserInboundDelivered(instanceName, { ...msg, threadId: replyThreadId });
|
|
13908
13992
|
this.logger.info(`${msg.username} → ${instanceName} (classic): ${text.slice(0, 100)}`);
|
|
13909
|
-
|
|
13910
|
-
|
|
13993
|
+
// #1592: the web chat shows what was asked, as for every other inbound — the user's own words (no chat-log
|
|
13994
|
+
// context), as a user (#1306: never a role read from text), in the room's history (#1565).
|
|
13995
|
+
this.emitSseEvent("message", {
|
|
13996
|
+
instance: instanceName, sender: msg.username, role: "user",
|
|
13997
|
+
text: text.slice(0, WEB_CHAT_TEXT_MAX), ts: new Date().toISOString(),
|
|
13998
|
+
});
|
|
13911
13999
|
}
|
|
13912
14000
|
/** Paste raw text directly to a classic instance's CLI (no [user:] wrapping) */
|
|
13913
14001
|
pasteRawToClassicInstance(instanceName, text) {
|