talon-agent 5.25.1 → 5.26.1
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/claude-sdk/model-provider.ts +59 -12
- 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/frontend-runtime/admin-notify.ts +46 -6
- package/src/core/frontend-runtime/alerts.ts +10 -6
- package/src/core/mesh/credentials/store.ts +60 -9
- package/src/core/models/active-model.ts +57 -48
- package/src/core/plugin/loader.ts +47 -12
- 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/actions/media.ts +60 -19
- package/src/frontend/telegram/actions/messaging.ts +9 -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
|
@@ -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
|
{
|
|
@@ -5,8 +5,11 @@ import { resolveModelId as resolveModelName } from "../../../core/models/catalog
|
|
|
5
5
|
import {
|
|
6
6
|
getChatSettings,
|
|
7
7
|
setChatModel,
|
|
8
|
-
setChatEffort,
|
|
9
8
|
} from "../../../storage/chat-settings.js";
|
|
9
|
+
import {
|
|
10
|
+
describeChatEffort,
|
|
11
|
+
setChatEffortLevel,
|
|
12
|
+
} from "../../presentation/model-commands.js";
|
|
10
13
|
import type { Command, CommandContext } from "../command-registry.js";
|
|
11
14
|
|
|
12
15
|
type BackendRef = CommandContext["backend"];
|
|
@@ -178,20 +181,26 @@ export const modelCommand: Command = {
|
|
|
178
181
|
export const effortCommand: Command = {
|
|
179
182
|
name: "effort",
|
|
180
183
|
argHint: "[lvl]",
|
|
181
|
-
description: "Thinking effort
|
|
184
|
+
description: "Thinking effort for the active model",
|
|
182
185
|
async handler(args, ctx) {
|
|
183
|
-
|
|
186
|
+
// Same resolution and validation as every chat frontend: the levels
|
|
187
|
+
// the active model registers, plus adaptive/reset/default.
|
|
188
|
+
const deps = {
|
|
189
|
+
config: ctx.config,
|
|
190
|
+
gateway: { backend: ctx.backend ?? null },
|
|
191
|
+
};
|
|
192
|
+
const arg = args.trim();
|
|
193
|
+
if (!arg) {
|
|
194
|
+
const { current, levels } = await describeChatEffort(ctx.chatId(), deps);
|
|
184
195
|
ctx.renderer.writeSystem(
|
|
185
|
-
|
|
196
|
+
levels.length
|
|
197
|
+
? `Effort: ${current} (${levels.join(", ")}, adaptive)`
|
|
198
|
+
: `Effort: ${current}`,
|
|
186
199
|
);
|
|
187
200
|
} else {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
? undefined
|
|
192
|
-
: (args as "off" | "low" | "medium" | "high" | "max"),
|
|
193
|
-
);
|
|
194
|
-
ctx.renderer.writeSystem(`Effort → ${args}`);
|
|
201
|
+
const outcome = await setChatEffortLevel(ctx.chatId(), arg, deps);
|
|
202
|
+
if (outcome.ok) ctx.renderer.writeSystem(outcome.text);
|
|
203
|
+
else ctx.renderer.writeError(outcome.text);
|
|
195
204
|
}
|
|
196
205
|
ctx.reprompt();
|
|
197
206
|
},
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* WhatsApp has no inline buttons, so everything Telegram does with a
|
|
6
6
|
* picker is done here with typed arguments: `/model` prints a numbered
|
|
7
7
|
* catalog, `/model 3` (or `/model <id>`, `/model <backend>`) picks from
|
|
8
|
-
* it. The
|
|
9
|
-
* this module only parses,
|
|
8
|
+
* it. The replies are shared with the native bridge
|
|
9
|
+
* (frontend/presentation/text-commands.ts); this module only parses,
|
|
10
|
+
* authorises, and delivers.
|
|
10
11
|
*
|
|
11
12
|
* Replies are written in Markdown — `sendText` translates them into
|
|
12
13
|
* WhatsApp's dialect and splits long listings into bubbles.
|
|
@@ -14,38 +15,17 @@
|
|
|
14
15
|
|
|
15
16
|
import { log } from "../../util/log.js";
|
|
16
17
|
import { recordMessageProcessed } from "../../util/watchdog.js";
|
|
17
|
-
import {
|
|
18
|
-
import { isPulseEnabled } from "../../core/background/pulse/pulse.js";
|
|
18
|
+
import { performSessionReset } from "../presentation/session-status.js";
|
|
19
19
|
import {
|
|
20
|
-
DEFAULT_PULSE_INTERVAL_MS,
|
|
21
|
-
formatBytes,
|
|
22
|
-
formatDuration,
|
|
23
|
-
formatModelLabel,
|
|
24
|
-
formatTokenCount,
|
|
25
|
-
formatUsd,
|
|
26
|
-
} from "../presentation/format.js";
|
|
27
|
-
import {
|
|
28
|
-
collectSessionStatus,
|
|
29
|
-
performSessionReset,
|
|
30
|
-
type SessionStatusData,
|
|
31
|
-
} from "../presentation/session-status.js";
|
|
32
|
-
import {
|
|
33
|
-
formatCacheTempLine,
|
|
34
|
-
formatDaemonLine,
|
|
35
|
-
} from "../presentation/status-context.js";
|
|
36
|
-
import {
|
|
37
|
-
describeChatEffort,
|
|
38
|
-
describeChatModels,
|
|
39
|
-
matchBackendArg,
|
|
40
|
-
resetChatBackend,
|
|
41
|
-
resetChatModel,
|
|
42
20
|
resolveChatBackendPair,
|
|
43
|
-
selectChatModel,
|
|
44
|
-
setChatEffortLevel,
|
|
45
|
-
switchChatBackend,
|
|
46
21
|
type ModelCommandDeps,
|
|
47
|
-
type ModelOverview,
|
|
48
22
|
} from "../presentation/model-commands.js";
|
|
23
|
+
import {
|
|
24
|
+
effortCommandReply,
|
|
25
|
+
modelCommandReply,
|
|
26
|
+
settingsCommandReply,
|
|
27
|
+
statusCommandReply,
|
|
28
|
+
} from "../presentation/text-commands.js";
|
|
49
29
|
import { sendText } from "./actions/send.js";
|
|
50
30
|
import { identityAllowed, type Identity } from "./connection/identity.js";
|
|
51
31
|
import type { WhatsAppChatInfo } from "./registry.js";
|
|
@@ -73,9 +53,6 @@ export type CommandInbound = {
|
|
|
73
53
|
isGroup: boolean;
|
|
74
54
|
};
|
|
75
55
|
|
|
76
|
-
/** How many catalog entries `/model` prints before pointing at `/model <id>`. */
|
|
77
|
-
const MAX_LISTED_MODELS = 40;
|
|
78
|
-
|
|
79
56
|
/**
|
|
80
57
|
* Parse a slash command out of message text. Group messages in mention
|
|
81
58
|
* mode arrive as "@<number> /model", so leading @mentions are skipped.
|
|
@@ -112,120 +89,6 @@ export function canChangeSettings(
|
|
|
112
89
|
return identityAllowed(inbound.identity, runtime.allowedDms);
|
|
113
90
|
}
|
|
114
91
|
|
|
115
|
-
// ── Renderers ───────────────────────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
function renderModelOverview(view: ModelOverview): string {
|
|
118
|
-
const lines: string[] = [];
|
|
119
|
-
lines.push(
|
|
120
|
-
view.activeModel
|
|
121
|
-
? `**Model:** \`${view.activeModel}\`${
|
|
122
|
-
view.activeDisplay && view.activeDisplay !== view.activeModel
|
|
123
|
-
? ` (${view.activeDisplay})`
|
|
124
|
-
: ""
|
|
125
|
-
}${view.hasModelOverride ? " — chat override" : ""}`
|
|
126
|
-
: "**Model:** none selected — messages will be refused until you pick one",
|
|
127
|
-
);
|
|
128
|
-
lines.push(
|
|
129
|
-
`**Backend:** ${view.backendLabel} (\`${view.backendId}\`)${view.hasBackendOverride ? " — chat override" : ""}`,
|
|
130
|
-
);
|
|
131
|
-
if (view.backends.length > 1) {
|
|
132
|
-
lines.push(
|
|
133
|
-
`Backends: ${view.backends.map((b) => `\`${b.id}\``).join(", ")} — \`/model <backend>\` switches (session restarts).`,
|
|
134
|
-
);
|
|
135
|
-
}
|
|
136
|
-
if (view.choices.length === 0) {
|
|
137
|
-
lines.push(
|
|
138
|
-
"",
|
|
139
|
-
"This backend has no browsable catalog; `/model <id>` sets one directly.",
|
|
140
|
-
);
|
|
141
|
-
} else {
|
|
142
|
-
lines.push("", `**Models** (${view.choices.length}):`);
|
|
143
|
-
for (const c of view.choices.slice(0, MAX_LISTED_MODELS)) {
|
|
144
|
-
const marker = c.id === view.activeModel ? " ✓" : "";
|
|
145
|
-
const free = c.free ? " · free" : "";
|
|
146
|
-
lines.push(`${c.index}. ${c.displayName} — \`${c.id}\`${free}${marker}`);
|
|
147
|
-
}
|
|
148
|
-
if (view.choices.length > MAX_LISTED_MODELS) {
|
|
149
|
-
lines.push(
|
|
150
|
-
`… ${view.choices.length - MAX_LISTED_MODELS} more — \`/model <id>\` picks any of them.`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
lines.push(
|
|
155
|
-
"",
|
|
156
|
-
"`/model <number>` or `/model <id>` picks; `/model default` clears the chat's pick" +
|
|
157
|
-
(view.hasBackendOverride
|
|
158
|
-
? "; `/model backend default` reverts the backend."
|
|
159
|
-
: "."),
|
|
160
|
-
);
|
|
161
|
-
return lines.join("\n");
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function renderSettings(
|
|
165
|
-
view: ModelOverview,
|
|
166
|
-
effort: { current: string; levels: string[] },
|
|
167
|
-
pulseOn: boolean,
|
|
168
|
-
pulseIntervalMs: number | undefined,
|
|
169
|
-
freeOnly: boolean,
|
|
170
|
-
): string {
|
|
171
|
-
const lines = [
|
|
172
|
-
"**Settings**",
|
|
173
|
-
`Model: \`${view.activeModel ?? "none selected"}\`${view.hasModelOverride ? " (chat override)" : ""}`,
|
|
174
|
-
`Backend: ${view.backendLabel} (\`${view.backendId}\`)${view.hasBackendOverride ? " (chat override)" : ""}`,
|
|
175
|
-
`Effort: ${effort.current}${effort.levels.length ? ` — levels: ${effort.levels.join(", ")}` : ""}`,
|
|
176
|
-
`Pulse: ${pulseOn ? "on" : "off"} (every ${formatDuration(pulseIntervalMs ?? DEFAULT_PULSE_INTERVAL_MS)})`,
|
|
177
|
-
];
|
|
178
|
-
if (freeOnly) lines.push("Free-only models: on");
|
|
179
|
-
lines.push(
|
|
180
|
-
"",
|
|
181
|
-
"Change with `/model`, `/effort`; `/reset` starts a fresh session.",
|
|
182
|
-
);
|
|
183
|
-
return lines.join("\n");
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
function renderStatus(s: SessionStatusData): string {
|
|
187
|
-
const used = s.context.known ? formatTokenCount(s.context.used) : "unknown";
|
|
188
|
-
const max = s.context.max > 0 ? formatTokenCount(s.context.max) : "unknown";
|
|
189
|
-
const pct = s.context.known ? `${s.context.pct}%` : "unknown";
|
|
190
|
-
const lines = [
|
|
191
|
-
`**Talon** · \`${formatModelLabel(s.activeModel)}\`${s.backendLabel ? ` · ${s.backendLabel}` : ""} · effort: ${s.effortName}${s.turnInProgress ? " · ⏳ turn running" : ""}`,
|
|
192
|
-
"",
|
|
193
|
-
`**Context** ${used} / ${max} (${pct})${s.context.warn ? " ⚠️ consider /reset" : ""}`,
|
|
194
|
-
`\`${s.context.bar}\``,
|
|
195
|
-
"",
|
|
196
|
-
"**Session stats**",
|
|
197
|
-
`Response: last ${s.lastResponseMs ? formatDuration(s.lastResponseMs) : "—"} · avg ${s.avgResponseMs ? formatDuration(s.avgResponseMs) : "—"} · best ${s.fastestMs ? formatDuration(s.fastestMs) : "—"}`,
|
|
198
|
-
`Turns: ${s.turns}${s.turnsModelLabel ? ` (${formatModelLabel(s.turnsModelLabel)})` : ""}`,
|
|
199
|
-
`Tokens: in ${formatTokenCount(s.inputTokens)} · out ${formatTokenCount(s.outputTokens)}${s.costUsd > 0 ? ` · cost ${formatUsd(s.costUsd)}` : ""}`,
|
|
200
|
-
];
|
|
201
|
-
if (s.cache) {
|
|
202
|
-
lines.push(
|
|
203
|
-
`Cache: ${s.cache.hitPct}% hit · read ${formatTokenCount(s.cache.read)}${s.cache.showsWrite ? ` · write ${formatTokenCount(s.cache.write)}` : ""}`,
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
if (s.cacheTemp) lines.push(formatCacheTempLine(s.cacheTemp));
|
|
207
|
-
if (s.plan) {
|
|
208
|
-
lines.push(
|
|
209
|
-
"",
|
|
210
|
-
`**Plan**${s.plan.plan ? ` ${s.plan.plan}` : ""}${s.plan.ageLabel ? ` (${s.plan.ageLabel})` : ""}`,
|
|
211
|
-
...s.plan.windows.map(
|
|
212
|
-
(w) =>
|
|
213
|
-
`\`${w.label.padEnd(6)}${w.bar} ${String(w.percent).padStart(3)}%\`${w.resetLabel ? ` reset ${w.resetLabel}` : ""}`,
|
|
214
|
-
),
|
|
215
|
-
);
|
|
216
|
-
}
|
|
217
|
-
lines.push(
|
|
218
|
-
"",
|
|
219
|
-
`**Pulse** ${s.pulseOn ? "on" : "off"}`,
|
|
220
|
-
`**Workspace** ${formatBytes(s.diskBytes)}`,
|
|
221
|
-
`**Session** ${s.sessionName ? `"${s.sessionName}" ` : ""}${s.sessionId ? `\`${s.sessionId.slice(0, 8)}…\`` : "(new)"} · ${s.sessionAge} old`,
|
|
222
|
-
`**Uptime** ${s.uptime} · ${s.activeSessionCount} active session${s.activeSessionCount === 1 ? "" : "s"}`,
|
|
223
|
-
`**Runtime** ${s.runtime} · ${formatBytes(s.rssBytes)} RSS`,
|
|
224
|
-
formatDaemonLine(s.daemon),
|
|
225
|
-
);
|
|
226
|
-
return lines.join("\n");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
92
|
const HELP_TEXT = [
|
|
230
93
|
"**Commands**",
|
|
231
94
|
"/model — list backends and models; `/model <n|id>` picks, `/model <backend>` switches",
|
|
@@ -238,71 +101,6 @@ const HELP_TEXT = [
|
|
|
238
101
|
|
|
239
102
|
// ── Handlers ────────────────────────────────────────────────────────────────
|
|
240
103
|
|
|
241
|
-
async function runModelCommand(
|
|
242
|
-
chatId: string,
|
|
243
|
-
arg: string,
|
|
244
|
-
deps: ModelCommandDeps,
|
|
245
|
-
): Promise<string> {
|
|
246
|
-
if (!arg) return renderModelOverview(await describeChatModels(chatId, deps));
|
|
247
|
-
const lower = arg.toLowerCase();
|
|
248
|
-
// `/reset` keeps history on WhatsApp because the local store is the only
|
|
249
|
-
// chat record; a backend switch keeps it for the same reason.
|
|
250
|
-
if (lower === "backend default" || lower === "backend reset") {
|
|
251
|
-
return (await resetChatBackend(chatId, deps, { keepHistory: true })).text;
|
|
252
|
-
}
|
|
253
|
-
if (lower === "reset" || lower === "default") {
|
|
254
|
-
return (await resetChatModel(chatId, deps)).text;
|
|
255
|
-
}
|
|
256
|
-
const backend = matchBackendArg(arg, deps.config);
|
|
257
|
-
if (backend) {
|
|
258
|
-
return (
|
|
259
|
-
await switchChatBackend(chatId, backend, deps, { keepHistory: true })
|
|
260
|
-
).text;
|
|
261
|
-
}
|
|
262
|
-
return (await selectChatModel(chatId, arg, deps)).text;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function runEffortCommand(
|
|
266
|
-
chatId: string,
|
|
267
|
-
arg: string,
|
|
268
|
-
deps: ModelCommandDeps,
|
|
269
|
-
): Promise<string> {
|
|
270
|
-
if (arg) return (await setChatEffortLevel(chatId, arg, deps)).text;
|
|
271
|
-
const effort = await describeChatEffort(chatId, deps);
|
|
272
|
-
if (effort.levels.length === 0) {
|
|
273
|
-
return `No reasoning levels available for ${effort.activeModel ?? "the active model"} on backend ${effort.backendId}.`;
|
|
274
|
-
}
|
|
275
|
-
return `**Effort:** ${effort.current}\nLevels: ${effort.levels.join(", ")}, or adaptive — \`/effort <level>\` sets one.`;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
async function runSettingsCommand(
|
|
279
|
-
chatId: string,
|
|
280
|
-
deps: ModelCommandDeps,
|
|
281
|
-
): Promise<string> {
|
|
282
|
-
const [view, effort] = await Promise.all([
|
|
283
|
-
describeChatModels(chatId, deps),
|
|
284
|
-
describeChatEffort(chatId, deps),
|
|
285
|
-
]);
|
|
286
|
-
const sets = getChatSettings(chatId);
|
|
287
|
-
return renderSettings(
|
|
288
|
-
view,
|
|
289
|
-
effort,
|
|
290
|
-
isPulseEnabled(chatId),
|
|
291
|
-
sets.pulseIntervalMs,
|
|
292
|
-
sets.freeOnly === true,
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function runStatusCommand(
|
|
297
|
-
chatId: string,
|
|
298
|
-
deps: ModelCommandDeps,
|
|
299
|
-
): Promise<string> {
|
|
300
|
-
const { backend, backendId } = resolveChatBackendPair(chatId, deps);
|
|
301
|
-
return renderStatus(
|
|
302
|
-
await collectSessionStatus(chatId, deps.config, backend, backendId),
|
|
303
|
-
);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
104
|
async function runResetCommand(
|
|
307
105
|
chatId: string,
|
|
308
106
|
senderName: string,
|
|
@@ -337,13 +135,15 @@ export async function executeWhatsAppCommand(
|
|
|
337
135
|
}
|
|
338
136
|
switch (cmd.name) {
|
|
339
137
|
case "model":
|
|
340
|
-
|
|
138
|
+
// `/reset` keeps history on WhatsApp because the local store is the
|
|
139
|
+
// only chat record; a backend switch keeps it for the same reason.
|
|
140
|
+
return modelCommandReply(chatId, cmd.arg, deps, { keepHistory: true });
|
|
341
141
|
case "effort":
|
|
342
|
-
return
|
|
142
|
+
return effortCommandReply(chatId, cmd.arg, deps);
|
|
343
143
|
case "settings":
|
|
344
|
-
return
|
|
144
|
+
return settingsCommandReply(chatId, deps);
|
|
345
145
|
case "status":
|
|
346
|
-
return
|
|
146
|
+
return statusCommandReply(chatId, deps);
|
|
347
147
|
case "reset":
|
|
348
148
|
return runResetCommand(chatId, inbound.senderName, deps);
|
|
349
149
|
case "help":
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One inbound message, from `messages.upsert` to the model's turn:
|
|
3
3
|
* access gates → media saved to the workspace → history recorded →
|
|
4
|
-
* slash commands (../commands.ts
|
|
4
|
+
* slash commands (../commands.ts; skipped in the catch-up backlog) →
|
|
5
|
+
* catch-up policy → `execute()`.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { isJidGroup, type WAMessage } from "baileys";
|
|
@@ -17,7 +18,7 @@ import {
|
|
|
17
18
|
recordMessageReceived,
|
|
18
19
|
} from "../../../util/watchdog.js";
|
|
19
20
|
import { isAddressedToSelf, isGroupAllowed } from "../access.js";
|
|
20
|
-
import { handleWhatsAppCommand } from "../commands.js";
|
|
21
|
+
import { handleWhatsAppCommand, parseWhatsAppCommand } from "../commands.js";
|
|
21
22
|
import { sendText } from "../actions/send.js";
|
|
22
23
|
import {
|
|
23
24
|
bareId,
|
|
@@ -207,6 +208,32 @@ function catchUpDeservesReply(inbound: RecordedMessage): boolean {
|
|
|
207
208
|
return true;
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
/**
|
|
212
|
+
* A slash command in the catch-up backlog was issued against an earlier
|
|
213
|
+
* state of the chat — replaying `/reset` or `/model x` after a restart
|
|
214
|
+
* would silently undo whatever happened since. It stays recorded but does
|
|
215
|
+
* not run; while fresh, the sender is told so they can resend it.
|
|
216
|
+
*/
|
|
217
|
+
async function skipCatchUpCommand(
|
|
218
|
+
runtime: WhatsAppRuntime,
|
|
219
|
+
inbound: RecordedMessage,
|
|
220
|
+
name: string,
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
const { chat, senderName } = inbound;
|
|
223
|
+
log(
|
|
224
|
+
"whatsapp",
|
|
225
|
+
`[${chat.chatId}] Skipped /${name} from ${senderName} (sent while offline)`,
|
|
226
|
+
);
|
|
227
|
+
recordMessageProcessed();
|
|
228
|
+
const sock = runtime.sock;
|
|
229
|
+
if (!sock || !shouldReplyToCatchUp(inbound.platformTs)) return;
|
|
230
|
+
await sendText(
|
|
231
|
+
{ sock, gateway: runtime.gateway },
|
|
232
|
+
chat,
|
|
233
|
+
`Not run: /${name} was sent while offline. Send it again to run it.`,
|
|
234
|
+
).catch(() => {});
|
|
235
|
+
}
|
|
236
|
+
|
|
210
237
|
async function onTurnEvent(
|
|
211
238
|
runtime: WhatsAppRuntime,
|
|
212
239
|
chat: WhatsAppChatInfo,
|
|
@@ -307,6 +334,10 @@ export async function handleInbound(
|
|
|
307
334
|
if (!admitted) return;
|
|
308
335
|
const inbound = await recordInbound(runtime, msg, admitted);
|
|
309
336
|
if (!inbound) return;
|
|
337
|
+
if (options.catchUp) {
|
|
338
|
+
const cmd = parseWhatsAppCommand(inbound.text);
|
|
339
|
+
if (cmd) return skipCatchUpCommand(runtime, inbound, cmd.name);
|
|
340
|
+
}
|
|
310
341
|
if (await handleWhatsAppCommand(runtime, inbound)) return;
|
|
311
342
|
if (options.catchUp && !catchUpDeservesReply(inbound)) return;
|
|
312
343
|
await runInboundTurn(runtime, inbound);
|