polygram 0.37.2 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/lib/handlers/config-ui.js +53 -44
- package/lib/telegram/display-hint.js +1 -0
- package/package.json +1 -1
- package/polygram.js +120 -55
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.38.0",
|
|
5
5
|
"description": "Telegram integration for Claude Code that preserves the OpenClaw per-chat session model. Migration target for OpenClaw users. Multi-bot, multi-chat, per-topic isolation; SQLite transcripts; inline-keyboard approvals. Bundles /polygram:status|logs|pair-code|approvals admin commands plus history (transcript queries) and polygram-send (out-of-turn IPC sends with file-upload validation) skills.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"telegram",
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
|
|
19
19
|
const MODEL_OPTIONS = ['opus', 'sonnet', 'haiku'];
|
|
20
20
|
const EFFORT_OPTIONS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
21
|
-
const CODEX_MODEL_OPTIONS =
|
|
22
|
-
'gpt-5.6-sol',
|
|
23
|
-
'gpt-5.6-terra',
|
|
24
|
-
'gpt-5.6-luna',
|
|
21
|
+
const CODEX_MODEL_OPTIONS = Object.freeze([
|
|
22
|
+
Object.freeze({ model: 'gpt-5.6-sol', label: 'Sol' }),
|
|
23
|
+
Object.freeze({ model: 'gpt-5.6-terra', label: 'Terra' }),
|
|
24
|
+
Object.freeze({ model: 'gpt-5.6-luna', label: 'Luna' }),
|
|
25
25
|
]);
|
|
26
26
|
// Which runtimes the card offers. The sdk backend stays fully supported in
|
|
27
27
|
// code and config files — it is per-token and nobody picks it from a chat, so
|
|
@@ -63,20 +63,30 @@ function getCodexCatalogModels(runtimeView) {
|
|
|
63
63
|
if (!isCodexRuntimeView(runtimeView) || !Array.isArray(runtimeView.models)) {
|
|
64
64
|
return [];
|
|
65
65
|
}
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
!entry
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|| !CODEX_MODEL_OPTIONS.has(entry.model)
|
|
73
|
-
|| seen.has(entry.model)
|
|
74
|
-
) {
|
|
75
|
-
return false;
|
|
66
|
+
const catalogByModel = new Map();
|
|
67
|
+
for (const entry of runtimeView.models) {
|
|
68
|
+
if (entry && typeof entry.model === 'string' && entry.model.length > 0) {
|
|
69
|
+
if (!catalogByModel.has(entry.model)) {
|
|
70
|
+
catalogByModel.set(entry.model, entry);
|
|
71
|
+
}
|
|
76
72
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
}
|
|
74
|
+
return CODEX_MODEL_OPTIONS.flatMap(({ model }) => (
|
|
75
|
+
catalogByModel.has(model) ? [catalogByModel.get(model)] : []
|
|
76
|
+
));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function codexModelLabel(model) {
|
|
80
|
+
return CODEX_MODEL_OPTIONS.find((entry) => entry.model === model)?.label
|
|
81
|
+
?? model;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hasCodexCatalogModels(runtimeView) {
|
|
85
|
+
return isCodexRuntimeView(runtimeView)
|
|
86
|
+
&& Array.isArray(runtimeView.models)
|
|
87
|
+
&& runtimeView.models.some(
|
|
88
|
+
(entry) => typeof entry?.model === 'string' && entry.model.length > 0,
|
|
89
|
+
);
|
|
80
90
|
}
|
|
81
91
|
|
|
82
92
|
function getCodexCatalogEfforts(runtimeView, model = runtimeView?.model) {
|
|
@@ -262,10 +272,15 @@ function buildConfigKeyboard(
|
|
|
262
272
|
}
|
|
263
273
|
if ((show === 'model' || show === 'all') && modelOptions.length > 0) {
|
|
264
274
|
rows.push(modelOptions.map((entry) => {
|
|
265
|
-
|
|
275
|
+
let label = entry.model;
|
|
276
|
+
if (codex) {
|
|
277
|
+
label = codexModelLabel(entry.model);
|
|
278
|
+
} else if (
|
|
266
279
|
typeof entry.displayName === 'string'
|
|
267
280
|
&& entry.displayName.length > 0
|
|
268
|
-
)
|
|
281
|
+
) {
|
|
282
|
+
label = entry.displayName;
|
|
283
|
+
}
|
|
269
284
|
return {
|
|
270
285
|
text: entry.model === model ? `✓ ${label}` : label,
|
|
271
286
|
callback_data: `cfg:model:${entry.model}`,
|
|
@@ -325,33 +340,30 @@ function createFormatConfigInfoText({
|
|
|
325
340
|
? codexModels.length > 0 ? [
|
|
326
341
|
'',
|
|
327
342
|
'**Models**',
|
|
328
|
-
...codexModels.map(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
&& entry.displayName.length > 0
|
|
332
|
-
&& entry.displayName !== entry.model
|
|
333
|
-
) ? ` — ${entry.displayName}` : '';
|
|
334
|
-
return `• **${entry.model}**${displayName}`;
|
|
335
|
-
}),
|
|
343
|
+
...codexModels.map(
|
|
344
|
+
(entry) => `• **${codexModelLabel(entry.model)}** — ${entry.model}`,
|
|
345
|
+
),
|
|
336
346
|
].join('\n') : [
|
|
337
347
|
'',
|
|
338
348
|
'**Models**',
|
|
339
|
-
|
|
349
|
+
hasCodexCatalogModels(runtimeView)
|
|
350
|
+
? 'Sol, Terra, and Luna are unavailable for this account.'
|
|
351
|
+
: 'Models are unavailable until Codex preflight succeeds.',
|
|
340
352
|
].join('\n')
|
|
341
353
|
: [
|
|
342
354
|
'',
|
|
343
355
|
'**Models**',
|
|
344
|
-
'🧠 **opus** —
|
|
345
|
-
'🤖 **sonnet** — default
|
|
346
|
-
'⚡ **haiku** —
|
|
356
|
+
'🧠 **opus** — deepest reasoning',
|
|
357
|
+
'🤖 **sonnet** — balanced default',
|
|
358
|
+
'⚡ **haiku** — fastest',
|
|
347
359
|
].join('\n');
|
|
348
360
|
|
|
349
361
|
const effortHelp = codex
|
|
350
362
|
? codexEfforts.length > 0 ? [
|
|
351
363
|
'',
|
|
352
|
-
`**Effort
|
|
353
|
-
|
|
354
|
-
'
|
|
364
|
+
`**Effort for ${codexModelLabel(codexModel)}**`,
|
|
365
|
+
codexEfforts.map((value) => `**${value}**`).join(' · '),
|
|
366
|
+
'Changes apply on the next turn; an active turn is unchanged.',
|
|
355
367
|
].join('\n') : [
|
|
356
368
|
'',
|
|
357
369
|
'**Effort**',
|
|
@@ -359,11 +371,9 @@ function createFormatConfigInfoText({
|
|
|
359
371
|
].join('\n')
|
|
360
372
|
: [
|
|
361
373
|
'',
|
|
362
|
-
'**Effort**
|
|
363
|
-
'
|
|
364
|
-
'
|
|
365
|
-
'• **high** — multi-step tasks. Audit, debug, multi-source analysis.',
|
|
366
|
-
'• **xhigh** / **max** — heaviest. Hard reasoning, edge cases.',
|
|
374
|
+
'**Effort**',
|
|
375
|
+
'**low** · **medium** · **high** · **xhigh** · **max**',
|
|
376
|
+
'Higher values allow more reasoning when needed.',
|
|
367
377
|
].join('\n');
|
|
368
378
|
|
|
369
379
|
// The card is its buttons. Everything the body used to state — model,
|
|
@@ -373,11 +383,10 @@ function createFormatConfigInfoText({
|
|
|
373
383
|
// button to live on.
|
|
374
384
|
let body = '⚙️ Settings';
|
|
375
385
|
if (codex) {
|
|
376
|
-
body += '\n\
|
|
377
|
-
+ '
|
|
378
|
-
+ '
|
|
379
|
-
+ 'Native goals are disabled
|
|
380
|
-
+ 'native goal support is implemented.';
|
|
386
|
+
body += '\n\n_Codex beta: no command network, web search, product MCP tools, '
|
|
387
|
+
+ 'or interactive approvals. Detached/background servers are unsupported '
|
|
388
|
+
+ 'and may survive hard runtime loss. '
|
|
389
|
+
+ 'Native goals are currently disabled._';
|
|
381
390
|
}
|
|
382
391
|
if (show === 'model' || show === 'all') body += '\n' + modelHelp;
|
|
383
392
|
if (show === 'effort' || show === 'all') body += '\n' + effortHelp;
|
|
@@ -90,6 +90,7 @@ const richSection = (inlineMedia) => [
|
|
|
90
90
|
'- A numbered sequence (steps, ranked facts, top-N) → a real ordered list (`1.` at line start, continuation lines indented under their item), not bold pseudo-items like `**1. First thing.**` on their own paragraphs. A real list renders with native numbering and hanging indentation; the bold fake renders as flat paragraphs that merely start with a digit.',
|
|
91
91
|
'- A list of items each with a yes/no/pending state (approve this? confirm that?) → a real task list (`- [ ] item`), not `• item` with an emoji glued on.',
|
|
92
92
|
'- A comparison or key/value summary → a real markdown table.',
|
|
93
|
+
'- Rows and columns you are laying out yourself → a real markdown table, never hand-aligned columns inside a ``` fence. A fence is not a table: its columns are just whitespace you typed, so the client cannot re-lay-out them to fit and on a narrow screen the rows break mid-row and the alignment collapses into noise. Keep fences for code, logs, and command output you are quoting verbatim — including output that is already table-shaped, which you reproduce exactly rather than retype as a markdown table.',
|
|
93
94
|
'- A quoted aside, caveat, or callout you want visually set apart → a real blockquote (`> like this`), not a bold prefix.',
|
|
94
95
|
'- A visual break between distinct sections of a long reply → a real divider (`---` on its own line), the same way you already reach for it, but now it renders as a divider instead of literal dashes.',
|
|
95
96
|
'- Verbose supporting detail (a diff, a log, raw output) you want available but not front-and-center → `<details><summary>Label</summary>...</details>`.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"description": "Telegram daemon for Claude Code that preserves the OpenClaw per-chat session model. Migration path for OpenClaw users moving to Claude Code.",
|
|
5
5
|
"main": "lib/ipc/client.js",
|
|
6
6
|
"bin": {
|
package/polygram.js
CHANGED
|
@@ -482,6 +482,13 @@ let richEditMessageText;
|
|
|
482
482
|
// classification, but it never throws: its caller holds the only copy of the
|
|
483
483
|
// reply and owns the plain fallback.
|
|
484
484
|
let richSendMessage;
|
|
485
|
+
// The rich half of the reply-tool delivery chain, hoisted so the interactive
|
|
486
|
+
// turn path can reuse the same instance rather than configure a second one.
|
|
487
|
+
// Two instances would be two capability latches, two styling verdicts and two
|
|
488
|
+
// sets of ceilings to keep in step; one is the only way they cannot drift.
|
|
489
|
+
// Null until main() wires it — callers fall back to plain, which is what a
|
|
490
|
+
// chat without rich gets anyway.
|
|
491
|
+
let makeRichDeliverText = null;
|
|
485
492
|
let richMediaFileIdCache;
|
|
486
493
|
|
|
487
494
|
// 0.15 secret redaction (agent-flagged path): the agent marks a secret it saw
|
|
@@ -1554,12 +1561,15 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1554
1561
|
// path — same contract for "not opted in", "capability known
|
|
1555
1562
|
// unsupported", and "content doesn't need it" (needsRichRendering
|
|
1556
1563
|
// inside toTelegramRichBlocks handles the last one).
|
|
1557
|
-
//
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1564
|
+
// This callback covers the streamed bubble only. A turn that ends with
|
|
1565
|
+
// text instead of streaming it renders rich too, through the reply tool's
|
|
1566
|
+
// own factory reused at the non-streamed branch below — one gate, one
|
|
1567
|
+
// sender, one latch, so the same markdown cannot format two ways
|
|
1568
|
+
// depending on how the answer arrived.
|
|
1569
|
+
// Still plain: the non-streaming send paths — autonomous-wakeup/
|
|
1560
1570
|
// auto-resume/autosteer via lib/telegram/process-agent-reply.js, cron/
|
|
1561
|
-
// IPC sends
|
|
1562
|
-
//
|
|
1571
|
+
// IPC sends. Those render plain even when richText is on and the content
|
|
1572
|
+
// qualifies.
|
|
1563
1573
|
// Re-read both controls for every flush so a config change or a
|
|
1564
1574
|
// capability result applies to the turn already in progress.
|
|
1565
1575
|
// Typed media resolution for rich replies. Same
|
|
@@ -2907,27 +2917,78 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2907
2917
|
console.error(`[${label}] sendSticker failed: ${err.message}`);
|
|
2908
2918
|
});
|
|
2909
2919
|
} else if (parsed.text) {
|
|
2910
|
-
//
|
|
2911
|
-
//
|
|
2912
|
-
//
|
|
2913
|
-
//
|
|
2914
|
-
//
|
|
2915
|
-
//
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2920
|
+
// A turn that ends with text instead of calling the reply tool lands
|
|
2921
|
+
// here. It is the same agent writing the same markdown as the reply
|
|
2922
|
+
// tool, so it renders through the same gate and the same sender —
|
|
2923
|
+
// otherwise a table is a table or a wall of pipes depending only on
|
|
2924
|
+
// which way the answer happened to arrive, which is not a distinction
|
|
2925
|
+
// the reader can see or act on.
|
|
2926
|
+
//
|
|
2927
|
+
// Text-only by construction: no allowedRoots, so media stays degraded
|
|
2928
|
+
// to caption text exactly as it was here before, and this path opens no
|
|
2929
|
+
// upload surface of its own.
|
|
2930
|
+
// Wrapped even though the factory is contracted not to throw: that
|
|
2931
|
+
// contract is convention, and its styling-retry ladder evaluates
|
|
2932
|
+
// blocksAreStyled/flattenStyledBlocks outside any try. A throw escaping
|
|
2933
|
+
// here would skip the plain arm and cost the whole reply, so this catch
|
|
2934
|
+
// is the difference between a flat answer and no answer.
|
|
2935
|
+
let richOut = null;
|
|
2936
|
+
try {
|
|
2937
|
+
richOut = makeRichDeliverText
|
|
2938
|
+
? await makeRichDeliverText({ chatId, threadId })({
|
|
2939
|
+
text: parsed.text,
|
|
2940
|
+
replyToMessageId: msg.message_id,
|
|
2941
|
+
// Its own source, because rich-message-sent carries none and the
|
|
2942
|
+
// streamed rich open reports as bot-reply-stream-open-rich.
|
|
2943
|
+
// Without a distinct tag nothing downstream can tell this branch
|
|
2944
|
+
// from the other sites writing the bare streamer tag.
|
|
2945
|
+
meta: { ...outMeta, source: 'bot-reply-final-rich' },
|
|
2946
|
+
})
|
|
2947
|
+
: null;
|
|
2948
|
+
} catch (richErr) {
|
|
2949
|
+
// Named apart from the turn's own `err` on purpose: this is the
|
|
2950
|
+
// rich attempt failing, not the turn, and the completion tail is
|
|
2951
|
+
// located in tests by scanning for the turn handler's catch.
|
|
2952
|
+
console.error(`[${label}] rich delivery threw, falling back to plain: ${richErr.message}`);
|
|
2953
|
+
richOut = null;
|
|
2954
|
+
}
|
|
2955
|
+
if (richOut?.handled) {
|
|
2956
|
+
// One bubble, no chunks, nothing failed — but still report, because
|
|
2957
|
+
// this is the same incomplete-delivery gate the plain arm feeds and
|
|
2958
|
+
// it must hear from whichever arm ran.
|
|
2959
|
+
mediaContext.recordTextFailures(richOut.failed.length);
|
|
2960
|
+
} else {
|
|
2961
|
+
// 0.7.0: use markdown-aware chunker + deliverReplies primitive.
|
|
2962
|
+
// The old chunkText was newline/byte-only; chunkMarkdownText also
|
|
2963
|
+
// respects code-fence boundaries (closes + reopens across chunks).
|
|
2964
|
+
// Image markdown from a rich-enabled chat degrades to caption text
|
|
2965
|
+
// here too — never a raw local path in the bubble.
|
|
2966
|
+
//
|
|
2967
|
+
// The `??` arm is load-bearing: a chat with rich off declines with
|
|
2968
|
+
// no body at all, and chunking `undefined` would drop the reply
|
|
2969
|
+
// entirely for most of the fleet. Both arms strip through the same
|
|
2970
|
+
// call, so the delivered text is byte-identical either way.
|
|
2971
|
+
const shortText = richOut?.text ?? (resolveRichTextEnabled(config, chatId, threadId)
|
|
2972
|
+
? stripMediaMarkdown(parsed.text)
|
|
2973
|
+
: parsed.text);
|
|
2974
|
+
const chunks = chunkMarkdownText(shortText, TG_CHUNK_BUDGET);
|
|
2975
|
+
const deliveryResult = await deliverReplies({
|
|
2976
|
+
bot,
|
|
2977
|
+
send: (b, method, params, m) => tg(b, method, params, m),
|
|
2978
|
+
chatId,
|
|
2979
|
+
threadId,
|
|
2980
|
+
chunks,
|
|
2981
|
+
replyToMessageId: msg.message_id,
|
|
2982
|
+
// Tagged like its rich sibling. Five call sites write the bare
|
|
2983
|
+
// 'bot-reply-stream' tag and three of them deliver reply bodies,
|
|
2984
|
+
// so a row carrying it says nothing about which one produced it.
|
|
2985
|
+
// Naming both arms makes this branch identifiable from the
|
|
2986
|
+
// messages table whichever way the reply went out.
|
|
2987
|
+
meta: { ...outMeta, source: 'bot-reply-final' },
|
|
2988
|
+
logger: { error: (m) => console.error(`[${label}] ${m}`) },
|
|
2989
|
+
});
|
|
2990
|
+
mediaContext.recordTextFailures(deliveryResult.failed.length);
|
|
2991
|
+
}
|
|
2931
2992
|
}
|
|
2932
2993
|
|
|
2933
2994
|
// A turn whose final output was a solo sticker/reaction skips the
|
|
@@ -4062,6 +4123,38 @@ async function main() {
|
|
|
4062
4123
|
//
|
|
4063
4124
|
// Neither feature may switch the other off, which a single strategy slot
|
|
4064
4125
|
// could not express.
|
|
4126
|
+
// Built before the chain rather than inline in it, because the interactive
|
|
4127
|
+
// turn path reuses this exact instance for its non-streamed replies. See
|
|
4128
|
+
// the makeRichDeliverText declaration for why it must be one instance.
|
|
4129
|
+
// Injected rather than imported by the dispatcher: rich-media.js requires
|
|
4130
|
+
// the dispatcher module, so the reverse direction would be a require cycle.
|
|
4131
|
+
makeRichDeliverText = createRichDeliveryFactory({
|
|
4132
|
+
bot,
|
|
4133
|
+
sendRich: (args) => richSendMessage(args),
|
|
4134
|
+
isRichTextEnabled: (chatId, threadId) => resolveRichTextEnabled(config, chatId, threadId),
|
|
4135
|
+
getRichKnownUnsupported: () => richSendKnownUnsupported,
|
|
4136
|
+
// No config knob: the automatic flatten-and-retry below IS the control,
|
|
4137
|
+
// and it needs no operator to notice anything. A knob would add a state
|
|
4138
|
+
// where styling is off while the server accepts it, which nothing in
|
|
4139
|
+
// the system can detect or correct.
|
|
4140
|
+
isInlineStylingEnabled: () => !richInlineStylingUnsupported,
|
|
4141
|
+
onStylingRejected: () => richStylingLatch?.recordStylingRejection(),
|
|
4142
|
+
onStylingAccepted: () => richStylingLatch?.recordHealthyOutcome(),
|
|
4143
|
+
redactError: redactBotToken,
|
|
4144
|
+
// Media for one reply. The envelope itself (no URL media, the `files:`
|
|
4145
|
+
// fan-out ceiling, the shared stat) lives in rich-media.js so it is
|
|
4146
|
+
// pinned by behavior rather than by this literal — nothing executes
|
|
4147
|
+
// this file. `allowedRoots` arrives per call from the dispatcher, which
|
|
4148
|
+
// computed it once and validates `files:` against that very array, so a
|
|
4149
|
+
// path the reply tool may upload is exactly a path image syntax may
|
|
4150
|
+
// upload. The interactive path passes none, so it renders text-only.
|
|
4151
|
+
makeMediaWiring: makeReplyMediaWiring({
|
|
4152
|
+
config,
|
|
4153
|
+
fileIdCache: richMediaFileIdCache,
|
|
4154
|
+
logEvent,
|
|
4155
|
+
botName: BOT_NAME,
|
|
4156
|
+
}),
|
|
4157
|
+
});
|
|
4065
4158
|
const makeDeliverText = composeDeliverTextFactories([
|
|
4066
4159
|
createDeliverTextFactory({
|
|
4067
4160
|
registry: streamerRegistry,
|
|
@@ -4079,35 +4172,7 @@ async function main() {
|
|
|
4079
4172
|
resolveRichTextEnabled(config, chatId, threadId) ? stripMediaMarkdown(text) : text
|
|
4080
4173
|
),
|
|
4081
4174
|
}),
|
|
4082
|
-
|
|
4083
|
-
// the dispatcher module, so the reverse direction would be a require cycle.
|
|
4084
|
-
createRichDeliveryFactory({
|
|
4085
|
-
bot,
|
|
4086
|
-
sendRich: (args) => richSendMessage(args),
|
|
4087
|
-
isRichTextEnabled: (chatId, threadId) => resolveRichTextEnabled(config, chatId, threadId),
|
|
4088
|
-
getRichKnownUnsupported: () => richSendKnownUnsupported,
|
|
4089
|
-
// No config knob: the automatic flatten-and-retry below IS the control,
|
|
4090
|
-
// and it needs no operator to notice anything. A knob would add a state
|
|
4091
|
-
// where styling is off while the server accepts it, which nothing in
|
|
4092
|
-
// the system can detect or correct.
|
|
4093
|
-
isInlineStylingEnabled: () => !richInlineStylingUnsupported,
|
|
4094
|
-
onStylingRejected: () => richStylingLatch?.recordStylingRejection(),
|
|
4095
|
-
onStylingAccepted: () => richStylingLatch?.recordHealthyOutcome(),
|
|
4096
|
-
redactError: redactBotToken,
|
|
4097
|
-
// Media for one reply. The envelope itself (no URL media, the `files:`
|
|
4098
|
-
// fan-out ceiling, the shared stat) lives in rich-media.js so it is
|
|
4099
|
-
// pinned by behavior rather than by this literal — nothing executes
|
|
4100
|
-
// this file. `allowedRoots` arrives per call from the dispatcher, which
|
|
4101
|
-
// computed it once and validates `files:` against that very array, so a
|
|
4102
|
-
// path the reply tool may upload is exactly a path image syntax may
|
|
4103
|
-
// upload.
|
|
4104
|
-
makeMediaWiring: makeReplyMediaWiring({
|
|
4105
|
-
config,
|
|
4106
|
-
fileIdCache: richMediaFileIdCache,
|
|
4107
|
-
logEvent,
|
|
4108
|
-
botName: BOT_NAME,
|
|
4109
|
-
}),
|
|
4110
|
-
}),
|
|
4175
|
+
makeRichDeliverText,
|
|
4111
4176
|
]);
|
|
4112
4177
|
|
|
4113
4178
|
// The edit tool's counterpart to the reply chain above. An agent that posts
|