@nordbyte/nordrelay 0.5.2 → 0.7.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.
Files changed (71) hide show
  1. package/.env.example +80 -11
  2. package/README.md +154 -22
  3. package/dist/access-control.js +7 -1
  4. package/dist/activity-events.js +44 -0
  5. package/dist/audit-log.js +40 -2
  6. package/dist/bot-preferences.js +1 -0
  7. package/dist/bot-rendering.js +10 -7
  8. package/dist/bot.js +535 -11
  9. package/dist/channel-actions.js +7 -2
  10. package/dist/channel-adapter.js +40 -7
  11. package/dist/channel-command-catalog.js +88 -0
  12. package/dist/channel-command-service.js +369 -0
  13. package/dist/channel-mirror-registry.js +77 -0
  14. package/dist/channel-peer-prompt.js +95 -0
  15. package/dist/channel-runtime.js +12 -5
  16. package/dist/channel-turn-service.js +237 -0
  17. package/dist/codex-state.js +114 -78
  18. package/dist/config-metadata.js +93 -13
  19. package/dist/config.js +103 -8
  20. package/dist/context-key.js +87 -5
  21. package/dist/discord-artifacts.js +165 -0
  22. package/dist/discord-bot.js +2073 -0
  23. package/dist/discord-channel-runtime.js +133 -0
  24. package/dist/discord-command-surface.js +57 -0
  25. package/dist/discord-rate-limit.js +141 -0
  26. package/dist/index.js +36 -5
  27. package/dist/job-store.js +127 -0
  28. package/dist/metrics.js +87 -0
  29. package/dist/peer-auth.js +85 -0
  30. package/dist/peer-client.js +256 -0
  31. package/dist/peer-context.js +21 -0
  32. package/dist/peer-identity.js +127 -0
  33. package/dist/peer-runtime-service.js +636 -0
  34. package/dist/peer-server.js +220 -0
  35. package/dist/peer-store.js +294 -0
  36. package/dist/peer-types.js +52 -0
  37. package/dist/relay-external-activity-monitor.js +47 -6
  38. package/dist/relay-runtime-helpers.js +208 -0
  39. package/dist/relay-runtime.js +897 -394
  40. package/dist/remote-prompt.js +98 -0
  41. package/dist/runtime-cache.js +57 -0
  42. package/dist/session-locks.js +10 -7
  43. package/dist/support-bundle.js +1 -0
  44. package/dist/telegram-access-commands.js +15 -2
  45. package/dist/telegram-access-middleware.js +16 -3
  46. package/dist/telegram-agent-commands.js +25 -0
  47. package/dist/telegram-artifact-commands.js +46 -0
  48. package/dist/telegram-command-menu.js +3 -53
  49. package/dist/telegram-diagnostics-command.js +5 -50
  50. package/dist/telegram-general-commands.js +16 -6
  51. package/dist/telegram-operational-commands.js +14 -6
  52. package/dist/telegram-preference-commands.js +23 -127
  53. package/dist/telegram-queue-commands.js +74 -4
  54. package/dist/telegram-support-command.js +7 -0
  55. package/dist/telegram-update-commands.js +27 -0
  56. package/dist/user-management.js +208 -0
  57. package/dist/web-api-contract.js +17 -0
  58. package/dist/web-dashboard-access-routes.js +74 -1
  59. package/dist/web-dashboard-artifact-routes.js +3 -3
  60. package/dist/web-dashboard-assets.js +2 -0
  61. package/dist/web-dashboard-pages.js +109 -13
  62. package/dist/web-dashboard-peer-routes.js +204 -0
  63. package/dist/web-dashboard-runtime-routes.js +53 -8
  64. package/dist/web-dashboard-session-routes.js +27 -20
  65. package/dist/web-dashboard-ui.js +2 -0
  66. package/dist/web-dashboard.js +160 -6
  67. package/dist/web-state.js +33 -2
  68. package/dist/webui-assets/dashboard.css +75 -1
  69. package/dist/webui-assets/dashboard.js +779 -55
  70. package/package.json +5 -2
  71. package/plugins/nordrelay/scripts/nordrelay.mjs +578 -19
package/dist/bot.js CHANGED
@@ -11,6 +11,8 @@ import { AuditLogStore } from "./audit-log.js";
11
11
  import { formatSessionLabel } from "./bot-ui.js";
12
12
  import { BotPreferencesStore, isQuietNow, } from "./bot-preferences.js";
13
13
  import { renderAgentUpdateJobAction } from "./channel-actions.js";
14
+ import { ChannelCommandService } from "./channel-command-service.js";
15
+ import { runChannelPeerPrompt } from "./channel-peer-prompt.js";
14
16
  import { deliverChannelAction } from "./channel-runtime.js";
15
17
  import { agentLabel, agentReasoningLabel, agentReasoningOptions, } from "./agent.js";
16
18
  import { getExternalActivityForSession, getExternalSnapshotForSession, } from "./agent-activity.js";
@@ -23,6 +25,7 @@ import { escapeHTML } from "./format.js";
23
25
  import { PromptStore, toPromptEnvelope } from "./prompt-store.js";
24
26
  import { checkHermesAuthStatus, startHermesLogin, startHermesLogout } from "./hermes-auth.js";
25
27
  import { checkOpenClawAuthStatus } from "./openclaw-auth.js";
28
+ import { RemoteRelayClient } from "./peer-client.js";
26
29
  import { checkPiAuthStatus } from "./pi-auth.js";
27
30
  import { configureRedaction, redactText } from "./redaction.js";
28
31
  import { canWriteWithLock, SessionLockStore } from "./session-locks.js";
@@ -45,6 +48,7 @@ import { registerTelegramSupportCommands } from "./telegram-support-command.js";
45
48
  import { registerTelegramUpdateCommands } from "./telegram-update-commands.js";
46
49
  import { appendWithCap, authHelpText, buildStreamingPreview, capabilitiesOf, filterSessions, formatAgentLaunchProfileLabel, formatAgentSettingScope, formatDurationSeconds, formatError, formatLocalDateTime, formatLockOwner, formatModelButtonLabel, formatRelativeTime, formatTelegramName, formatToolSummaryLine, formatTurnUsageLine, getWorkspaceShortName, idOf, isEmptyArtifactReport, isPromptEnvelopeLike, isQueuedPromptLike, labelOf, orderPinnedSessions, parseFastModeArgument, renderExternalMirrorEvent, renderExternalMirrorStatus, renderPromptFailure, renderTodoList, renderToolEndMessage, renderToolStartMessage, requiresTurnApproval, trimLine, } from "./bot-rendering.js";
47
50
  import { UserStore } from "./user-management.js";
51
+ import { WebActivityStore } from "./web-state.js";
48
52
  import { evaluateWorkspacePolicy, filterAllowedWorkspaces, renderWorkspacePolicyLine, } from "./workspace-policy.js";
49
53
  export { formatToolSummaryLine, formatTurnUsageLine, summarizeToolName } from "./bot-rendering.js";
50
54
  export { registerCommands } from "./telegram-command-menu.js";
@@ -54,6 +58,10 @@ const TOOL_OUTPUT_PREVIEW_LIMIT = 500;
54
58
  const MAX_AUDIO_FILE_SIZE = 25 * 1024 * 1024;
55
59
  const MEDIA_GROUP_FLUSH_MS = 1200;
56
60
  const LAUNCH_PROFILES_COMMAND = "/launch_profiles";
61
+ const CLI_ACTIVITY_ACTOR = {
62
+ channel: "cli",
63
+ label: "CLI",
64
+ };
57
65
  export function createBot(config, registry) {
58
66
  configureRedaction(config.telegramRedactPatterns);
59
67
  telegramRateLimiter.configure({
@@ -80,11 +88,17 @@ export function createBot(config, registry) {
80
88
  const turnProgress = new Map();
81
89
  const promptStore = new PromptStore(config.workspace, config.stateBackend);
82
90
  const preferencesStore = new BotPreferencesStore(config.workspace, config.stateBackend);
91
+ const activityStore = new WebActivityStore(config.workspace, config.stateBackend, config.auditMaxEvents);
83
92
  const auditLog = new AuditLogStore(config.workspace, config.stateBackend, config.auditMaxEvents);
84
93
  const lockStore = new SessionLockStore(config.workspace, config.stateBackend);
85
94
  const userStore = new UserStore();
86
95
  const contextUsers = new WeakMap();
87
- const agentUpdates = new AgentUpdateManager();
96
+ const agentUpdateActors = new Map();
97
+ const agentUpdateStates = new Map();
98
+ const commandService = new ChannelCommandService(config);
99
+ const agentUpdates = new AgentUpdateManager({
100
+ onUpdate: (job) => recordTelegramAgentUpdateLifecycle(job),
101
+ });
88
102
  const linkAttempts = new Map();
89
103
  const drainingQueues = new Set();
90
104
  const externalQueueTimers = new Map();
@@ -228,6 +242,19 @@ export function createBot(config, registry) {
228
242
  const startTelegramAgentUpdate = async (ctx, agentId, operation = "update") => {
229
243
  try {
230
244
  const job = agentUpdates.start(agentId, agentUpdateContext(), operation);
245
+ const actor = telegramActivityActor(ctx);
246
+ agentUpdateActors.set(job.id, actor);
247
+ agentUpdateStates.set(job.id, { status: job.status, needsInput: job.needsInput });
248
+ appendActivity({
249
+ source: "telegram",
250
+ status: "info",
251
+ type: operation === "install" ? "agent_install_started" : "agent_update_started",
252
+ threadId: null,
253
+ workspace: config.workspace,
254
+ agentId,
255
+ actor,
256
+ detail: `${job.method}: ${job.summary}`,
257
+ });
231
258
  const contextKey = contextKeyFromCtx(ctx);
232
259
  if (contextKey) {
233
260
  audit({
@@ -235,6 +262,9 @@ export function createBot(config, registry) {
235
262
  status: "ok",
236
263
  contextKey,
237
264
  agentId,
265
+ actor,
266
+ actorId: getAuthenticatedUser(ctx)?.user.id ?? ctx.from?.id,
267
+ actorRole: getUserRole(ctx),
238
268
  description: `${operation} ${agentId}`,
239
269
  detail: job.summary,
240
270
  });
@@ -495,6 +525,26 @@ export function createBot(config, registry) {
495
525
  if (snapshot.activity.active) {
496
526
  state.turnId = snapshot.activity.turnId;
497
527
  state.startedAt = snapshot.activity.startedAt;
528
+ const turnKey = snapshot.activity.turnId ?? snapshot.activity.startedAt?.toISOString() ?? "unknown";
529
+ if (state.activityStartedTurnKey !== turnKey) {
530
+ const info = session.getInfo();
531
+ appendActivity({
532
+ source: "cli",
533
+ status: "running",
534
+ type: "cli_turn_started",
535
+ contextKey,
536
+ threadId: snapshot.threadId,
537
+ workspace: info.workspace,
538
+ agentId: info.agentId,
539
+ actor: CLI_ACTIVITY_ACTOR,
540
+ prompt: snapshot.latestUserMessage ?? `${snapshot.agentLabel} CLI task`,
541
+ detail: `${snapshot.sourceLabel}: ${snapshot.sourcePath}`,
542
+ });
543
+ state.activityStartedTurnKey = turnKey;
544
+ state.activityFinishedTurnKey = undefined;
545
+ state.activityToolStartLines = [];
546
+ state.activityToolEndLines = [];
547
+ }
498
548
  if (mirrorMode !== "off") {
499
549
  await sendExternalMirrorTyping(chatId, parsed.messageThreadId, state);
500
550
  }
@@ -542,6 +592,43 @@ export function createBot(config, registry) {
542
592
  state.latestMirroredEventLine = event.lineNumber;
543
593
  }
544
594
  }
595
+ const info = session.getInfo();
596
+ const loggedStartLines = new Set(state.activityToolStartLines ?? []);
597
+ const loggedEndLines = new Set(state.activityToolEndLines ?? []);
598
+ for (const event of snapshot.events.filter((event) => event.lineNumber > state.lastLine && event.kind === "tool")) {
599
+ if (event.status === "started" && !loggedStartLines.has(event.lineNumber)) {
600
+ appendActivity({
601
+ source: "cli",
602
+ status: "running",
603
+ type: "cli_tool_started",
604
+ contextKey,
605
+ threadId: snapshot.threadId,
606
+ workspace: info.workspace,
607
+ agentId: info.agentId,
608
+ actor: CLI_ACTIVITY_ACTOR,
609
+ prompt: snapshot.latestUserMessage ?? undefined,
610
+ detail: event.toolName ?? "tool",
611
+ });
612
+ loggedStartLines.add(event.lineNumber);
613
+ }
614
+ if ((event.status === "finished" || event.status === "failed") && !loggedEndLines.has(event.lineNumber)) {
615
+ appendActivity({
616
+ source: "cli",
617
+ status: event.status === "failed" ? "failed" : "completed",
618
+ type: event.status === "failed" ? "cli_tool_failed" : "cli_tool_completed",
619
+ contextKey,
620
+ threadId: snapshot.threadId,
621
+ workspace: info.workspace,
622
+ agentId: info.agentId,
623
+ actor: CLI_ACTIVITY_ACTOR,
624
+ prompt: snapshot.latestUserMessage ?? undefined,
625
+ detail: event.toolName ?? "tool",
626
+ });
627
+ loggedEndLines.add(event.lineNumber);
628
+ }
629
+ }
630
+ state.activityToolStartLines = [...loggedStartLines].slice(-200);
631
+ state.activityToolEndLines = [...loggedEndLines].slice(-200);
545
632
  state.lastLine = Math.max(state.lastLine, snapshot.lineCount);
546
633
  return;
547
634
  }
@@ -551,6 +638,25 @@ export function createBot(config, registry) {
551
638
  }
552
639
  const terminalEvent = [...snapshot.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
553
640
  if (terminalEvent) {
641
+ const turnKey = terminalEvent.turnId ?? snapshot.activity.turnId ?? state.startedAt?.toString() ?? "unknown";
642
+ if (state.activityFinishedTurnKey !== turnKey) {
643
+ const info = session.getInfo();
644
+ const startedAt = state.startedAt instanceof Date ? state.startedAt : state.startedAt ? new Date(state.startedAt) : snapshot.activity.startedAt;
645
+ appendActivity({
646
+ source: "cli",
647
+ status: terminalEvent.status === "aborted" ? "aborted" : terminalEvent.status === "failed" ? "failed" : "completed",
648
+ type: "cli_turn_finished",
649
+ contextKey,
650
+ threadId: snapshot.threadId,
651
+ workspace: info.workspace,
652
+ agentId: info.agentId,
653
+ actor: CLI_ACTIVITY_ACTOR,
654
+ prompt: snapshot.latestUserMessage ?? undefined,
655
+ detail: `${snapshot.agentLabel} CLI task ${terminalEvent.status ?? "finished"}.`,
656
+ durationMs: startedAt && terminalEvent.timestamp ? Math.max(0, terminalEvent.timestamp.getTime() - startedAt.getTime()) : undefined,
657
+ });
658
+ state.activityFinishedTurnKey = turnKey;
659
+ }
554
660
  if (mirrorMode !== "off") {
555
661
  const doneText = `${snapshot.agentLabel} CLI task ${terminalEvent.status}.`;
556
662
  if (state.statusMessageId) {
@@ -637,6 +743,18 @@ export function createBot(config, registry) {
637
743
  for (const artifact of (persistedReport?.artifacts ?? report.artifacts)) {
638
744
  await sendArtifactFileByApi(bot.api, chatId, artifact, messageThreadId);
639
745
  }
746
+ const info = session.getInfo();
747
+ appendActivity({
748
+ source: "cli",
749
+ status: "info",
750
+ type: "artifacts_sent",
751
+ contextKey,
752
+ threadId: info.threadId,
753
+ workspace: info.workspace,
754
+ agentId: info.agentId,
755
+ actor: CLI_ACTIVITY_ACTOR,
756
+ detail: summary,
757
+ });
640
758
  if (state)
641
759
  state.artifactsDeliveredForTurnId = turnId;
642
760
  };
@@ -688,9 +806,11 @@ export function createBot(config, registry) {
688
806
  };
689
807
  const auditContext = (ctx, contextKey, session, patch) => {
690
808
  const info = session.getInfo();
809
+ const authUser = getAuthenticatedUser(ctx);
691
810
  audit({
692
811
  contextKey,
693
- actorId: ctx.from?.id,
812
+ actor: telegramActivityActor(ctx),
813
+ actorId: authUser?.user.id ?? ctx.from?.id,
694
814
  actorRole: getUserRole(ctx),
695
815
  agentId: idOf(info),
696
816
  threadId: info.threadId,
@@ -698,10 +818,68 @@ export function createBot(config, registry) {
698
818
  ...patch,
699
819
  });
700
820
  };
821
+ function telegramActivityActor(ctx) {
822
+ const user = ctx.from;
823
+ const authUser = getAuthenticatedUser(ctx);
824
+ const label = authUser?.user.displayName || formatTelegramName(ctx) || user?.username || (user?.id ? String(user.id) : "Telegram user");
825
+ return {
826
+ channel: "telegram",
827
+ id: authUser?.user.id ?? (user?.id !== undefined ? `telegram:${user.id}` : undefined),
828
+ label,
829
+ username: authUser?.user.email ?? user?.username,
830
+ channelUserId: user?.id !== undefined ? String(user.id) : undefined,
831
+ };
832
+ }
833
+ function appendActivity(input) {
834
+ return activityStore.append(input);
835
+ }
836
+ function appendTelegramActivity(ctx, contextKey, session, input) {
837
+ const info = session.getInfo();
838
+ return appendActivity({
839
+ source: "telegram",
840
+ contextKey,
841
+ ...input,
842
+ threadId: input.threadId ?? info.threadId,
843
+ workspace: input.workspace ?? info.workspace,
844
+ agentId: input.agentId ?? idOf(info),
845
+ actor: input.actor ?? telegramActivityActor(ctx),
846
+ });
847
+ }
848
+ function recordTelegramAgentUpdateLifecycle(job) {
849
+ const previous = agentUpdateStates.get(job.id);
850
+ const actor = agentUpdateActors.get(job.id);
851
+ if (job.needsInput && !previous?.needsInput) {
852
+ appendActivity({
853
+ source: "telegram",
854
+ status: "info",
855
+ type: "agent_update_input_required",
856
+ threadId: null,
857
+ workspace: config.workspace,
858
+ agentId: job.agentId,
859
+ actor,
860
+ detail: `${job.agentLabel} ${job.operation} may require input.`,
861
+ });
862
+ }
863
+ if (job.status !== "running" && previous?.status === "running") {
864
+ appendActivity({
865
+ source: "telegram",
866
+ status: job.status === "completed" ? "completed" : job.status === "cancelled" ? "aborted" : "failed",
867
+ type: job.operation === "install" ? `agent_install_${job.status}` : `agent_update_${job.status}`,
868
+ threadId: null,
869
+ workspace: config.workspace,
870
+ agentId: job.agentId,
871
+ actor,
872
+ detail: job.error ?? `${job.agentLabel} ${job.operation} ${job.status}.`,
873
+ durationMs: Math.max(0, Date.parse(job.finishedAt ?? job.updatedAt) - Date.parse(job.startedAt)),
874
+ });
875
+ agentUpdateActors.delete(job.id);
876
+ }
877
+ agentUpdateStates.set(job.id, { status: job.status, needsInput: job.needsInput });
878
+ }
701
879
  const denyIfLocked = async (ctx, contextKey, session) => {
702
880
  const lock = lockStore.get(contextKey);
703
881
  const isAdmin = isAdminUser(ctx);
704
- if (canWriteWithLock(lock, ctx.from?.id, isAdmin)) {
882
+ if (canWriteWithLock(lock, getAuthenticatedUser(ctx)?.user.id, isAdmin)) {
705
883
  return false;
706
884
  }
707
885
  const owner = formatLockOwner(lock);
@@ -711,6 +889,11 @@ export function createBot(config, registry) {
711
889
  status: "denied",
712
890
  detail: text,
713
891
  });
892
+ appendTelegramActivity(ctx, contextKey, session, {
893
+ status: "failed",
894
+ type: "lock_denied",
895
+ detail: text,
896
+ });
714
897
  await safeReply(ctx, escapeHTML(text), { fallbackText: text });
715
898
  return true;
716
899
  };
@@ -803,13 +986,90 @@ export function createBot(config, registry) {
803
986
  ].join("\n");
804
987
  await safeReply(ctx, html, { fallbackText: plain, replyMarkup: keyboard });
805
988
  };
989
+ const remoteClient = new RemoteRelayClient();
990
+ const handleRemoteUserPrompt = async (ctx, contextKey, chatId, prompt) => {
991
+ const targetPeerId = preferencesStore.get(contextKey).targetPeerId ?? undefined;
992
+ const parsed = parseContextKey(contextKey);
993
+ const messageThreadId = parsed.messageThreadId;
994
+ return runChannelPeerPrompt({
995
+ targetPeerId,
996
+ contextKey,
997
+ prompt,
998
+ remoteClient,
999
+ editMinIntervalMs: config.telegramEditMinIntervalMs,
1000
+ typingIntervalMs: TYPING_INTERVAL_MS,
1001
+ sendTyping: () => sendChatActionSafe(ctx.api, chatId, "typing", messageThreadId),
1002
+ sendResponse: async (text) => {
1003
+ const message = await sendTextMessage(ctx.api, chatId, escapeHTML(text), {
1004
+ fallbackText: text,
1005
+ messageThreadId,
1006
+ });
1007
+ return message.message_id;
1008
+ },
1009
+ editResponse: (messageId, text) => safeEditMessage(bot, chatId, messageId, escapeHTML(text), {
1010
+ fallbackText: text,
1011
+ }),
1012
+ sendTurnStart: (remotePrompt) => safeReply(ctx, `<b>Remote peer working on:</b>\n${escapeHTML(remotePrompt)}`, {
1013
+ fallbackText: `Remote peer working on:\n${remotePrompt}`,
1014
+ }),
1015
+ sendToolStart: (toolName) => safeReply(ctx, `<b>Remote tool:</b> <code>${escapeHTML(toolName)}</code>`, {
1016
+ fallbackText: `Remote tool: ${toolName}`,
1017
+ }),
1018
+ sendQueued: async (queueId) => {
1019
+ const keyboard = queueId ? new InlineKeyboard().text("Cancel queued message", `peer_queue_cancel:${targetPeerId}:${queueId}`) : undefined;
1020
+ await safeReply(ctx, escapeHTML(`Remote prompt queued${queueId ? `: ${queueId}` : ""}.`), {
1021
+ fallbackText: `Remote prompt queued${queueId ? `: ${queueId}` : ""}.`,
1022
+ replyMarkup: keyboard,
1023
+ });
1024
+ },
1025
+ sendCompleted: () => safeReply(ctx, escapeHTML("Remote turn completed."), { fallbackText: "Remote turn completed." }),
1026
+ sendFailure: (message) => safeReply(ctx, escapeHTML(`Remote peer failed: ${message}`), {
1027
+ fallbackText: `Remote peer failed: ${message}`,
1028
+ }),
1029
+ });
1030
+ };
1031
+ bot.callbackQuery(/^peer_queue_cancel:([^:]+):([a-z0-9]+)$/, async (ctx) => {
1032
+ const targetPeerId = ctx.match?.[1];
1033
+ const queueId = ctx.match?.[2];
1034
+ const contextKey = contextKeyFromCtx(ctx);
1035
+ if (!targetPeerId || !queueId || !contextKey) {
1036
+ await ctx.answerCallbackQuery();
1037
+ return;
1038
+ }
1039
+ try {
1040
+ await remoteClient.webProxy(targetPeerId, {
1041
+ method: "POST",
1042
+ path: "/api/queue",
1043
+ body: { action: "cancel", id: queueId },
1044
+ contextKey,
1045
+ }, telegramActivityActor(ctx), contextKey);
1046
+ await ctx.answerCallbackQuery({ text: `Cancelled remote queued prompt ${queueId}.` });
1047
+ const chatId = ctx.chat?.id;
1048
+ const messageId = ctx.callbackQuery.message?.message_id;
1049
+ if (chatId && messageId) {
1050
+ await safeEditMessage(bot, chatId, messageId, escapeHTML(`Cancelled remote queued prompt ${queueId}.`), {
1051
+ fallbackText: `Cancelled remote queued prompt ${queueId}.`,
1052
+ });
1053
+ }
1054
+ }
1055
+ catch (error) {
1056
+ await ctx.answerCallbackQuery({ text: friendlyErrorText(error), show_alert: true });
1057
+ }
1058
+ });
806
1059
  const handleUserPrompt = async (ctx, contextKey, chatId, session, prompt, options = {}) => {
807
1060
  if (!canSendSystemMessagesToContext(contextKey)) {
808
1061
  return;
809
1062
  }
810
1063
  const parsed = parseContextKey(contextKey);
811
1064
  const messageThreadId = parsed.messageThreadId;
812
- const envelope = isPromptEnvelopeLike(prompt) ? prompt : toPromptEnvelope(prompt);
1065
+ const rawEnvelope = isPromptEnvelopeLike(prompt) ? prompt : toPromptEnvelope(prompt);
1066
+ const envelope = {
1067
+ ...rawEnvelope,
1068
+ activityActor: rawEnvelope.activityActor ?? telegramActivityActor(ctx),
1069
+ };
1070
+ if (!options.fromQueue && await handleRemoteUserPrompt(ctx, contextKey, chatId, envelope)) {
1071
+ return;
1072
+ }
813
1073
  if (!options.fromQueue && await denyIfLocked(ctx, contextKey, session)) {
814
1074
  return;
815
1075
  }
@@ -840,6 +1100,13 @@ export function createBot(config, registry) {
840
1100
  description: item.description,
841
1101
  detail: busy.kind,
842
1102
  });
1103
+ appendTelegramActivity(ctx, contextKey, session, {
1104
+ status: "queued",
1105
+ type: "prompt_queued",
1106
+ prompt: item.description,
1107
+ detail: `Queued prompt ${item.id} at position ${position}; busy=${busy.kind}`,
1108
+ actor: envelope.activityActor,
1109
+ });
843
1110
  if (busy.kind === "external") {
844
1111
  scheduleExternalQueueDrain(ctx, contextKey, chatId, session);
845
1112
  }
@@ -877,6 +1144,9 @@ export function createBot(config, registry) {
877
1144
  let lastRenderedPlan = "";
878
1145
  let planMessageSending = false;
879
1146
  let lastTurnUsage;
1147
+ let promptStartedAt;
1148
+ const toolActivityNames = new Map();
1149
+ const toolActivityStartedAt = new Map();
880
1150
  const typingInterval = setInterval(() => {
881
1151
  void sendChatActionSafe(bot.api, chatId, "typing", messageThreadId).catch(() => { });
882
1152
  }, TYPING_INTERVAL_MS);
@@ -1080,6 +1350,15 @@ export function createBot(config, registry) {
1080
1350
  progress.lastTool = toolName;
1081
1351
  progress.updatedAt = Date.now();
1082
1352
  progress.toolCounts.set(toolName, (progress.toolCounts.get(toolName) ?? 0) + 1);
1353
+ toolActivityNames.set(toolCallId, toolName);
1354
+ toolActivityStartedAt.set(toolCallId, Date.now());
1355
+ appendTelegramActivity(ctx, contextKey, session, {
1356
+ status: "running",
1357
+ type: "tool_started",
1358
+ prompt: envelope.description,
1359
+ detail: toolName,
1360
+ actor: envelope.activityActor,
1361
+ });
1083
1362
  if (toolVerbosity === "summary") {
1084
1363
  toolCounts.set(toolName, (toolCounts.get(toolName) ?? 0) + 1);
1085
1364
  return;
@@ -1127,6 +1406,18 @@ export function createBot(config, registry) {
1127
1406
  onToolEnd: (toolCallId, isError) => {
1128
1407
  progress.currentTool = undefined;
1129
1408
  progress.updatedAt = Date.now();
1409
+ const activityToolName = toolActivityNames.get(toolCallId) ?? "tool";
1410
+ const activityStartedAt = toolActivityStartedAt.get(toolCallId);
1411
+ appendTelegramActivity(ctx, contextKey, session, {
1412
+ status: isError ? "failed" : "completed",
1413
+ type: isError ? "tool_failed" : "tool_completed",
1414
+ prompt: envelope.description,
1415
+ detail: activityToolName,
1416
+ actor: envelope.activityActor,
1417
+ durationMs: activityStartedAt ? Date.now() - activityStartedAt : undefined,
1418
+ });
1419
+ toolActivityNames.delete(toolCallId);
1420
+ toolActivityStartedAt.delete(toolCallId);
1130
1421
  if (toolVerbosity === "none" || toolVerbosity === "summary") {
1131
1422
  return;
1132
1423
  }
@@ -1266,11 +1557,25 @@ export function createBot(config, registry) {
1266
1557
  replyMarkup: createQueuedPromptCancelKeyboard(contextKey, item.id),
1267
1558
  });
1268
1559
  await updateQueueStatusMessage(contextKey, `Waiting for ${label} CLI task... ${promptStore.list(contextKey).length} queued.`);
1560
+ appendTelegramActivity(ctx, contextKey, session, {
1561
+ status: "queued",
1562
+ type: "prompt_queued",
1563
+ prompt: item.description,
1564
+ detail: `Queued prompt ${item.id} at position 1; external ${label} CLI task active`,
1565
+ actor: envelope.activityActor,
1566
+ });
1269
1567
  scheduleExternalQueueDrain(ctx, contextKey, chatId, session);
1270
1568
  turnProgress.delete(contextKey);
1271
1569
  return;
1272
1570
  }
1273
1571
  promptStore.setLastPrompt(contextKey, envelope);
1572
+ promptStartedAt = Date.now();
1573
+ appendTelegramActivity(ctx, contextKey, session, {
1574
+ status: "running",
1575
+ type: "prompt_started",
1576
+ prompt: envelope.description,
1577
+ actor: envelope.activityActor,
1578
+ });
1274
1579
  auditContext(ctx, contextKey, session, {
1275
1580
  action: "prompt_started",
1276
1581
  status: "ok",
@@ -1300,6 +1605,13 @@ export function createBot(config, registry) {
1300
1605
  status: "ok",
1301
1606
  description: envelope.description,
1302
1607
  });
1608
+ appendTelegramActivity(ctx, contextKey, session, {
1609
+ status: "completed",
1610
+ type: "prompt_completed",
1611
+ prompt: envelope.description,
1612
+ actor: envelope.activityActor,
1613
+ durationMs: promptStartedAt ? Date.now() - promptStartedAt : undefined,
1614
+ });
1303
1615
  }
1304
1616
  catch (error) {
1305
1617
  progress.status = "failed";
@@ -1310,6 +1622,16 @@ export function createBot(config, registry) {
1310
1622
  description: envelope.description,
1311
1623
  detail: progress.error,
1312
1624
  });
1625
+ if (promptStartedAt) {
1626
+ appendTelegramActivity(ctx, contextKey, session, {
1627
+ status: "failed",
1628
+ type: "prompt_failed",
1629
+ prompt: envelope.description,
1630
+ detail: progress.error,
1631
+ actor: envelope.activityActor,
1632
+ durationMs: Date.now() - promptStartedAt,
1633
+ });
1634
+ }
1313
1635
  progress.completedAt = Date.now();
1314
1636
  progress.updatedAt = progress.completedAt;
1315
1637
  stopTyping();
@@ -1400,6 +1722,15 @@ export function createBot(config, registry) {
1400
1722
  source: "turn",
1401
1723
  };
1402
1724
  await deliverArtifactReport(ctx, chatId, report, messageThreadId);
1725
+ const contextKey = contextKeyFromCtx(ctx);
1726
+ const session = contextKey ? registry.get(contextKey) : undefined;
1727
+ if (contextKey && session) {
1728
+ appendTelegramActivity(ctx, contextKey, session, {
1729
+ status: "info",
1730
+ type: "artifacts_sent",
1731
+ detail: formatArtifactSummary(report.artifacts, report.skippedCount, report.omittedCount),
1732
+ });
1733
+ }
1403
1734
  await pruneArtifacts(workspace);
1404
1735
  };
1405
1736
  const deliverArtifactReport = async (ctx, chatId, report, messageThreadId) => {
@@ -1596,6 +1927,11 @@ export function createBot(config, registry) {
1596
1927
  }
1597
1928
  const receivedText = `Received ${stagedFiles.length} media group file${stagedFiles.length === 1 ? "" : "s"}${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}.`;
1598
1929
  await safeReply(pending.ctx, escapeHTML(receivedText), { fallbackText: receivedText });
1930
+ appendTelegramActivity(pending.ctx, pending.contextKey, pending.session, {
1931
+ status: "info",
1932
+ type: "attachment_staged",
1933
+ detail: receivedText,
1934
+ });
1599
1935
  await sendChatActionSafe(pending.ctx.api, pending.chatId, "typing", pending.messageThreadId).catch(() => { });
1600
1936
  const promptInput = {
1601
1937
  stagedFileInstructions: buildFileInstructions(stagedFiles, outDir),
@@ -1625,6 +1961,8 @@ export function createBot(config, registry) {
1625
1961
  checkAgentAuthStatus,
1626
1962
  isTopicContext,
1627
1963
  replyChannelAction,
1964
+ commandService,
1965
+ preferencesStore,
1628
1966
  });
1629
1967
  registerTelegramAgentCommands({
1630
1968
  bot,
@@ -1641,18 +1979,20 @@ export function createBot(config, registry) {
1641
1979
  startAgentLogout,
1642
1980
  hostLoginCommand,
1643
1981
  hostLogoutCommand,
1982
+ appendActivity: (ctx, input) => appendActivity({
1983
+ source: "telegram",
1984
+ ...input,
1985
+ threadId: input.threadId ?? null,
1986
+ workspace: input.workspace ?? config.workspace,
1987
+ actor: input.actor ?? telegramActivityActor(ctx),
1988
+ }),
1644
1989
  });
1645
1990
  registerTelegramPreferenceCommands({
1646
1991
  bot,
1647
1992
  config,
1993
+ commandService,
1648
1994
  preferencesStore,
1649
1995
  getContextSession,
1650
- getEffectiveMirrorMode,
1651
- getEffectiveNotifyMode,
1652
- getEffectiveQuietHours,
1653
- getEffectiveVoiceBackend,
1654
- getEffectiveVoiceLanguage,
1655
- isVoiceTranscribeOnly,
1656
1996
  });
1657
1997
  registerTelegramDiagnosticsCommands({
1658
1998
  bot,
@@ -1673,6 +2013,7 @@ export function createBot(config, registry) {
1673
2013
  getEffectiveVoiceLanguage,
1674
2014
  isVoiceTranscribeOnly,
1675
2015
  replyChannelAction,
2016
+ commandService,
1676
2017
  });
1677
2018
  registerTelegramOperationalCommands({
1678
2019
  bot,
@@ -1686,10 +2027,34 @@ export function createBot(config, registry) {
1686
2027
  getExternalActivity,
1687
2028
  isAdminUser,
1688
2029
  auditContext,
2030
+ getLockOwner: (ctx) => {
2031
+ const authUser = getAuthenticatedUser(ctx);
2032
+ if (!authUser) {
2033
+ return null;
2034
+ }
2035
+ return {
2036
+ userId: authUser.user.id,
2037
+ label: authUser.user.displayName || authUser.user.email,
2038
+ channel: "telegram",
2039
+ channelUserId: ctx.from?.id !== undefined ? String(ctx.from.id) : undefined,
2040
+ };
2041
+ },
1689
2042
  updateSessionMetadata,
1690
2043
  });
1691
2044
  registerTelegramSupportCommands({ bot, config, auditLog, agentUpdates, getUserRole, audit });
1692
- registerTelegramUpdateCommands({ bot, agentUpdates, replyChannelAction, startTelegramAgentUpdate });
2045
+ registerTelegramUpdateCommands({
2046
+ bot,
2047
+ agentUpdates,
2048
+ replyChannelAction,
2049
+ startTelegramAgentUpdate,
2050
+ appendActivity: (ctx, input) => appendActivity({
2051
+ source: "telegram",
2052
+ ...input,
2053
+ threadId: input.threadId ?? null,
2054
+ workspace: input.workspace ?? config.workspace,
2055
+ actor: input.actor ?? telegramActivityActor(ctx),
2056
+ }),
2057
+ });
1693
2058
  bot.command("new", async (ctx) => {
1694
2059
  const chatId = ctx.chat?.id;
1695
2060
  if (!chatId) {
@@ -1718,6 +2083,14 @@ export function createBot(config, registry) {
1718
2083
  try {
1719
2084
  const info = await session.newThread();
1720
2085
  updateSessionMetadata(contextKey, session);
2086
+ appendTelegramActivity(ctx, contextKey, session, {
2087
+ status: "info",
2088
+ type: "session_new",
2089
+ threadId: info.threadId,
2090
+ workspace: info.workspace,
2091
+ agentId: info.agentId,
2092
+ detail: info.workspace,
2093
+ });
1721
2094
  const label = isTopicContext(contextKey) ? "New thread created for this topic." : "New thread created.";
1722
2095
  const policyLine = renderWorkspacePolicyLine(info.workspace, config);
1723
2096
  const plainText = [label, policyLine, "", renderSessionInfoPlain(info)].filter((line) => line !== undefined).join("\n");
@@ -1755,12 +2128,22 @@ export function createBot(config, registry) {
1755
2128
  if (busy.kind === "external") {
1756
2129
  const text = `Cannot abort the external ${busy.activity.agentLabel} CLI task from NordRelay. Stop it in the terminal where it is running; queued Telegram messages will wait.`;
1757
2130
  await safeReply(ctx, escapeHTML(text), { fallbackText: text });
2131
+ appendTelegramActivity(ctx, contextKey, session, {
2132
+ status: "failed",
2133
+ type: "prompt_abort_rejected",
2134
+ detail: text,
2135
+ });
1758
2136
  return;
1759
2137
  }
1760
2138
  await session.abort();
1761
2139
  await safeReply(ctx, escapeHTML("Aborted current operation"), {
1762
2140
  fallbackText: "Aborted current operation",
1763
2141
  });
2142
+ appendTelegramActivity(ctx, contextKey, session, {
2143
+ status: "aborted",
2144
+ type: "prompt_aborted",
2145
+ detail: "Abort requested from Telegram.",
2146
+ });
1764
2147
  }
1765
2148
  catch (error) {
1766
2149
  await safeReply(ctx, `<b>Failed:</b> ${escapeHTML(friendlyErrorText(error))}`, {
@@ -1809,6 +2192,8 @@ export function createBot(config, registry) {
1809
2192
  drainQueuedPrompts,
1810
2193
  handleUserPrompt,
1811
2194
  auditContext,
2195
+ activityActor: telegramActivityActor,
2196
+ appendActivity: appendTelegramActivity,
1812
2197
  });
1813
2198
  registerTelegramArtifactCommands({
1814
2199
  bot,
@@ -1816,6 +2201,13 @@ export function createBot(config, registry) {
1816
2201
  getContextSession,
1817
2202
  deliverArtifactReport,
1818
2203
  deliverArtifactReportZip,
2204
+ appendActivity: (ctx, input) => appendActivity({
2205
+ source: "telegram",
2206
+ ...input,
2207
+ threadId: input.threadId ?? null,
2208
+ workspace: input.workspace ?? config.workspace,
2209
+ actor: input.actor ?? telegramActivityActor(ctx),
2210
+ }),
1819
2211
  });
1820
2212
  bot.command("session", async (ctx) => {
1821
2213
  const contextSession = await getContextSession(ctx, { deferThreadStart: true });
@@ -1910,6 +2302,14 @@ export function createBot(config, registry) {
1910
2302
  try {
1911
2303
  const info = session.handback();
1912
2304
  updateSessionMetadata(contextKey, session);
2305
+ appendTelegramActivity(ctx, contextKey, session, {
2306
+ status: "info",
2307
+ type: "handback",
2308
+ threadId: info.threadId,
2309
+ workspace: info.workspace,
2310
+ agentId: idOf(session.getInfo()),
2311
+ detail: info.command ?? info.threadId ?? "handback",
2312
+ });
1913
2313
  if (!info.threadId) {
1914
2314
  await safeReply(ctx, escapeHTML("This thread has not started yet, so there is no resumable thread ID. Send a message to create one, or use /new to start fresh."), {
1915
2315
  fallbackText: "This thread has not started yet, so there is no resumable thread ID. Send a message to create one, or use /new to start fresh.",
@@ -2005,6 +2405,14 @@ export function createBot(config, registry) {
2005
2405
  try {
2006
2406
  const info = await session.switchSession(threadId);
2007
2407
  updateSessionMetadata(contextKey, session);
2408
+ appendTelegramActivity(ctx, contextKey, session, {
2409
+ status: "info",
2410
+ type: "session_attach",
2411
+ threadId: info.threadId,
2412
+ workspace: info.workspace,
2413
+ agentId: info.agentId,
2414
+ detail: threadId,
2415
+ });
2008
2416
  const policyLine = renderWorkspacePolicyLine(info.workspace, config);
2009
2417
  const html = ["<b>Attached to thread.</b>", policyLine ? `<i>${escapeHTML(policyLine)}</i>` : undefined, "", renderSessionInfoHTML(info)].filter((line) => line !== undefined).join("\n");
2010
2418
  const plain = ["Attached to thread.", policyLine, "", renderSessionInfoPlain(info)].filter((line) => line !== undefined).join("\n");
@@ -2051,6 +2459,14 @@ export function createBot(config, registry) {
2051
2459
  try {
2052
2460
  const info = await session.switchSession(threadId);
2053
2461
  updateSessionMetadata(contextKey, session);
2462
+ appendTelegramActivity(ctx, contextKey, session, {
2463
+ status: "info",
2464
+ type: "session_switch",
2465
+ threadId: info.threadId,
2466
+ workspace: info.workspace,
2467
+ agentId: info.agentId,
2468
+ detail: threadId,
2469
+ });
2054
2470
  const policyLine = renderWorkspacePolicyLine(info.workspace, config);
2055
2471
  const html = ["<b>Switched thread.</b>", policyLine ? `<i>${escapeHTML(policyLine)}</i>` : undefined, "", renderSessionInfoHTML(info)].filter((line) => line !== undefined).join("\n");
2056
2472
  const plain = ["Switched thread.", policyLine, "", renderSessionInfoPlain(info)].filter((line) => line !== undefined).join("\n");
@@ -2137,6 +2553,12 @@ export function createBot(config, registry) {
2137
2553
  return;
2138
2554
  }
2139
2555
  const pinned = registry.pinThread(contextKey, threadId);
2556
+ appendTelegramActivity(ctx, contextKey, session, {
2557
+ status: "info",
2558
+ type: "session_pinned",
2559
+ threadId,
2560
+ detail: threadId,
2561
+ });
2140
2562
  await safeReply(ctx, `<b>Pinned thread:</b> <code>${escapeHTML(threadId)}</code>\n<b>Total pinned:</b> <code>${pinned.length}</code>`, {
2141
2563
  fallbackText: `Pinned thread: ${threadId}\nTotal pinned: ${pinned.length}`,
2142
2564
  });
@@ -2157,6 +2579,12 @@ export function createBot(config, registry) {
2157
2579
  return;
2158
2580
  }
2159
2581
  const pinned = registry.unpinThread(contextKey, threadId);
2582
+ appendTelegramActivity(ctx, contextKey, session, {
2583
+ status: "info",
2584
+ type: "session_unpinned",
2585
+ threadId,
2586
+ detail: threadId,
2587
+ });
2160
2588
  await safeReply(ctx, `<b>Unpinned thread:</b> <code>${escapeHTML(threadId)}</code>\n<b>Total pinned:</b> <code>${pinned.length}</code>`, {
2161
2589
  fallbackText: `Unpinned thread: ${threadId}\nTotal pinned: ${pinned.length}`,
2162
2590
  });
@@ -2272,6 +2700,14 @@ export function createBot(config, registry) {
2272
2700
  const result = session.setFastMode(nextFastMode);
2273
2701
  updateSessionMetadata(contextKey, session);
2274
2702
  const info = session.getInfo();
2703
+ appendTelegramActivity(ctx, contextKey, session, {
2704
+ status: "info",
2705
+ type: "fast_mode_changed",
2706
+ threadId: info.threadId,
2707
+ workspace: info.workspace,
2708
+ agentId: info.agentId,
2709
+ detail: result.enabled ? "on" : "off",
2710
+ });
2275
2711
  const plain = [
2276
2712
  `Fast mode: ${result.enabled ? "on" : "off"}`,
2277
2713
  `Launch profile: ${result.profile.label} (${formatLaunchProfileBehavior(result.profile)})`,
@@ -2384,6 +2820,15 @@ export function createBot(config, registry) {
2384
2820
  });
2385
2821
  }
2386
2822
  const session = registry.get(pending.contextKey);
2823
+ if (session) {
2824
+ appendTelegramActivity(ctx, pending.contextKey, session, {
2825
+ status: "aborted",
2826
+ type: "prompt_approval_denied",
2827
+ prompt: pending.prompt.description,
2828
+ detail: approvalId,
2829
+ actor: pending.prompt.activityActor,
2830
+ });
2831
+ }
2387
2832
  if (chatId && session) {
2388
2833
  void drainQueuedPrompts(ctx, pending.contextKey, chatId, session).catch((error) => {
2389
2834
  console.error("Failed to drain queue after approval denial:", error);
@@ -2402,6 +2847,13 @@ export function createBot(config, registry) {
2402
2847
  fallbackText: `Approved prompt ${approvalId}.`,
2403
2848
  });
2404
2849
  }
2850
+ appendTelegramActivity(ctx, pending.contextKey, contextSession.session, {
2851
+ status: "info",
2852
+ type: "prompt_approval_approved",
2853
+ prompt: pending.prompt.description,
2854
+ detail: approvalId,
2855
+ actor: pending.prompt.activityActor,
2856
+ });
2405
2857
  await handleUserPrompt(ctx, pending.contextKey, chatId ?? parseContextKey(pending.contextKey).chatId, contextSession.session, pending.prompt, {
2406
2858
  approved: true,
2407
2859
  });
@@ -2445,6 +2897,14 @@ export function createBot(config, registry) {
2445
2897
  try {
2446
2898
  const info = await session.switchSession(threadId);
2447
2899
  updateSessionMetadata(contextKey, session);
2900
+ appendTelegramActivity(ctx, contextKey, session, {
2901
+ status: "info",
2902
+ type: "session_switch",
2903
+ threadId: info.threadId,
2904
+ workspace: info.workspace,
2905
+ agentId: info.agentId,
2906
+ detail: threadId,
2907
+ });
2448
2908
  const policyLine = renderWorkspacePolicyLine(info.workspace, config);
2449
2909
  const plainText = ["Switched session.", policyLine, "", renderSessionInfoPlain(info)].filter((line) => line !== undefined).join("\n");
2450
2910
  const html = ["<b>Switched session.</b>", policyLine ? `<i>${escapeHTML(policyLine)}</i>` : undefined, "", renderSessionInfoHTML(info)].filter((line) => line !== undefined).join("\n");
@@ -2507,6 +2967,14 @@ export function createBot(config, registry) {
2507
2967
  try {
2508
2968
  const info = await session.newThread(workspace);
2509
2969
  updateSessionMetadata(contextKey, session);
2970
+ appendTelegramActivity(ctx, contextKey, session, {
2971
+ status: "info",
2972
+ type: "session_new",
2973
+ threadId: info.threadId,
2974
+ workspace: info.workspace,
2975
+ agentId: info.agentId,
2976
+ detail: workspace,
2977
+ });
2510
2978
  const label = isTopicContext(contextKey) ? "New thread created for this topic." : "New thread created.";
2511
2979
  const policyLine = renderWorkspacePolicyLine(info.workspace, config);
2512
2980
  const plainText = [label, policyLine, "", renderSessionInfoPlain(info)].filter((line) => line !== undefined).join("\n");
@@ -2602,6 +3070,14 @@ export function createBot(config, registry) {
2602
3070
  session.setLaunchProfile(profile.id);
2603
3071
  updateSessionMetadata(contextKey, session);
2604
3072
  const info = session.getInfo();
3073
+ appendTelegramActivity(ctx, contextKey, session, {
3074
+ status: "info",
3075
+ type: "launch_profile_changed",
3076
+ threadId: info.threadId,
3077
+ workspace: info.workspace,
3078
+ agentId: info.agentId,
3079
+ detail: info.launchProfileLabel,
3080
+ });
2605
3081
  const html = [
2606
3082
  `<b>Launch profile set to</b> <code>${escapeHTML(info.launchProfileLabel)}</code>`,
2607
3083
  `<b>Behavior:</b> <code>${escapeHTML(info.launchProfileBehavior)}</code>`,
@@ -2664,6 +3140,14 @@ export function createBot(config, registry) {
2664
3140
  session.setLaunchProfile(profile.id);
2665
3141
  updateSessionMetadata(contextKey, session);
2666
3142
  const info = session.getInfo();
3143
+ appendTelegramActivity(ctx, contextKey, session, {
3144
+ status: "info",
3145
+ type: "launch_profile_changed",
3146
+ threadId: info.threadId,
3147
+ workspace: info.workspace,
3148
+ agentId: info.agentId,
3149
+ detail: info.launchProfileLabel,
3150
+ });
2667
3151
  await ctx.answerCallbackQuery({ text: `Launch set to ${info.launchProfileLabel}` });
2668
3152
  const html = [
2669
3153
  `<b>Launch profile set to</b> <code>${escapeHTML(info.launchProfileLabel)}</code>`,
@@ -2710,6 +3194,15 @@ export function createBot(config, registry) {
2710
3194
  try {
2711
3195
  const result = await session.setModelForCurrentSession(slug);
2712
3196
  updateSessionMetadata(contextKey, session);
3197
+ const info = session.getInfo();
3198
+ appendTelegramActivity(ctx, contextKey, session, {
3199
+ status: "info",
3200
+ type: "model_changed",
3201
+ threadId: info.threadId,
3202
+ workspace: info.workspace,
3203
+ agentId: info.agentId,
3204
+ detail: result.value,
3205
+ });
2713
3206
  const scope = formatAgentSettingScope(session.getInfo(), result.appliedToActiveThread);
2714
3207
  const html = `<b>Model set to</b> <code>${escapeHTML(result.value)}</code> — ${escapeHTML(scope)}.`;
2715
3208
  const plainText = `Model set to ${result.value} — ${scope}.`;
@@ -2756,6 +3249,15 @@ export function createBot(config, registry) {
2756
3249
  pendingEffortButtons.delete(contextKey);
2757
3250
  const result = await session.setReasoningEffortForCurrentSession(effort);
2758
3251
  updateSessionMetadata(contextKey, session);
3252
+ const info = session.getInfo();
3253
+ appendTelegramActivity(ctx, contextKey, session, {
3254
+ status: "info",
3255
+ type: "reasoning_changed",
3256
+ threadId: info.threadId,
3257
+ workspace: info.workspace,
3258
+ agentId: info.agentId,
3259
+ detail: result.value,
3260
+ });
2759
3261
  const label = agentReasoningLabel(idOf(session.getInfo()));
2760
3262
  const scope = formatAgentSettingScope(session.getInfo(), result.appliedToActiveThread);
2761
3263
  const html = `⚡ ${escapeHTML(label)} set to <code>${escapeHTML(effort)}</code> — ${escapeHTML(scope)}.`;
@@ -2815,12 +3317,24 @@ export function createBot(config, registry) {
2815
3317
  }
2816
3318
  const preview = trimLine(transcript.replace(/\s+/g, " "), 100);
2817
3319
  await safeReply(ctx, `🎙️ <b>Transcribed:</b> ${escapeHTML(preview)} <i>(via ${escapeHTML(result.backend)}, ${formatDurationSeconds(result.durationMs / 1000)})</i>`, { fallbackText: `🎙️ Transcribed: ${preview} (via ${result.backend}, ${formatDurationSeconds(result.durationMs / 1000)})` });
3320
+ appendTelegramActivity(ctx, contextKey, session, {
3321
+ status: "info",
3322
+ type: "voice_transcribed",
3323
+ prompt: preview,
3324
+ detail: result.backend,
3325
+ durationMs: result.durationMs,
3326
+ });
2818
3327
  }
2819
3328
  catch (error) {
2820
3329
  const note = "Voice uses faster-whisper/parakeet locally or OPENAI_API_KEY for cloud transcription, not CODEX_API_KEY.";
2821
3330
  await safeReply(ctx, `<b>Transcription failed:</b>\n${escapeHTML(friendlyErrorText(error))}\n\n<i>${escapeHTML(note)}</i>`, {
2822
3331
  fallbackText: `Transcription failed:\n${friendlyErrorText(error)}\n\n${note}`,
2823
3332
  });
3333
+ appendTelegramActivity(ctx, contextKey, session, {
3334
+ status: "failed",
3335
+ type: "voice_transcription_failed",
3336
+ detail: friendlyErrorText(error),
3337
+ });
2824
3338
  return;
2825
3339
  }
2826
3340
  finally {
@@ -2915,6 +3429,11 @@ export function createBot(config, registry) {
2915
3429
  if (caption) {
2916
3430
  promptInput.text = caption;
2917
3431
  }
3432
+ appendTelegramActivity(ctx, contextKey, session, {
3433
+ status: "info",
3434
+ type: "attachment_staged",
3435
+ detail: stagedPhoto.safeName,
3436
+ });
2918
3437
  await setReaction(ctx, "👀");
2919
3438
  try {
2920
3439
  await handleUserPrompt(ctx, contextKey, chatId, session, toPromptEnvelope(promptInput, outDir));
@@ -2997,6 +3516,11 @@ export function createBot(config, registry) {
2997
3516
  await safeReply(ctx, `📎 <b>Received:</b> <code>${escapeHTML(stagedFile.safeName)}</code>`, {
2998
3517
  fallbackText: `📎 Received: ${stagedFile.safeName}`,
2999
3518
  });
3519
+ appendTelegramActivity(ctx, contextKey, session, {
3520
+ status: "info",
3521
+ type: "attachment_staged",
3522
+ detail: stagedFile.safeName,
3523
+ });
3000
3524
  // Keep typing visible during the gap between staging and prompt execution
3001
3525
  await sendChatActionSafe(ctx.api, chatId, "typing", ctx.message?.message_thread_id).catch(() => { });
3002
3526
  const outDir = outboxPath(workspace, turnId);