talon-agent 5.25.0 → 5.26.0
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 +1 -0
- package/package.json +1 -1
- package/src/backend/agy/one-shot.ts +34 -3
- package/src/backend/codex/auth.ts +31 -2
- package/src/backend/codex/constants.ts +21 -7
- package/src/backend/codex/discovery.ts +32 -1
- package/src/backend/codex/factory.ts +3 -5
- package/src/backend/codex/handler/message.ts +7 -7
- package/src/backend/codex/models.ts +53 -11
- package/src/backend/codex/one-shot.ts +139 -37
- package/src/backend/codex/state.ts +11 -0
- package/src/core/agents/abort-reason.ts +46 -0
- package/src/core/agents/registry.ts +3 -2
- package/src/core/background/dream/index.ts +2 -2
- package/src/core/background/heartbeat/agent.ts +1 -1
- package/src/core/background/isolated-agent.ts +1 -1
- package/src/core/backup/index.ts +1 -0
- package/src/core/backup/status.ts +30 -1
- package/src/core/config/index.ts +8 -0
- package/src/core/engine/gateway.ts +5 -0
- package/src/core/mesh/credentials/store.ts +60 -9
- package/src/core/tools/bridge.ts +58 -4
- package/src/frontend/discord/callbacks/components/effort.ts +4 -4
- package/src/frontend/discord/callbacks/components/index.ts +2 -0
- package/src/frontend/discord/commands/backup-panel.ts +219 -0
- package/src/frontend/discord/commands/backup.ts +25 -34
- package/src/frontend/discord/commands/info.ts +17 -5
- package/src/frontend/discord/commands/settings.ts +12 -9
- package/src/frontend/discord/render.ts +1 -1
- package/src/frontend/native/bridge/credentials/claims.ts +5 -5
- package/src/frontend/native/bridge/routes/chats.ts +40 -20
- package/src/frontend/native/bridge/routes/host.ts +15 -2
- package/src/frontend/native/bridge/routes/table.ts +4 -0
- package/src/frontend/native/commands/admin.ts +64 -0
- package/src/frontend/native/commands/backup.ts +191 -0
- package/src/frontend/native/commands/definitions.ts +113 -0
- package/src/frontend/native/commands/format.ts +24 -0
- package/src/frontend/native/commands/index.ts +106 -0
- package/src/frontend/native/commands/info.ts +97 -0
- package/src/frontend/native/commands/session.ts +130 -0
- package/src/frontend/native/commands/types.ts +26 -0
- package/src/frontend/native/protocol.ts +18 -0
- package/src/frontend/native/surface/handlers.ts +14 -1
- package/src/frontend/native/surface/status.ts +9 -1
- package/src/frontend/native/turn/emit.ts +32 -1
- package/src/frontend/presentation/backup-panel.ts +425 -0
- package/src/frontend/presentation/memory-report.ts +109 -0
- package/src/frontend/presentation/text-commands.ts +243 -0
- package/src/frontend/telegram/admin/sessions.ts +20 -5
- package/src/frontend/telegram/admin.ts +9 -2
- package/src/frontend/telegram/callbacks/backup.ts +153 -22
- package/src/frontend/telegram/callbacks/effort.ts +5 -5
- package/src/frontend/telegram/callbacks/index.ts +2 -2
- package/src/frontend/telegram/callbacks/settings.ts +3 -40
- package/src/frontend/telegram/commands/admin.ts +8 -7
- package/src/frontend/telegram/commands/backup.ts +56 -46
- package/src/frontend/telegram/commands/index.ts +3 -2
- package/src/frontend/telegram/commands/info.ts +45 -26
- package/src/frontend/telegram/commands/memory.ts +8 -92
- package/src/frontend/telegram/commands/settings.ts +13 -45
- package/src/frontend/telegram/commands/whatsapp-pairing.ts +19 -15
- package/src/frontend/telegram/render/backup-panel.ts +47 -0
- package/src/frontend/telegram/render/menu.ts +21 -30
- package/src/frontend/terminal/builtins/model.ts +20 -11
- package/src/frontend/whatsapp/commands.ts +16 -216
- package/src/frontend/whatsapp/messages/inbound.ts +33 -2
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* /backup — the snapshot panel (admin only).
|
|
3
3
|
*
|
|
4
|
-
* /backup
|
|
4
|
+
* /backup the interactive panel: status + buttons
|
|
5
5
|
* /backup now take a snapshot now
|
|
6
6
|
* /backup checkpoint <label> take a labelled, pinned-on-request snapshot
|
|
7
7
|
* /backup list recent snapshots
|
|
8
8
|
* /backup pin|unpin <id> keep past retention, or release
|
|
9
9
|
* /backup restore <id> confirm button → staged restore + restart
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The bare command opens a panel (render/backup-panel.ts) whose buttons —
|
|
12
|
+
* Back up now, Snapshots, How restore works, Refresh — are handled in
|
|
13
|
+
* callbacks/backup.ts. Restore is the only destructive action, so it is
|
|
14
|
+
* always behind an explicit confirmation button. It does not restore in place: a running daemon holds the
|
|
13
15
|
* database it would replace, so the request is staged to
|
|
14
16
|
* ~/.talon/restore-pending.json and applied by the next boot before
|
|
15
17
|
* anything opens the database (see core/backup/restore.ts).
|
|
@@ -17,8 +19,6 @@
|
|
|
17
19
|
|
|
18
20
|
import type { Bot, Context } from "grammy";
|
|
19
21
|
import {
|
|
20
|
-
collectBackupStatus,
|
|
21
|
-
formatBackupStatus,
|
|
22
22
|
formatSnapshotList,
|
|
23
23
|
isSnapshotId,
|
|
24
24
|
listSnapshots,
|
|
@@ -29,13 +29,16 @@ import {
|
|
|
29
29
|
} from "../../../core/backup/index.js";
|
|
30
30
|
import { respawnSelf } from "../../../core/daemon/respawn.js";
|
|
31
31
|
import { escapeHtml } from "../formatting.js";
|
|
32
|
+
import { renderRestoreConfirm } from "../../presentation/backup-panel.js";
|
|
33
|
+
import { TELEGRAM_REPORTS } from "../render/html.js";
|
|
34
|
+
import { inlineKeyboard, loadStatusPanel } from "../render/backup-panel.js";
|
|
32
35
|
import { logError } from "../../../util/log.js";
|
|
33
36
|
import { isAuthorizedAdmin } from "./state.js";
|
|
34
37
|
|
|
35
38
|
const HELP = [
|
|
36
39
|
"<b>/backup</b> — snapshots and checkpoints",
|
|
37
40
|
"",
|
|
38
|
-
"<code>/backup</code> —
|
|
41
|
+
"<code>/backup</code> — the backup panel",
|
|
39
42
|
"<code>/backup now</code> — take a snapshot",
|
|
40
43
|
"<code>/backup checkpoint <label></code> — labelled checkpoint",
|
|
41
44
|
"<code>/backup list</code> — recent snapshots",
|
|
@@ -48,8 +51,11 @@ function pre(text: string): string {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
async function sendStatus(ctx: Context): Promise<void> {
|
|
51
|
-
const
|
|
52
|
-
await ctx.reply(
|
|
54
|
+
const view = await loadStatusPanel();
|
|
55
|
+
await ctx.reply(view.text, {
|
|
56
|
+
parse_mode: "HTML",
|
|
57
|
+
reply_markup: { inline_keyboard: inlineKeyboard(view.buttons) },
|
|
58
|
+
});
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
async function sendList(ctx: Context): Promise<void> {
|
|
@@ -57,13 +63,14 @@ async function sendList(ctx: Context): Promise<void> {
|
|
|
57
63
|
await ctx.reply(pre(formatSnapshotList(snapshots)), { parse_mode: "HTML" });
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Run one snapshot and describe the outcome as a single HTML line. Shared
|
|
68
|
+
* by `/backup now|checkpoint` and the panel's 📸 button, which edit their
|
|
69
|
+
* own messages with it. Never throws: a failure is the line.
|
|
70
|
+
*/
|
|
71
|
+
export async function runSnapshotForChat(
|
|
72
|
+
label?: string,
|
|
73
|
+
): Promise<{ ok: boolean; text: string }> {
|
|
67
74
|
try {
|
|
68
75
|
const manifest = await runBackup({
|
|
69
76
|
kind: label ? "checkpoint" : "backup",
|
|
@@ -71,25 +78,35 @@ async function takeSnapshot(ctx: Context, label?: string): Promise<void> {
|
|
|
71
78
|
pinned: Boolean(label),
|
|
72
79
|
trigger: "command",
|
|
73
80
|
});
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
return {
|
|
82
|
+
ok: true,
|
|
83
|
+
text:
|
|
84
|
+
`✅ <code>${escapeHtml(manifest.id)}</code> — ${manifest.parts.length} part(s), ` +
|
|
78
85
|
`${(manifest.sizeBytes / 1024 / 1024).toFixed(1)} MB` +
|
|
79
86
|
(label ? " (pinned)" : ""),
|
|
80
|
-
|
|
81
|
-
);
|
|
87
|
+
};
|
|
82
88
|
} catch (err) {
|
|
83
89
|
logError("backup", "/backup now failed", err);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
{ parse_mode: "HTML" },
|
|
89
|
-
);
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
text: `⚠️ Backup failed: ${escapeHtml(err instanceof Error ? err.message : String(err))}`,
|
|
93
|
+
};
|
|
90
94
|
}
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
async function takeSnapshot(ctx: Context, label?: string): Promise<void> {
|
|
98
|
+
const sent = await ctx.reply(
|
|
99
|
+
label
|
|
100
|
+
? `📸 Taking checkpoint “${escapeHtml(label)}”…`
|
|
101
|
+
: "📸 Taking a snapshot…",
|
|
102
|
+
{ parse_mode: "HTML" },
|
|
103
|
+
);
|
|
104
|
+
const result = await runSnapshotForChat(label);
|
|
105
|
+
await ctx.api.editMessageText(ctx.chat!.id, sent.message_id, result.text, {
|
|
106
|
+
parse_mode: "HTML",
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
async function setPinned(
|
|
94
111
|
ctx: Context,
|
|
95
112
|
id: string,
|
|
@@ -124,27 +141,20 @@ async function askToRestore(ctx: Context, id: string): Promise<void> {
|
|
|
124
141
|
);
|
|
125
142
|
return;
|
|
126
143
|
}
|
|
127
|
-
await ctx.reply(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
[
|
|
138
|
-
{
|
|
139
|
-
text: "♻️ Restore and restart",
|
|
140
|
-
callback_data: `backup:restore:${id}`,
|
|
141
|
-
},
|
|
142
|
-
{ text: "Cancel", callback_data: "backup:cancel" },
|
|
143
|
-
],
|
|
144
|
+
await ctx.reply(renderRestoreConfirm(TELEGRAM_REPORTS, manifest), {
|
|
145
|
+
parse_mode: "HTML",
|
|
146
|
+
reply_markup: {
|
|
147
|
+
inline_keyboard: [
|
|
148
|
+
[
|
|
149
|
+
{
|
|
150
|
+
text: "♻️ Restore and restart",
|
|
151
|
+
callback_data: `backup:restore:${id}`,
|
|
152
|
+
},
|
|
153
|
+
{ text: "Cancel", callback_data: "backup:cancel" },
|
|
144
154
|
],
|
|
145
|
-
|
|
155
|
+
],
|
|
146
156
|
},
|
|
147
|
-
);
|
|
157
|
+
});
|
|
148
158
|
}
|
|
149
159
|
|
|
150
160
|
/**
|
|
@@ -34,10 +34,11 @@ export { setAdminUserId } from "./state.js";
|
|
|
34
34
|
export function registerCommands(
|
|
35
35
|
bot: Bot,
|
|
36
36
|
config: TalonConfig,
|
|
37
|
-
gateway?: { backend: Backend | null },
|
|
37
|
+
gateway?: { backend: Backend | null; isListening?: () => boolean },
|
|
38
38
|
): void {
|
|
39
39
|
const deps = { config, gateway };
|
|
40
|
-
|
|
40
|
+
const isListening = gateway?.isListening?.bind(gateway);
|
|
41
|
+
registerInfoCommands(bot, { bridgeListening: isListening });
|
|
41
42
|
registerMemoryCommand(bot);
|
|
42
43
|
registerSessionCommands(bot, deps);
|
|
43
44
|
registerSettingsCommands(bot, deps);
|
|
@@ -16,7 +16,50 @@ import { getLoadedPlugins } from "../../../core/plugin/index.js";
|
|
|
16
16
|
import { getMeshService } from "../../../core/mesh/index.js";
|
|
17
17
|
import type { MeshPingResult } from "../../../core/mesh/devices/service.js";
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* `/ping` — Bot API round trip, plus live bridge and userbot state. The
|
|
21
|
+
* reply landing at all is the Bot API check; its round trip is the
|
|
22
|
+
* latency. The Bridge field is omitted when no health source is wired.
|
|
23
|
+
*/
|
|
24
|
+
function registerPingCommand(
|
|
25
|
+
bot: Bot,
|
|
26
|
+
bridgeListening: (() => boolean) | undefined,
|
|
27
|
+
): void {
|
|
28
|
+
bot.command("ping", async (ctx) => {
|
|
29
|
+
const start = Date.now();
|
|
30
|
+
const sent = await ctx.reply("...");
|
|
31
|
+
const latency = Date.now() - start;
|
|
32
|
+
|
|
33
|
+
const userbotOk = isUserClientReady();
|
|
34
|
+
const uptime = formatDuration(process.uptime() * 1000);
|
|
35
|
+
|
|
36
|
+
const statusLine = [
|
|
37
|
+
...(bridgeListening ? [`Bridge: ${bridgeListening() ? "✓" : "✗"}`] : []),
|
|
38
|
+
`Userbot: ${userbotOk ? "✓" : "✗"}`,
|
|
39
|
+
`Uptime: ${uptime}`,
|
|
40
|
+
].join(" | ");
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await bot.api.editMessageText(
|
|
44
|
+
ctx.chat.id,
|
|
45
|
+
sent.message_id,
|
|
46
|
+
`Pong! ${latency}ms\n${statusLine}`,
|
|
47
|
+
);
|
|
48
|
+
} catch {
|
|
49
|
+
// ignore edit failure
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type InfoCommandDeps = {
|
|
55
|
+
/** Tool-bridge health; the Bridge field is omitted when absent. */
|
|
56
|
+
bridgeListening?: () => boolean;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export function registerInfoCommands(
|
|
60
|
+
bot: Bot,
|
|
61
|
+
{ bridgeListening }: InfoCommandDeps = {},
|
|
62
|
+
): void {
|
|
20
63
|
bot.command("start", (ctx) =>
|
|
21
64
|
ctx.reply(
|
|
22
65
|
[
|
|
@@ -87,31 +130,7 @@ export function registerInfoCommands(bot: Bot): void {
|
|
|
87
130
|
),
|
|
88
131
|
);
|
|
89
132
|
|
|
90
|
-
bot
|
|
91
|
-
const start = Date.now();
|
|
92
|
-
const sent = await ctx.reply("...");
|
|
93
|
-
const latency = Date.now() - start;
|
|
94
|
-
|
|
95
|
-
const bridgeOk = true;
|
|
96
|
-
const userbotOk = isUserClientReady();
|
|
97
|
-
const uptime = formatDuration(process.uptime() * 1000);
|
|
98
|
-
|
|
99
|
-
const statusLine = [
|
|
100
|
-
`Bridge: ${bridgeOk ? "✓" : "✗"}`,
|
|
101
|
-
`Userbot: ${userbotOk ? "✓" : "✗"}`,
|
|
102
|
-
`Uptime: ${uptime}`,
|
|
103
|
-
].join(" | ");
|
|
104
|
-
|
|
105
|
-
try {
|
|
106
|
-
await bot.api.editMessageText(
|
|
107
|
-
ctx.chat.id,
|
|
108
|
-
sent.message_id,
|
|
109
|
-
`Pong! ${latency}ms\n${statusLine}`,
|
|
110
|
-
);
|
|
111
|
-
} catch {
|
|
112
|
-
// ignore edit failure
|
|
113
|
-
}
|
|
114
|
-
});
|
|
133
|
+
registerPingCommand(bot, bridgeListening);
|
|
115
134
|
|
|
116
135
|
/** True only in a 1:1 chat with the bot — never a group or channel. */
|
|
117
136
|
const isPrivate = (ctx: Context): boolean => ctx.chat?.type === "private";
|
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
* `/memory` — a read-only window on the typed memory store.
|
|
3
3
|
*
|
|
4
4
|
* Four shapes, all reads: the ranked listing, a full-text search, one
|
|
5
|
-
* row's provenance (`why <id>`) and a per-kind listing
|
|
6
|
-
*
|
|
7
|
-
* (rollout PR 6), so the operator can always ask what Talon remembers
|
|
8
|
-
* without the answer being able to change it.
|
|
5
|
+
* row's provenance (`why <id>`) and a per-kind listing — rendered by
|
|
6
|
+
* frontend/presentation/memory-report.ts, which the native bridge shares.
|
|
9
7
|
*
|
|
10
8
|
* Admin only, and only in a private chat. The store holds the operator's
|
|
11
9
|
* private notes — people, places, health, relationships — so a read of it
|
|
@@ -13,28 +11,16 @@
|
|
|
13
11
|
* reply, and anyone else who can reach the bot must not see it at all.
|
|
14
12
|
*
|
|
15
13
|
* Every line is model- or user-authored text reaching an HTML-parsed
|
|
16
|
-
* send, so
|
|
17
|
-
*
|
|
18
|
-
*
|
|
14
|
+
* send, so the report escapes it with the HTML dialect; the reply is
|
|
15
|
+
* chunked because a listing of 15 rows can outgrow Telegram's 4096-char
|
|
16
|
+
* cap on its own.
|
|
19
17
|
*/
|
|
20
18
|
|
|
21
19
|
import type { Bot, Context } from "grammy";
|
|
22
|
-
import { escapeHtml } from "../formatting.js";
|
|
23
20
|
import { replyHtmlChunked } from "../admin/chunked-reply.js";
|
|
21
|
+
import { TELEGRAM_REPORTS } from "../render/html.js";
|
|
22
|
+
import { renderMemoryReport } from "../../presentation/memory-report.js";
|
|
24
23
|
import { isAuthorizedAdmin } from "./state.js";
|
|
25
|
-
import {
|
|
26
|
-
formatMemory,
|
|
27
|
-
getMemory,
|
|
28
|
-
isMemoryKind,
|
|
29
|
-
listMemories,
|
|
30
|
-
memoryHistory,
|
|
31
|
-
searchMemories,
|
|
32
|
-
MEMORY_KINDS,
|
|
33
|
-
type MemoryRow,
|
|
34
|
-
} from "../../../storage/memory.js";
|
|
35
|
-
|
|
36
|
-
/** Rows per reply — a chat listing is a glance, not an export. */
|
|
37
|
-
const LIST_LIMIT = 15;
|
|
38
24
|
|
|
39
25
|
export function registerMemoryCommand(bot: Bot): void {
|
|
40
26
|
bot.command("memory", async (ctx: Context) => {
|
|
@@ -48,7 +34,7 @@ export function registerMemoryCommand(bot: Bot): void {
|
|
|
48
34
|
return;
|
|
49
35
|
}
|
|
50
36
|
const arg = (ctx.match ?? "").toString().trim();
|
|
51
|
-
await replyHtmlChunked(ctx,
|
|
37
|
+
await replyHtmlChunked(ctx, renderMemoryReport(arg, TELEGRAM_REPORTS));
|
|
52
38
|
});
|
|
53
39
|
}
|
|
54
40
|
|
|
@@ -62,73 +48,3 @@ function memoryAccess(ctx: Context): "ok" | "not-admin" | "not-private" {
|
|
|
62
48
|
if (ctx.chat?.type !== "private") return "not-private";
|
|
63
49
|
return "ok";
|
|
64
50
|
}
|
|
65
|
-
|
|
66
|
-
/** Route the argument to one of the four reads. Returns ready HTML. */
|
|
67
|
-
function renderMemory(arg: string): string {
|
|
68
|
-
const why = /^why\b\s*(.*)$/is.exec(arg);
|
|
69
|
-
if (why) return renderWhy(why[1]!.trim());
|
|
70
|
-
const kind = /^kind\b\s*(.*)$/is.exec(arg);
|
|
71
|
-
if (kind) return renderKind(kind[1]!.trim());
|
|
72
|
-
if (!arg)
|
|
73
|
-
return renderRows(
|
|
74
|
-
listMemories({ limit: LIST_LIMIT }),
|
|
75
|
-
"Nothing remembered yet.",
|
|
76
|
-
);
|
|
77
|
-
return renderRows(
|
|
78
|
-
searchMemories(arg, { limit: LIST_LIMIT }),
|
|
79
|
-
`No memories matching "${arg}".`,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/** One escaped line per row, or the (escaped) empty-case sentence. */
|
|
84
|
-
function renderRows(rows: MemoryRow[], empty: string): string {
|
|
85
|
-
if (rows.length === 0) return escapeHtml(empty);
|
|
86
|
-
return rows.map((row) => escapeHtml(formatMemory(row))).join("\n");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function renderKind(kind: string): string {
|
|
90
|
-
if (!isMemoryKind(kind))
|
|
91
|
-
return escapeHtml(
|
|
92
|
-
`No such kind "${kind}". Valid kinds: ${MEMORY_KINDS.join(", ")}.`,
|
|
93
|
-
);
|
|
94
|
-
return renderRows(
|
|
95
|
-
listMemories({ kind, limit: LIST_LIMIT }),
|
|
96
|
-
`Nothing remembered under ${kind}.`,
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Provenance for one row: the row itself, the numbers that decide where
|
|
102
|
-
* it ranks, and its audit trail. Reads by id rather than by the live
|
|
103
|
-
* listing, so a superseded or dropped row still explains itself.
|
|
104
|
-
*/
|
|
105
|
-
function renderWhy(raw: string): string {
|
|
106
|
-
const id = Number(raw);
|
|
107
|
-
if (!raw || !Number.isInteger(id))
|
|
108
|
-
return escapeHtml(`No memory with id ${raw || "(none given)"}.`);
|
|
109
|
-
const row = getMemory(id);
|
|
110
|
-
if (!row) return escapeHtml(`No memory with id ${id}.`);
|
|
111
|
-
const lines = [
|
|
112
|
-
escapeHtml(formatMemory(row)),
|
|
113
|
-
"",
|
|
114
|
-
escapeHtml(
|
|
115
|
-
`trust ${row.trust} · confidence ${row.confidence} · hits ${row.hitCount} · salience ${row.salience}`,
|
|
116
|
-
),
|
|
117
|
-
escapeHtml(
|
|
118
|
-
`created ${isoTime(row.createdAt)} · last seen ${isoTime(row.lastSeenAt)}`,
|
|
119
|
-
),
|
|
120
|
-
];
|
|
121
|
-
const history = memoryHistory(row.id);
|
|
122
|
-
if (history.length > 0) {
|
|
123
|
-
lines.push("", "<b>History</b>");
|
|
124
|
-
for (const entry of history) {
|
|
125
|
-
const reason = entry.reason ? ` — ${entry.reason}` : "";
|
|
126
|
-
lines.push(escapeHtml(`${isoTime(entry.at)} ${entry.op}${reason}`));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return lines.join("\n");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function isoTime(ms: number): string {
|
|
133
|
-
return new Date(ms).toISOString();
|
|
134
|
-
}
|
|
@@ -30,7 +30,6 @@ import {
|
|
|
30
30
|
renderEffortRows,
|
|
31
31
|
renderModelMenuText,
|
|
32
32
|
renderModelMenuKeyboard,
|
|
33
|
-
type SettingsButton,
|
|
34
33
|
} from "../render/menu.js";
|
|
35
34
|
import {
|
|
36
35
|
buildModelMenuViewForChat,
|
|
@@ -175,6 +174,17 @@ function registerEffortCommand(bot: Bot, { config, gateway }: RegisterDeps) {
|
|
|
175
174
|
config,
|
|
176
175
|
});
|
|
177
176
|
|
|
177
|
+
// Reset first: adaptive needs no model levels, so it must stay
|
|
178
|
+
// reachable on a model that registers none.
|
|
179
|
+
if (arg === "reset" || arg === "default" || arg === "adaptive") {
|
|
180
|
+
setChatEffort(cid, undefined);
|
|
181
|
+
await ctx.reply(
|
|
182
|
+
"Effort reset to <b>adaptive</b> (model decides when to think)",
|
|
183
|
+
{ parse_mode: "HTML" },
|
|
184
|
+
);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
178
188
|
if (reasoning.levels.length === 0) {
|
|
179
189
|
const modelText = reasoning.activeModel
|
|
180
190
|
? `<code>${escapeHtml(formatModelLabel(reasoning.activeModel))}</code>`
|
|
@@ -201,15 +211,6 @@ function registerEffortCommand(bot: Bot, { config, gateway }: RegisterDeps) {
|
|
|
201
211
|
return;
|
|
202
212
|
}
|
|
203
213
|
|
|
204
|
-
if (arg === "reset" || arg === "default" || arg === "adaptive") {
|
|
205
|
-
setChatEffort(cid, undefined);
|
|
206
|
-
await ctx.reply(
|
|
207
|
-
"Effort reset to <b>adaptive</b> (model decides when to think)",
|
|
208
|
-
{ parse_mode: "HTML" },
|
|
209
|
-
);
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
214
|
if (supportsReasoningLevel(arg, reasoning.levels)) {
|
|
214
215
|
setChatEffort(cid, arg as EffortLevel);
|
|
215
216
|
await ctx.reply(`Effort set to <b>${arg}</b>`, { parse_mode: "HTML" });
|
|
@@ -311,36 +312,6 @@ function registerSettingsCommand(bot: Bot, { config, gateway }: RegisterDeps) {
|
|
|
311
312
|
reasoning.levels,
|
|
312
313
|
);
|
|
313
314
|
const pulseOn = isPulseEnabled(cid);
|
|
314
|
-
let modelButtons: Array<SettingsButton> | undefined;
|
|
315
|
-
let pager:
|
|
316
|
-
| {
|
|
317
|
-
page: number;
|
|
318
|
-
totalPages: number;
|
|
319
|
-
filter: "all" | "free";
|
|
320
|
-
freeCount: number;
|
|
321
|
-
totalCount: number;
|
|
322
|
-
provider?: string;
|
|
323
|
-
}
|
|
324
|
-
| undefined;
|
|
325
|
-
let view: "models" | "groups" = "models";
|
|
326
|
-
let activeProvider: string | undefined;
|
|
327
|
-
if (settingsBe?.models?.getSettingsPresentation && resolvedSettingsModel) {
|
|
328
|
-
const pres = await settingsBe.models?.getSettingsPresentation(
|
|
329
|
-
resolvedSettingsModel,
|
|
330
|
-
);
|
|
331
|
-
modelButtons = pres.modelButtons;
|
|
332
|
-
pager = {
|
|
333
|
-
page: pres.page,
|
|
334
|
-
totalPages: pres.totalPages,
|
|
335
|
-
filter: pres.filter,
|
|
336
|
-
freeCount: pres.freeCount,
|
|
337
|
-
totalCount: pres.totalCount,
|
|
338
|
-
provider: pres.provider,
|
|
339
|
-
};
|
|
340
|
-
view = pres.view;
|
|
341
|
-
activeProvider = pres.provider;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
315
|
await ctx.reply(
|
|
345
316
|
renderSettingsText(
|
|
346
317
|
activeModel,
|
|
@@ -352,14 +323,11 @@ function registerSettingsCommand(bot: Bot, { config, gateway }: RegisterDeps) {
|
|
|
352
323
|
parse_mode: "HTML",
|
|
353
324
|
reply_markup: {
|
|
354
325
|
inline_keyboard: renderSettingsKeyboard(
|
|
355
|
-
|
|
326
|
+
resolvedSettingsModel,
|
|
356
327
|
effortName,
|
|
357
328
|
pulseOn,
|
|
358
|
-
modelButtons,
|
|
359
|
-
pager,
|
|
360
|
-
view,
|
|
361
|
-
activeProvider,
|
|
362
329
|
reasoning.levels,
|
|
330
|
+
Boolean(settingsBe?.models?.getSettingsPresentation),
|
|
363
331
|
),
|
|
364
332
|
},
|
|
365
333
|
},
|
|
@@ -33,23 +33,27 @@ export function whatsAppPanel(): WhatsAppPanel {
|
|
|
33
33
|
if (!provider) {
|
|
34
34
|
return { text: WHATSAPP_NOT_ENABLED, keyboard: [] };
|
|
35
35
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
const refresh = { text: "🔄 Refresh", callback_data: "whatsapp:refresh" };
|
|
37
|
+
// No pair button while linked: pairing refuses on a linked account, and
|
|
38
|
+
// unlinking from here would drop a working link. When the phone unlinks
|
|
39
|
+
// this device the connection loop parks, isLinked() turns false, and
|
|
40
|
+
// the button comes back on the next refresh.
|
|
41
|
+
if (provider.isLinked()) {
|
|
42
|
+
return {
|
|
43
|
+
text:
|
|
44
|
+
"<b>WhatsApp</b>\n✅ Linked and connected.\n\n" +
|
|
45
|
+
"To link a different phone, unlink this device on the phone " +
|
|
46
|
+
"first, then pair here.",
|
|
47
|
+
keyboard: [[refresh]],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
43
50
|
return {
|
|
44
|
-
text
|
|
51
|
+
text:
|
|
52
|
+
"<b>WhatsApp</b>\n📴 Not linked.\n\n" +
|
|
53
|
+
"Tap <b>Pair device</b> when you're holding the phone — I'll reply " +
|
|
54
|
+
"with a QR to scan (WhatsApp → ⋮ → Linked devices → Link a device).",
|
|
45
55
|
keyboard: [
|
|
46
|
-
[
|
|
47
|
-
{
|
|
48
|
-
text: linked ? "🔗 Re-pair" : "🔗 Pair device",
|
|
49
|
-
callback_data: "whatsapp:pair",
|
|
50
|
-
},
|
|
51
|
-
{ text: "🔄 Refresh", callback_data: "whatsapp:refresh" },
|
|
52
|
-
],
|
|
56
|
+
[{ text: "🔗 Pair device", callback_data: "whatsapp:pair" }, refresh],
|
|
53
57
|
],
|
|
54
58
|
};
|
|
55
59
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram's spelling of the /backup panel: the shared views from
|
|
3
|
+
* `frontend/presentation/backup-panel.ts`, rendered in HTML and mapped
|
|
4
|
+
* onto inline keyboards. The command (`commands/backup.ts`) sends these;
|
|
5
|
+
* the `backup:*` callbacks (`callbacks/backup.ts`) edit them in place.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
collectBackupStatus,
|
|
10
|
+
listSnapshots,
|
|
11
|
+
} from "../../../core/backup/index.js";
|
|
12
|
+
import {
|
|
13
|
+
GUIDE_BUTTONS,
|
|
14
|
+
backupPanelView,
|
|
15
|
+
renderRestoreGuide,
|
|
16
|
+
snapshotPageView,
|
|
17
|
+
type PanelButton,
|
|
18
|
+
type PanelView,
|
|
19
|
+
} from "../../presentation/backup-panel.js";
|
|
20
|
+
import { TELEGRAM_REPORTS } from "./html.js";
|
|
21
|
+
import type { SettingsButton } from "./menu.js";
|
|
22
|
+
|
|
23
|
+
export function inlineKeyboard(
|
|
24
|
+
rows: readonly PanelButton[][],
|
|
25
|
+
): SettingsButton[][] {
|
|
26
|
+
return rows.map((row) =>
|
|
27
|
+
row.map((button) => ({ text: button.label, callback_data: button.data })),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The status panel, freshly collected. `notice` leads it (a result line). */
|
|
32
|
+
export async function loadStatusPanel(notice?: string): Promise<PanelView> {
|
|
33
|
+
return backupPanelView(
|
|
34
|
+
TELEGRAM_REPORTS,
|
|
35
|
+
await collectBackupStatus(),
|
|
36
|
+
Date.now(),
|
|
37
|
+
notice,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function loadSnapshotPage(page: number): Promise<PanelView> {
|
|
42
|
+
return snapshotPageView(TELEGRAM_REPORTS, await listSnapshots(), page);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function restoreGuideView(): PanelView {
|
|
46
|
+
return { text: renderRestoreGuide(TELEGRAM_REPORTS), buttons: GUIDE_BUTTONS };
|
|
47
|
+
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
type ModelInfo,
|
|
19
19
|
} from "../../../core/models/catalog.js";
|
|
20
20
|
import { renderSettingsText as renderSettingsTextWith } from "../../presentation/reports.js";
|
|
21
|
+
import { formatModelLabel } from "../../presentation/format.js";
|
|
21
22
|
import { TELEGRAM_REPORTS } from "./html.js";
|
|
22
23
|
|
|
23
24
|
/** Display name for a known ModelInfo. */
|
|
@@ -440,49 +441,39 @@ export function renderModelBrowseKeyboard(
|
|
|
440
441
|
];
|
|
441
442
|
}
|
|
442
443
|
|
|
444
|
+
/**
|
|
445
|
+
* The /settings keyboard: one button that opens the /model menu (the single
|
|
446
|
+
* place models are browsed and picked), the effort levels the active model
|
|
447
|
+
* registers, and the pulse toggle. `hasModelMenu` is false when the backend
|
|
448
|
+
* has no model catalog — the /model menu cannot render there, so the button
|
|
449
|
+
* would do nothing.
|
|
450
|
+
*/
|
|
443
451
|
export function renderSettingsKeyboard(
|
|
444
|
-
model: string,
|
|
452
|
+
model: string | null,
|
|
445
453
|
effort: string,
|
|
446
454
|
proactive: boolean,
|
|
447
|
-
modelButtons?: Array<SettingsButton>,
|
|
448
|
-
pager?: SettingsPager,
|
|
449
|
-
view: "models" | "groups" = "models",
|
|
450
|
-
activeProvider?: string,
|
|
451
455
|
reasoningLevels: readonly ReasoningEffortLevel[] = [],
|
|
456
|
+
hasModelMenu = true,
|
|
452
457
|
): Array<Array<SettingsButton>> {
|
|
453
|
-
const selectedButtons = modelButtons?.length
|
|
454
|
-
? modelButtons
|
|
455
|
-
: getTelegramModelOptions().map((m) => ({
|
|
456
|
-
text: isSelectedModel(model, m.id)
|
|
457
|
-
? `✓ ${formatCompactModelLabel(m)}`
|
|
458
|
-
: formatCompactModelLabel(m),
|
|
459
|
-
callback_data: `settings:model:${m.id}`,
|
|
460
|
-
}));
|
|
461
|
-
const cols = modelButtons?.length ? 2 : 3;
|
|
462
|
-
const modelRows: Array<Array<SettingsButton>> = [];
|
|
463
|
-
for (let i = 0; i < selectedButtons.length; i += cols) {
|
|
464
|
-
modelRows.push(selectedButtons.slice(i, i + cols));
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const controlRows = pager
|
|
468
|
-
? renderModelPickerControlRows(
|
|
469
|
-
pager,
|
|
470
|
-
"settings:models",
|
|
471
|
-
"settings:noop",
|
|
472
|
-
view,
|
|
473
|
-
activeProvider,
|
|
474
|
-
)
|
|
475
|
-
: [];
|
|
476
|
-
|
|
477
458
|
const effortRows = renderEffortRows(
|
|
478
459
|
effort,
|
|
479
460
|
reasoningLevels,
|
|
480
461
|
"settings:effort:",
|
|
481
462
|
);
|
|
482
463
|
|
|
464
|
+
const modelRows: Array<Array<SettingsButton>> = hasModelMenu
|
|
465
|
+
? [
|
|
466
|
+
[
|
|
467
|
+
{
|
|
468
|
+
text: model ? `Model: ${formatModelLabel(model)}` : "Choose model",
|
|
469
|
+
callback_data: "model:menu",
|
|
470
|
+
},
|
|
471
|
+
],
|
|
472
|
+
]
|
|
473
|
+
: [];
|
|
474
|
+
|
|
483
475
|
return [
|
|
484
476
|
...modelRows,
|
|
485
|
-
...controlRows,
|
|
486
477
|
...effortRows,
|
|
487
478
|
[
|
|
488
479
|
{
|