@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
@@ -1,8 +1,5 @@
1
- import { formatQuietHours, isQuietNow, parseMirrorMode, parseNotifyMode, parseQuietHours, parseVoiceBackendPreference, } from "./bot-preferences.js";
2
- import { capabilitiesOf, idOf, labelOf, parseToggle, } from "./bot-rendering.js";
3
- import { friendlyErrorText } from "./error-messages.js";
1
+ import { capabilitiesOf, labelOf, } from "./bot-rendering.js";
4
2
  import { escapeHTML } from "./format.js";
5
- import { getAvailableBackends } from "./voice.js";
6
3
  import { evaluateWorkspacePolicy, filterAllowedWorkspaces, renderWorkspacePolicyLine, } from "./workspace-policy.js";
7
4
  import { safeReply } from "./telegram-output.js";
8
5
  export function registerTelegramPreferenceCommands(options) {
@@ -18,28 +15,15 @@ export function registerTelegramPreferenceCommands(options) {
18
15
  return;
19
16
  }
20
17
  const argument = (ctx.message?.text ?? "").replace(/^\/mirror(?:@\w+)?\s*/i, "").trim();
21
- if (argument) {
22
- const mode = parseMirrorMode(argument, options.getEffectiveMirrorMode(contextKey));
23
- if (!["off", "status", "final", "full"].includes(argument.toLowerCase())) {
24
- await safeReply(ctx, escapeHTML("Usage: /mirror [off|status|final|full]"), {
25
- fallbackText: "Usage: /mirror [off|status|final|full]",
26
- });
27
- return;
28
- }
29
- options.preferencesStore.update(contextKey, { mirrorMode: mode });
30
- }
31
- const mode = options.getEffectiveMirrorMode(contextKey);
32
- const plain = [
33
- `CLI mirroring: ${mode}`,
34
- `Minimum update interval: ${options.config.telegramMirrorMinUpdateMs} ms`,
35
- "Modes: off, status, final, full",
36
- ].join("\n");
37
- const html = [
38
- `<b>CLI mirroring:</b> <code>${escapeHTML(mode)}</code>`,
39
- `<b>Minimum update interval:</b> <code>${options.config.telegramMirrorMinUpdateMs} ms</code>`,
40
- "<b>Modes:</b> <code>off</code>, <code>status</code>, <code>final</code>, <code>full</code>",
41
- ].join("\n");
42
- await safeReply(ctx, html, { fallbackText: plain });
18
+ const response = options.commandService.renderMirrorPreference({
19
+ source: "telegram",
20
+ contextKey,
21
+ argument,
22
+ preferencesStore: options.preferencesStore,
23
+ cliMirrorSupported: capabilitiesOf(session.getInfo()).cliMirror,
24
+ agentLabel: labelOf(session.getInfo()),
25
+ });
26
+ await safeReply(ctx, response.html, { fallbackText: response.plain });
43
27
  });
44
28
  options.bot.command("notify", async (ctx) => {
45
29
  const contextSession = await options.getContextSession(ctx, { deferThreadStart: true });
@@ -48,45 +32,13 @@ export function registerTelegramPreferenceCommands(options) {
48
32
  }
49
33
  const { contextKey } = contextSession;
50
34
  const argument = (ctx.message?.text ?? "").replace(/^\/notify(?:@\w+)?\s*/i, "").trim();
51
- if (argument) {
52
- const quietMatch = argument.match(/^quiet\s+(.+)$/i);
53
- if (quietMatch) {
54
- let quietHours;
55
- try {
56
- quietHours = quietMatch[1].toLowerCase() === "off" ? null : parseQuietHours(quietMatch[1]);
57
- }
58
- catch (error) {
59
- await safeReply(ctx, escapeHTML(`Invalid quiet hours: ${friendlyErrorText(error)}`), {
60
- fallbackText: `Invalid quiet hours: ${friendlyErrorText(error)}`,
61
- });
62
- return;
63
- }
64
- options.preferencesStore.update(contextKey, { quietHours });
65
- }
66
- else {
67
- const mode = parseNotifyMode(argument, options.getEffectiveNotifyMode(contextKey));
68
- if (!["off", "minimal", "all"].includes(argument.toLowerCase())) {
69
- await safeReply(ctx, escapeHTML("Usage: /notify [off|minimal|all] or /notify quiet HH-HH"), {
70
- fallbackText: "Usage: /notify [off|minimal|all] or /notify quiet HH-HH",
71
- });
72
- return;
73
- }
74
- options.preferencesStore.update(contextKey, { notifyMode: mode });
75
- }
76
- }
77
- const mode = options.getEffectiveNotifyMode(contextKey);
78
- const quietHours = options.getEffectiveQuietHours(contextKey);
79
- const plain = [
80
- `Notifications: ${mode}`,
81
- `Quiet hours: ${formatQuietHours(quietHours)}`,
82
- `Currently quiet: ${isQuietNow(quietHours) ? "yes" : "no"}`,
83
- ].join("\n");
84
- const html = [
85
- `<b>Notifications:</b> <code>${escapeHTML(mode)}</code>`,
86
- `<b>Quiet hours:</b> <code>${escapeHTML(formatQuietHours(quietHours))}</code>`,
87
- `<b>Currently quiet:</b> <code>${isQuietNow(quietHours) ? "yes" : "no"}</code>`,
88
- ].join("\n");
89
- await safeReply(ctx, html, { fallbackText: plain });
35
+ const response = options.commandService.renderNotifyPreference({
36
+ source: "telegram",
37
+ contextKey,
38
+ argument,
39
+ preferencesStore: options.preferencesStore,
40
+ });
41
+ await safeReply(ctx, response.html, { fallbackText: response.plain });
90
42
  });
91
43
  options.bot.command("workspaces", async (ctx) => {
92
44
  const contextSession = await options.getContextSession(ctx, { deferThreadStart: true });
@@ -131,68 +83,12 @@ export function registerTelegramPreferenceCommands(options) {
131
83
  }
132
84
  const { contextKey } = contextSession;
133
85
  const argument = (ctx.message?.text ?? "").replace(/^\/voice(?:@\w+)?\s*/i, "").trim();
134
- if (argument) {
135
- const parts = argument.split(/\s+/);
136
- const key = parts[0]?.toLowerCase();
137
- const value = parts.slice(1).join(" ").trim();
138
- if (key === "backend" && value) {
139
- options.preferencesStore.update(contextKey, { voiceBackend: parseVoiceBackendPreference(value) });
140
- }
141
- else if (key === "language") {
142
- options.preferencesStore.update(contextKey, { voiceLanguage: value && value.toLowerCase() !== "auto" ? value : null });
143
- }
144
- else if (key === "transcribe_only" || key === "transcribe-only") {
145
- const enabled = parseToggle(value);
146
- if (enabled === undefined) {
147
- await safeReply(ctx, escapeHTML("Usage: /voice transcribe_only on|off"), {
148
- fallbackText: "Usage: /voice transcribe_only on|off",
149
- });
150
- return;
151
- }
152
- options.preferencesStore.update(contextKey, { voiceTranscribeOnly: enabled });
153
- }
154
- else {
155
- await safeReply(ctx, escapeHTML("Usage: /voice, /voice backend auto|parakeet|faster-whisper|openai, /voice language auto|<code>, /voice transcribe_only on|off"), {
156
- fallbackText: "Usage: /voice, /voice backend auto|parakeet|faster-whisper|openai, /voice language auto|<code>, /voice transcribe_only on|off",
157
- });
158
- return;
159
- }
160
- }
161
- const backends = await getAvailableBackends().catch(() => []);
162
- if (backends.length === 0) {
163
- await safeReply(ctx, [
164
- "<b>Voice transcription is not available.</b>",
165
- "",
166
- "Install <code>faster-whisper</code> + ffmpeg, install <code>parakeet-coreml</code> on macOS Apple Silicon, or set <code>OPENAI_API_KEY</code>.",
167
- "<i>Cloud transcription uses OPENAI_API_KEY, not CODEX_API_KEY.</i>",
168
- ].join("\n"), {
169
- fallbackText: [
170
- "Voice transcription is not available.",
171
- "",
172
- "Install faster-whisper + ffmpeg, install parakeet-coreml on macOS Apple Silicon, or set OPENAI_API_KEY.",
173
- "Cloud transcription uses OPENAI_API_KEY, not CODEX_API_KEY.",
174
- ].join("\n"),
175
- });
176
- return;
177
- }
178
- const joined = backends.join(" + ");
179
- const backendPreference = options.getEffectiveVoiceBackend(contextKey);
180
- const language = options.getEffectiveVoiceLanguage(contextKey);
181
- const transcribeOnly = options.isVoiceTranscribeOnly(contextKey);
182
- const plain = [
183
- `Voice backends: ${joined}`,
184
- `Preferred backend: ${backendPreference}`,
185
- `Language: ${language ?? "auto"}`,
186
- `Transcribe only: ${transcribeOnly ? "on" : "off"}`,
187
- ].join("\n");
188
- const html = [
189
- `<b>Voice backends:</b> <code>${escapeHTML(joined)}</code>`,
190
- `<b>Preferred backend:</b> <code>${escapeHTML(backendPreference)}</code>`,
191
- `<b>Language:</b> <code>${escapeHTML(language ?? "auto")}</code>`,
192
- `<b>Transcribe only:</b> <code>${transcribeOnly ? "on" : "off"}</code>`,
193
- ].join("\n");
194
- await safeReply(ctx, html, {
195
- fallbackText: plain,
86
+ const response = await options.commandService.renderVoicePreference({
87
+ source: "telegram",
88
+ contextKey,
89
+ argument,
90
+ preferencesStore: options.preferencesStore,
196
91
  });
92
+ await safeReply(ctx, response.html, { fallbackText: response.plain });
197
93
  });
198
94
  }
@@ -48,12 +48,21 @@ export function registerTelegramQueueCommands(options) {
48
48
  const minutes = Math.min(7 * 24 * 60, Math.max(1, Number(laterMatch[1])));
49
49
  const text = laterMatch[2].trim();
50
50
  const notBefore = Date.now() + minutes * 60 * 1000;
51
- const item = promptStore.enqueue(contextKey, toPromptEnvelope(text), { notBefore });
51
+ const item = promptStore.enqueue(contextKey, {
52
+ ...toPromptEnvelope(text),
53
+ activityActor: options.activityActor?.(ctx),
54
+ }, { notBefore });
52
55
  const message = `Queued prompt ${item.id} for ${formatLocalDateTime(new Date(notBefore))}.`;
53
56
  await safeReply(ctx, escapeHTML(message), {
54
57
  fallbackText: message,
55
58
  replyMarkup: createQueuedPromptCancelKeyboard(contextKey, item.id),
56
59
  });
60
+ options.appendActivity?.(ctx, contextKey, session, {
61
+ status: "queued",
62
+ type: "prompt_queued",
63
+ prompt: item.description,
64
+ detail: `Queued prompt ${item.id} for ${formatLocalDateTime(new Date(notBefore))}.`,
65
+ });
57
66
  options.auditContext(ctx, contextKey, session, {
58
67
  action: "prompt_queued",
59
68
  status: "ok",
@@ -81,12 +90,22 @@ export function registerTelegramQueueCommands(options) {
81
90
  const message = `Queue paused. ${promptStore.list(contextKey).length} queued.`;
82
91
  await safeReply(ctx, escapeHTML(message), { fallbackText: message });
83
92
  await options.updateQueueStatusMessage(contextKey, message);
93
+ options.appendActivity?.(ctx, contextKey, session, {
94
+ status: "info",
95
+ type: "queue_pause",
96
+ detail: message,
97
+ });
84
98
  return;
85
99
  }
86
100
  if (/^resume$/i.test(argument)) {
87
101
  promptStore.resume(contextKey);
88
102
  const message = `Queue resumed. ${promptStore.list(contextKey).length} queued.`;
89
103
  await safeReply(ctx, escapeHTML(message), { fallbackText: message });
104
+ options.appendActivity?.(ctx, contextKey, session, {
105
+ status: "info",
106
+ type: "queue_resume",
107
+ detail: message,
108
+ });
90
109
  if (chatId) {
91
110
  void options.drainQueuedPrompts(ctx, contextKey, chatId, session).catch((error) => {
92
111
  console.error("Failed to drain queue after resume:", error);
@@ -110,6 +129,12 @@ export function registerTelegramQueueCommands(options) {
110
129
  }
111
130
  const message = `Moved queued prompt ${item.id} ${direction}.`;
112
131
  await safeReply(ctx, escapeHTML(message), { fallbackText: message });
132
+ options.appendActivity?.(ctx, contextKey, session, {
133
+ status: "info",
134
+ type: "queue_move",
135
+ prompt: item.description,
136
+ detail: message,
137
+ });
113
138
  return;
114
139
  }
115
140
  const runMatch = argument.match(/^run\s+([a-z0-9]+)$/i);
@@ -123,6 +148,12 @@ export function registerTelegramQueueCommands(options) {
123
148
  }
124
149
  promptStore.enqueueFront(contextKey, item);
125
150
  promptStore.resume(contextKey);
151
+ options.appendActivity?.(ctx, contextKey, session, {
152
+ status: "info",
153
+ type: "queue_run",
154
+ prompt: item.description,
155
+ detail: `Queued prompt ${item.id} moved to next.`,
156
+ });
126
157
  if (!chatId) {
127
158
  return;
128
159
  }
@@ -159,9 +190,15 @@ export function registerTelegramQueueCommands(options) {
159
190
  if (!contextSession) {
160
191
  return;
161
192
  }
162
- const count = promptStore.clear(contextSession.contextKey);
193
+ const { contextKey, session } = contextSession;
194
+ const count = promptStore.clear(contextKey);
163
195
  const message = `Cleared ${count} queued prompt${count === 1 ? "" : "s"}.`;
164
196
  await safeReply(ctx, escapeHTML(message), { fallbackText: message });
197
+ options.appendActivity?.(ctx, contextKey, session, {
198
+ status: "info",
199
+ type: "queue_clear",
200
+ detail: message,
201
+ });
165
202
  });
166
203
  bot.command("cancel", async (ctx) => {
167
204
  const contextSession = await options.getContextSession(ctx, { deferThreadStart: true });
@@ -176,7 +213,8 @@ export function registerTelegramQueueCommands(options) {
176
213
  });
177
214
  return;
178
215
  }
179
- const removed = promptStore.remove(contextSession.contextKey, id);
216
+ const { contextKey, session } = contextSession;
217
+ const removed = promptStore.remove(contextKey, id);
180
218
  if (!removed) {
181
219
  await safeReply(ctx, escapeHTML(`No queued prompt found with id ${id}.`), {
182
220
  fallbackText: `No queued prompt found with id ${id}.`,
@@ -186,6 +224,12 @@ export function registerTelegramQueueCommands(options) {
186
224
  await safeReply(ctx, escapeHTML(`Cancelled queued prompt ${removed.id}.`), {
187
225
  fallbackText: `Cancelled queued prompt ${removed.id}.`,
188
226
  });
227
+ options.appendActivity?.(ctx, contextKey, session, {
228
+ status: "aborted",
229
+ type: "queue_cancel",
230
+ prompt: removed.description,
231
+ detail: `Cancelled queued prompt ${removed.id}.`,
232
+ });
189
233
  });
190
234
  bot.callbackQuery(/^queue_(cancel|remove|top|up|down|run):(-?\d+(?::\d+)?):([a-z0-9]+)$/, async (ctx) => {
191
235
  const action = ctx.match?.[1];
@@ -209,6 +253,15 @@ export function registerTelegramQueueCommands(options) {
209
253
  ? promptStore.moveUp(contextKey, queueId)
210
254
  : promptStore.moveDown(contextKey, queueId);
211
255
  await ctx.answerCallbackQuery({ text: item ? `Moved ${queueId} ${action}.` : "Queued prompt not found." });
256
+ const session = item ? options.getSession(contextKey) : undefined;
257
+ if (item && session) {
258
+ options.appendActivity?.(ctx, contextKey, session, {
259
+ status: "info",
260
+ type: "queue_move",
261
+ prompt: item.description,
262
+ detail: `Moved queued prompt ${item.id} ${action}.`,
263
+ });
264
+ }
212
265
  if (chatId && messageId) {
213
266
  const rendered = renderQueueList(promptStore, contextKey, promptStore.list(contextKey));
214
267
  await safeEditMessage(bot, chatId, messageId, rendered.html, {
@@ -227,6 +280,15 @@ export function registerTelegramQueueCommands(options) {
227
280
  promptStore.enqueueFront(contextKey, item);
228
281
  promptStore.resume(contextKey);
229
282
  await ctx.answerCallbackQuery({ text: `Queued prompt ${queueId} moved to next.` });
283
+ const session = options.getSession(contextKey);
284
+ if (session) {
285
+ options.appendActivity?.(ctx, contextKey, session, {
286
+ status: "info",
287
+ type: "queue_run",
288
+ prompt: item.description,
289
+ detail: `Queued prompt ${item.id} moved to next.`,
290
+ });
291
+ }
230
292
  if (chatId && messageId) {
231
293
  const rendered = renderQueueList(promptStore, contextKey, promptStore.list(contextKey));
232
294
  await safeEditMessage(bot, chatId, messageId, rendered.html, {
@@ -234,7 +296,6 @@ export function registerTelegramQueueCommands(options) {
234
296
  replyMarkup: rendered.keyboard,
235
297
  });
236
298
  }
237
- const session = options.getSession(contextKey);
238
299
  if (chatId && session && !options.getBusyReason(contextKey).busy) {
239
300
  void options.drainQueuedPrompts(ctx, contextKey, chatId, session).catch((error) => {
240
301
  console.error("Failed to drain queue after run-now callback:", error);
@@ -262,6 +323,15 @@ export function registerTelegramQueueCommands(options) {
262
323
  }
263
324
  const message = `Cancelled queued prompt ${removed.id}.`;
264
325
  await ctx.answerCallbackQuery({ text: message });
326
+ const session = options.getSession(contextKey);
327
+ if (session) {
328
+ options.appendActivity?.(ctx, contextKey, session, {
329
+ status: "aborted",
330
+ type: "queue_cancel",
331
+ prompt: removed.description,
332
+ detail: message,
333
+ });
334
+ }
265
335
  if (!chatId || !messageId) {
266
336
  return;
267
337
  }
@@ -26,6 +26,13 @@ export function registerTelegramSupportCommands(options) {
26
26
  action: "command",
27
27
  status: "ok",
28
28
  contextKey,
29
+ actor: {
30
+ channel: "telegram",
31
+ id: ctx.from?.id !== undefined ? `telegram:${ctx.from.id}` : undefined,
32
+ label: ctx.from?.username || ctx.from?.first_name || (ctx.from?.id !== undefined ? String(ctx.from.id) : undefined),
33
+ username: ctx.from?.username,
34
+ channelUserId: ctx.from?.id !== undefined ? String(ctx.from.id) : undefined,
35
+ },
29
36
  actorId: ctx.from?.id,
30
37
  actorRole: options.getUserRole(ctx),
31
38
  description: "export diagnostics bundle",
@@ -33,12 +33,26 @@ export function registerTelegramUpdateCommands(deps) {
33
33
  }
34
34
  if (subcommand === "cancel" && tokens[1]) {
35
35
  const job = agentUpdates.cancel(tokens[1]);
36
+ deps.appendActivity?.(ctx, {
37
+ status: "aborted",
38
+ type: "agent_update_cancel_requested",
39
+ threadId: null,
40
+ agentId: job.agentId,
41
+ detail: `${job.agentLabel} ${job.operation} cancellation requested.`,
42
+ });
36
43
  const rendered = renderAgentUpdateJobAction(job);
37
44
  await replyChannelAction(ctx, rendered);
38
45
  return;
39
46
  }
40
47
  if ((subcommand === "input" || subcommand === "send") && tokens[1] && tokens.slice(2).join(" ").trim()) {
41
48
  const job = agentUpdates.sendInput(tokens[1], tokens.slice(2).join(" "));
49
+ deps.appendActivity?.(ctx, {
50
+ status: "info",
51
+ type: "agent_update_input_sent",
52
+ threadId: null,
53
+ agentId: job.agentId,
54
+ detail: `Input sent to ${job.agentLabel} ${job.operation}.`,
55
+ });
42
56
  const rendered = renderAgentUpdateJobAction(job);
43
57
  await replyChannelAction(ctx, rendered);
44
58
  return;
@@ -54,6 +68,12 @@ export function registerTelegramUpdateCommands(deps) {
54
68
  return;
55
69
  }
56
70
  const update = spawnSelfUpdate();
71
+ deps.appendActivity?.(ctx, {
72
+ status: "info",
73
+ type: "update_started",
74
+ threadId: null,
75
+ detail: `${update.method}: ${update.summary}`,
76
+ });
57
77
  const rendered = renderSelfUpdateStartedAction(update);
58
78
  await replyChannelAction(ctx, rendered);
59
79
  });
@@ -87,6 +107,13 @@ export function registerTelegramUpdateCommands(deps) {
87
107
  return;
88
108
  }
89
109
  const job = agentUpdates.cancel(id);
110
+ deps.appendActivity?.(ctx, {
111
+ status: "aborted",
112
+ type: "agent_update_cancel_requested",
113
+ threadId: null,
114
+ agentId: job.agentId,
115
+ detail: `${job.agentLabel} ${job.operation} cancellation requested.`,
116
+ });
90
117
  const rendered = renderAgentUpdateJobAction(job);
91
118
  await replyChannelAction(ctx, rendered);
92
119
  });