threadwire 0.1.5 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +17 -4
  3. package/TELEGRAM-INGRESS.md +8 -2
  4. package/bin/isolated-runtime.js +5 -0
  5. package/bin/model-broker.js +5 -0
  6. package/docs/card-10520-plan.md +45 -0
  7. package/docs/container-runtime.md +3 -2
  8. package/docs/delegated-result-protocol.md +161 -0
  9. package/docs/evidence-artifacts.md +106 -0
  10. package/docs/isolated-provider-runtime.md +137 -0
  11. package/package.json +5 -1
  12. package/scripts/provider-shims/front-door.sh.template +18 -0
  13. package/scripts/verify-package.js +22 -1
  14. package/src/absolute-deadline.js +94 -0
  15. package/src/cli.js +362 -42
  16. package/src/context-budget-metrics.js +180 -0
  17. package/src/delegated-result-admission.js +377 -0
  18. package/src/docker-api.js +131 -0
  19. package/src/evidence-store.js +1472 -0
  20. package/src/isolated-runtime-client.js +149 -0
  21. package/src/isolated-runtime.js +982 -0
  22. package/src/isolated-state.js +409 -0
  23. package/src/isolated-worker.js +123 -0
  24. package/src/model-broker-policy.js +139 -0
  25. package/src/model-broker.js +313 -0
  26. package/src/mount-policy.js +28 -0
  27. package/src/normalized-output.js +68 -0
  28. package/src/notice-queue.js +22 -7
  29. package/src/providers/opencode.js +42 -18
  30. package/src/relay-write.js +44 -0
  31. package/src/relay.js +6 -6
  32. package/src/run-worker.js +158 -40
  33. package/src/telegram-ingress/command.js +16 -0
  34. package/src/telegram-ingress/config.js +96 -11
  35. package/src/telegram-ingress/core.js +201 -89
  36. package/src/telegram-ingress/http.js +17 -1
  37. package/src/telegram-webhook.js +22 -0
  38. package/src/types.js +2 -2
  39. package/src/worker-control.js +294 -0
  40. package/src/hermes-protocol.js +0 -126
@@ -1,30 +1,35 @@
1
1
  // @ts-check
2
2
 
3
- import {createHash, timingSafeEqual} from "node:crypto"
3
+ import {createHash, randomUUID, timingSafeEqual} from "node:crypto"
4
+ import {evidenceTelegramDestination} from "../evidence-store.js"
4
5
  import {NoticeQueue} from "../notice-queue.js"
5
6
  import {createFetchTransport} from "../notifiers/fetch-transport.js"
6
7
  import {createTelegramSender} from "../notifiers/telegram.js"
7
8
  import {createProvider} from "../providers/index.js"
8
9
  import {Relay} from "../relay.js"
9
10
  import {runWorker} from "../run-worker.js"
11
+ import {WorkerControl} from "../worker-control.js"
10
12
  import {resolveWorkspaceProfile} from "../workspace-profile.js"
11
- import {buildProviderEnvironment} from "./config.js"
12
- import {parseCodeCommand} from "./command.js"
13
+ import {buildProviderEnvironment, collectEvidenceRedactions} from "./config.js"
14
+ import {parseCodeCommand, parseEvidenceCommand} from "./command.js"
13
15
 
14
16
  /**
15
17
  * @typedef {{platform: "telegram", chatId: string, threadId: number | null}} TelegramTarget
16
- * @typedef {{provider: "codex" | "claude" | "opencode", prompt: string, target: TelegramTarget}} AcceptedJob
18
+ * @typedef {{provider: "codex" | "claude" | "opencode", prompt: string, target: TelegramTarget, senderId: string} | {type: "evidence", request: unknown, target: TelegramTarget, senderId: string}} AcceptedJob
17
19
  * @typedef {{
18
20
  * createProvider?: typeof createProvider,
19
21
  * createFetchTransport?: typeof createFetchTransport,
20
22
  * createTelegramSender?: typeof createTelegramSender,
21
23
  * runWorker?: typeof runWorker,
24
+ * WorkerControl?: typeof WorkerControl,
22
25
  * NoticeQueue?: typeof NoticeQueue,
23
26
  * Relay?: typeof Relay,
24
27
  * processNumber?: number,
25
28
  * providerEnvironment?: NodeJS.ProcessEnv,
26
29
  * workspaceProfileOperations?: import("../workspace-profile.js").WorkspaceProfileOperations,
27
30
  * activity?: Pick<import("../activity-log.js").ActivityLog, "recordWorkspace" | "recordStarted" | "recordSession" | "close">,
31
+ * evidenceStore?: import("../evidence-store.js").EvidenceStore,
32
+ * isolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">,
28
33
  * onWorkerFailure?: (message: string) => void,
29
34
  * onWorkerSettled?: () => void
30
35
  * }} IngressDependencies
@@ -55,11 +60,12 @@ export function interpretUpdate(update, config) {
55
60
  if (!isRecord(update)) return null
56
61
  const message = update.message
57
62
  if (!isRecord(message)) return null
63
+ if ([
64
+ "forward_origin", "forward_from", "forward_from_chat", "forward_signature",
65
+ "forward_sender_name", "is_automatic_forward"
66
+ ].some((field) => Object.prototype.hasOwnProperty.call(message, field))) return null
58
67
  if (typeof message.text !== "string") return null
59
68
 
60
- const command = parseCodeCommand(message.text)
61
- if (!command) return null
62
-
63
69
  const chatId = telegramIdString(isRecord(message.chat) ? message.chat.id : undefined)
64
70
  if (chatId === null || !config.allowedChatIds.has(chatId)) return null
65
71
 
@@ -69,7 +75,11 @@ export function interpretUpdate(update, config) {
69
75
  const target = deriveTarget(chatId, message.message_thread_id)
70
76
  if (target === null) return null
71
77
 
72
- return {provider: command.provider, prompt: command.prompt, target}
78
+ const command = parseCodeCommand(message.text)
79
+ if (command) return {provider: command.provider, prompt: command.prompt, target, senderId}
80
+ const evidence = parseEvidenceCommand(message.text)
81
+ if (evidence) return {type: "evidence", request: evidence, target, senderId}
82
+ return null
73
83
  }
74
84
 
75
85
  /**
@@ -83,10 +93,12 @@ export function interpretUpdate(update, config) {
83
93
  * @returns {Promise<void>}
84
94
  */
85
95
  export async function dispatchWorker(job, config, dependencies = {}) {
96
+ if ("type" in job) throw new Error("Worker dispatch requires a worker job")
86
97
  const createProviderImpl = dependencies.createProvider ?? createProvider
87
98
  const createFetchTransportImpl = dependencies.createFetchTransport ?? createFetchTransport
88
99
  const createTelegramSenderImpl = dependencies.createTelegramSender ?? createTelegramSender
89
100
  const runWorkerImpl = dependencies.runWorker ?? runWorker
101
+ const WorkerControlImpl = dependencies.WorkerControl ?? WorkerControl
90
102
  const NoticeQueueImpl = dependencies.NoticeQueue ?? NoticeQueue
91
103
  const RelayImpl = dependencies.Relay ?? Relay
92
104
  const processNumber = dependencies.processNumber ?? process.pid
@@ -106,111 +118,211 @@ export async function dispatchWorker(job, config, dependencies = {}) {
106
118
  )
107
119
  const transport = createFetchTransportImpl(config.botToken, undefined, config.telegramRequestTimeoutMs)
108
120
  const sender = createTelegramSenderImpl(job.target, transport)
109
- const queue = new NoticeQueueImpl({sender})
110
- const relay = new RelayImpl({queue, processNumber, toolMessages: config.toolMessages ?? false})
111
-
112
- /** @type {{resolve: () => void, reject: (error: Error) => void, settled: boolean}} */
113
- const spawnGate = {
114
- settled: false,
115
- resolve() {},
116
- reject() {}
117
- }
118
- const spawned = new Promise((resolve, reject) => {
119
- spawnGate.resolve = () => {
120
- if (spawnGate.settled) return
121
- spawnGate.settled = true
122
- resolve(undefined)
121
+ const control = new WorkerControlImpl({
122
+ sender,
123
+ processNumber,
124
+ toolMessages: config.toolMessages ?? false,
125
+ NoticeQueueClass: NoticeQueueImpl,
126
+ RelayClass: RelayImpl
127
+ })
128
+ const evidenceOwner = dependencies.evidenceStore?.createOwnerScope({
129
+ destinationId: evidenceTelegramDestination(job.target, job.senderId),
130
+ runId: randomUUID()
131
+ })
132
+ const evidence = evidenceOwner === undefined ? undefined : await dependencies.evidenceStore?.createArtifact(evidenceOwner, {
133
+ contentType: "text/plain; charset=utf-8",
134
+ redactions: await collectEvidenceRedactions(dependencies.providerEnvironment ?? {})
135
+ })
136
+ let evidenceTransferred = false
137
+ try {
138
+ await evidence?.append("prompt", `prompt\n${job.prompt}\nprovider-stream\n`)
139
+
140
+ if (job.provider === "codex" && dependencies.isolatedRuntimeClient !== undefined) {
141
+ evidenceTransferred = true
142
+ try {
143
+ const preflight = await dependencies.isolatedRuntimeClient.preflight({
144
+ provider: "codex",
145
+ profile: workspace.profile,
146
+ repositoryRoot: workspace.repositoryRoot,
147
+ cwd: workspace.cwd,
148
+ providerArguments: []
149
+ })
150
+ const exitCode = await dependencies.isolatedRuntimeClient.run({
151
+ preflightId: preflight.preflightId,
152
+ prompt: job.prompt,
153
+ providerArguments: [],
154
+ ...(preflight.deadline === undefined ? {} : {deadline: preflight.deadline}),
155
+ onEvent: async (event) => control.accept(event),
156
+ onRecord: async (record) => {
157
+ const id = provider.sessionId(record)
158
+ if (id !== undefined) dependencies.activity?.recordSession(provider.name, id)
159
+ },
160
+ onStdoutChunk: (chunk) => evidence?.append("provider-stdout", chunk),
161
+ onStderrChunk: (chunk) => evidence?.append("provider-stderr", chunk)
162
+ })
163
+ if (exitCode !== 0) throw new Error(`Isolated Codex worker exited with status ${exitCode}`)
164
+ await control.close()
165
+ if (evidence !== undefined) {
166
+ await evidence.finalize()
167
+ await sender.send(`Evidence: ${evidence.handle}`)
168
+ }
169
+ return
170
+ } catch (error) {
171
+ await control.close().catch(() => {})
172
+ await evidence?.abort()
173
+ throw error
174
+ } finally {
175
+ dependencies.onWorkerSettled?.()
176
+ }
123
177
  }
124
- spawnGate.reject = (error) => {
125
- if (spawnGate.settled) return
126
- spawnGate.settled = true
127
- reject(error)
178
+
179
+ /** @type {{resolve: () => void, reject: (error: Error) => void, settled: boolean}} */
180
+ const spawnGate = {
181
+ settled: false,
182
+ resolve() {},
183
+ reject() {}
128
184
  }
129
- })
185
+ const spawned = new Promise((resolve, reject) => {
186
+ spawnGate.resolve = () => {
187
+ if (spawnGate.settled) return
188
+ spawnGate.settled = true
189
+ resolve(undefined)
190
+ }
191
+ spawnGate.reject = (error) => {
192
+ if (spawnGate.settled) return
193
+ spawnGate.settled = true
194
+ reject(error)
195
+ }
196
+ })
130
197
 
131
- /** @param {unknown} error */
132
- const reportBackgroundFailure = (error) => {
133
- const failure = error instanceof Error ? error : new Error(safeFailureMessage(error))
134
- try {
135
- dependencies.onWorkerFailure?.(safeFailureMessage(failure))
136
- } catch {
198
+ /** @param {unknown} error */
199
+ const reportBackgroundFailure = (error) => {
200
+ const failure = error instanceof Error ? error : new Error(safeFailureMessage(error))
201
+ try {
202
+ dependencies.onWorkerFailure?.(safeFailureMessage(failure))
203
+ } catch {
137
204
  /* Observer errors must not become unhandled rejections. */
205
+ }
138
206
  }
139
- }
140
207
 
141
- const notifySettled = () => {
142
- try {
143
- dependencies.onWorkerSettled?.()
144
- } catch {
208
+ const notifySettled = () => {
209
+ try {
210
+ dependencies.onWorkerSettled?.()
211
+ } catch {
145
212
  /* Observer errors must not become unhandled rejections. */
213
+ }
146
214
  }
147
- }
148
215
 
149
- const running = runWorkerImpl({
150
- executable: provider.executable,
151
- arguments: provider.arguments,
152
- cwd: workspace.cwd,
153
- environment: providerEnvironment,
154
- parse: provider.parse,
155
- onEvent: (event) => relay.accept(event),
156
- onSpawn: (pid) => {
157
- if (pid !== undefined) dependencies.activity?.recordStarted(provider.name, pid)
158
- spawnGate.resolve()
159
- },
160
- onRecord: (record) => {
161
- const id = provider.sessionId(record)
162
- if (id !== undefined) dependencies.activity?.recordSession(provider.name, id)
163
- }
164
- })
216
+ const running = runWorkerImpl({
217
+ executable: provider.executable,
218
+ arguments: provider.arguments,
219
+ cwd: workspace.cwd,
220
+ environment: providerEnvironment,
221
+ parse: provider.parse,
222
+ onEvent: async (event) => control.accept(event),
223
+ onSpawn: (pid) => {
224
+ if (pid !== undefined) dependencies.activity?.recordStarted(provider.name, pid)
225
+ spawnGate.resolve()
226
+ },
227
+ onRecord: async (record) => {
228
+ const id = provider.sessionId(record)
229
+ if (id !== undefined) dependencies.activity?.recordSession(provider.name, id)
230
+ },
231
+ onStdoutChunk: (chunk) => evidence?.append("provider-stdout", chunk),
232
+ onStderrChunk: (chunk) => evidence?.append("provider-stderr", chunk)
233
+ })
165
234
 
166
- // Detached observer: never resolves the spawn gate from process exit — only
167
- // from onSpawn. Pre-spawn failure rejects the gate; post-spawn failure is
168
- // reported. Always notify settlement so concurrency slots are released.
169
- void (async () => {
170
- try {
235
+ // Detached observer: never resolves the spawn gate from process exit — only
236
+ // from onSpawn. Pre-spawn failure rejects the gate; post-spawn failure is
237
+ // reported. Always notify settlement so concurrency slots are released.
238
+ evidenceTransferred = true
239
+ void (async () => {
171
240
  try {
172
- await running
173
- } catch (error) {
174
- const failure = error instanceof Error ? error : new Error(safeFailureMessage(error))
175
- if (spawnGate.settled) {
176
- reportBackgroundFailure(failure)
177
- } else {
178
- spawnGate.reject(failure)
179
- }
180
241
  try {
181
- await relay.close()
182
- } catch {
242
+ await running
243
+ } catch (error) {
244
+ const failure = error instanceof Error ? error : new Error(safeFailureMessage(error))
245
+ if (spawnGate.settled) {
246
+ reportBackgroundFailure(failure)
247
+ } else {
248
+ spawnGate.reject(failure)
249
+ }
250
+ try {
251
+ await control.close()
252
+ } catch {
183
253
  /* Best-effort close after worker failure. */
254
+ }
255
+ return
184
256
  }
185
- return
186
- }
187
257
 
188
- try {
189
- await relay.close()
258
+ try {
259
+ await control.close()
260
+ } catch (error) {
261
+ if (spawnGate.settled) {
262
+ reportBackgroundFailure(error)
263
+ } else {
264
+ spawnGate.reject(error instanceof Error ? error : new Error(safeFailureMessage(error)))
265
+ }
266
+ return
267
+ }
268
+
269
+ if (!spawnGate.settled) {
270
+ spawnGate.reject(new Error("Worker exited before spawn was observed"))
271
+ }
190
272
  } catch (error) {
191
273
  if (spawnGate.settled) {
192
274
  reportBackgroundFailure(error)
193
275
  } else {
194
276
  spawnGate.reject(error instanceof Error ? error : new Error(safeFailureMessage(error)))
195
277
  }
196
- return
278
+ } finally {
279
+ if (evidence !== undefined) {
280
+ try {
281
+ await evidence.finalize()
282
+ await sender.send(`Evidence: ${evidence.handle}`)
283
+ } catch (error) {
284
+ await evidence.abort()
285
+ reportBackgroundFailure(error)
286
+ }
287
+ }
288
+ notifySettled()
197
289
  }
290
+ })()
198
291
 
199
- if (!spawnGate.settled) {
200
- spawnGate.reject(new Error("Worker exited before spawn was observed"))
201
- }
202
- } catch (error) {
203
- if (spawnGate.settled) {
204
- reportBackgroundFailure(error)
205
- } else {
206
- spawnGate.reject(error instanceof Error ? error : new Error(safeFailureMessage(error)))
207
- }
208
- } finally {
209
- notifySettled()
210
- }
211
- })()
292
+ await spawned
293
+ } finally {
294
+ if (!evidenceTransferred) await evidence?.abort()
295
+ }
296
+ }
297
+
298
+ /** @param {Extract<AcceptedJob, {type: "evidence"}>} job @param {{botToken: string, telegramRequestTimeoutMs?: number}} config @param {IngressDependencies} dependencies */
299
+ export async function dispatchEvidenceRead(job, config, dependencies) {
300
+ if (!dependencies.evidenceStore) throw new Error("Evidence retrieval is unavailable")
301
+ const transport = (dependencies.createFetchTransport ?? createFetchTransport)(config.botToken, undefined, config.telegramRequestTimeoutMs)
302
+ const sender = (dependencies.createTelegramSender ?? createTelegramSender)(job.target, transport)
303
+ const result = await dependencies.evidenceStore.readTelegram({...job.target, senderId: job.senderId}, job.request)
304
+ const label = result.encoding === "base64"
305
+ ? `Evidence (${result.contentType}, base64, truncated):\n`
306
+ : `Evidence (${result.contentType}, redacted, truncated):\n`
307
+ const boundedData = telegramEvidenceText(result.data, Math.max(0, 3_000 - Buffer.byteLength(label)))
308
+ await sender.send({
309
+ text: result.encoding === "base64"
310
+ ? `Evidence (${result.contentType}, base64${boundedData.truncated ? ", truncated" : ""}):\n${boundedData.text}`
311
+ : `Evidence (${result.contentType}${result.redacted ? ", redacted" : ""}${result.truncated || boundedData.truncated ? ", truncated" : ""}):\n${boundedData.text}`
312
+ })
313
+ }
212
314
 
213
- await spawned
315
+ /** @param {string} value @param {number} maxBytes */
316
+ function telegramEvidenceText(value, maxBytes) {
317
+ let text = ""
318
+ let bytes = 0
319
+ for (const character of value) {
320
+ const width = Buffer.byteLength(character, "utf8")
321
+ if (bytes + width > maxBytes) return {text, truncated: true}
322
+ text += character
323
+ bytes += width
324
+ }
325
+ return {text, truncated: false}
214
326
  }
215
327
 
216
328
  /** @param {string} chatId @param {unknown} threadId @returns {TelegramTarget | null} */
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import {createConcurrencyLimiter} from "./concurrency.js"
4
- import {dispatchWorker, interpretUpdate, secretsEqual} from "./core.js"
4
+ import {dispatchEvidenceRead, dispatchWorker, interpretUpdate, secretsEqual} from "./core.js"
5
5
  import {createUpdateGuard, parseUpdateId} from "./update-guard.js"
6
6
  import {WorkspaceProviderMismatchError} from "../workspace-profile.js"
7
7
 
@@ -112,6 +112,22 @@ async function handleRequest(request, response, config, dependencies) {
112
112
  return
113
113
  }
114
114
 
115
+ if ("type" in job) {
116
+ try {
117
+ await dispatchEvidenceRead(job, {
118
+ botToken: config.botToken,
119
+ telegramRequestTimeoutMs: config.telegramRequestTimeoutMs
120
+ }, dependencies)
121
+ dependencies.updateGuard.complete(updateId)
122
+ send(response, 200)
123
+ } catch (error) {
124
+ dependencies.updateGuard.release(updateId)
125
+ dependencies.onOperationalError?.(error instanceof Error ? error.message : "Evidence retrieval failed")
126
+ send(response, 500)
127
+ }
128
+ return
129
+ }
130
+
115
131
  if (!dependencies.concurrency.tryAcquire()) {
116
132
  dependencies.updateGuard.release(updateId)
117
133
  send(response, 503)
@@ -2,8 +2,10 @@
2
2
 
3
3
  import {createServer} from "node:http"
4
4
  import {ActivityLog} from "./activity-log.js"
5
+ import {EvidenceStore} from "./evidence-store.js"
5
6
  import {buildProviderEnvironment, parseIngressConfig, resolveIngressEnvironment} from "./telegram-ingress/config.js"
6
7
  import {createWebhookHandler} from "./telegram-ingress/http.js"
8
+ import {isolatedRuntimeClientFromEnvironment} from "./isolated-runtime-client.js"
7
9
 
8
10
  /**
9
11
  * Start the standalone Threadwire Telegram webhook service.
@@ -15,6 +17,7 @@ import {createWebhookHandler} from "./telegram-ingress/http.js"
15
17
  * onWorkerFailure?: (message: string) => void,
16
18
  * providerEnvironment?: NodeJS.ProcessEnv,
17
19
  * activity?: Pick<ActivityLog, "recordWorkspace" | "recordStarted" | "recordSession" | "close">,
20
+ * evidenceStore?: EvidenceStore,
18
21
  * handlerDependencies?: import("./telegram-ingress/http.js").WebhookDependencies
19
22
  * }} [options]
20
23
  * @returns {Promise<{server: import("node:http").Server, config: import("./telegram-ingress/config.js").IngressConfig, handler: (request: import("node:http").IncomingMessage, response: import("node:http").ServerResponse) => void}>}
@@ -32,16 +35,35 @@ export async function startTelegramWebhook(options = {}) {
32
35
  const providerEnvironment = buildProviderEnvironment(
33
36
  options.providerEnvironment ?? environment
34
37
  )
38
+ const isolatedConfigured = environment.THREADWIRE_ISOLATED_RUNTIME_URL !== undefined
39
+ || environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN !== undefined
40
+ const isolatedRuntimeClient = options.handlerDependencies?.isolatedRuntimeClient
41
+ ?? (isolatedConfigured ? isolatedRuntimeClientFromEnvironment(environment) : undefined)
35
42
  const activity = options.activity ?? options.handlerDependencies?.activity ?? new ActivityLog("/var/lib/threadwire/activity/threadwire.jsonl")
43
+ const evidenceStore = options.evidenceStore ?? options.handlerDependencies?.evidenceStore ?? await EvidenceStore.open({
44
+ root: config.evidenceRoot ?? "/var/lib/threadwire/evidence"
45
+ })
36
46
 
37
47
  const handler = createWebhookHandler(config, {
38
48
  ...options.handlerDependencies,
39
49
  activity,
50
+ evidenceStore,
40
51
  providerEnvironment,
52
+ ...(isolatedRuntimeClient === undefined ? {} : {isolatedRuntimeClient}),
41
53
  onOperationalError,
42
54
  onWorkerFailure
43
55
  })
44
56
  const server = createServerImpl(handler)
57
+ if (options.evidenceStore === undefined && options.handlerDependencies?.evidenceStore === undefined) {
58
+ const cleanupTimer = setInterval(() => {
59
+ void evidenceStore.cleanup().catch((error) => onOperationalError(error instanceof Error ? error.message : "Evidence cleanup failed"))
60
+ }, 60 * 60 * 1000)
61
+ cleanupTimer.unref()
62
+ server.once("close", () => {
63
+ clearInterval(cleanupTimer)
64
+ void evidenceStore.close()
65
+ })
66
+ }
45
67
 
46
68
  if (options.listen) {
47
69
  await options.listen(server, config.port, config.host)
package/src/types.js CHANGED
@@ -7,8 +7,8 @@
7
7
  * @typedef {{type: "diagnostic", level: "warning" | "error", summary: string}} DiagnosticEvent
8
8
  * @typedef {TextDeltaEvent | ToolEvent | LifecycleEvent | DiagnosticEvent} WorkerEvent
9
9
  * @typedef {{platform: "telegram", chatId: string, threadId: number | null}} TelegramTarget
10
- * @typedef {{text: string, format?: "javascript-code", label?: string, batchSeparator?: string}} TextNotice
11
- * @typedef {{activity: {key: string, phase: "started" | "finished"}, text: string, parseMode?: "HTML"}} ActivityNotice
10
+ * @typedef {{text: string, format?: "javascript-code", label?: string, batchSeparator?: string, metricClass?: string}} TextNotice
11
+ * @typedef {{activity: {key: string, phase: "started" | "finished"}, text: string, parseMode?: "HTML", metricClass?: string}} ActivityNotice
12
12
  * @typedef {TextNotice | ActivityNotice} RenderedNotice
13
13
  * @typedef {{text: string, parseMode?: "HTML"}} OutgoingMessage
14
14
  * @typedef {{messageId: number}} SentMessage