polygram 0.34.1 → 0.35.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-callback.js +82 -11
- package/lib/handlers/config-ui.js +29 -179
- package/lib/session-key.js +5 -7
- package/package.json +2 -2
- package/polygram.js +17 -5
|
@@ -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.35.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",
|
|
@@ -49,7 +49,9 @@ const BLOCKED_RUNTIME_STATES = new Set([
|
|
|
49
49
|
]);
|
|
50
50
|
|
|
51
51
|
function runtimeLabel(runtime) {
|
|
52
|
-
|
|
52
|
+
// Only ever called for a runtime that has a button — the callback rejects
|
|
53
|
+
// anything else as an invalid runtime before this point.
|
|
54
|
+
return RUNTIME_OPTION_BY_BACKEND.get(runtime).label;
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
function runtimeSwitchBlocked(proc) {
|
|
@@ -485,10 +487,15 @@ function createHandleConfigCallback({
|
|
|
485
487
|
// (so Music ≠ General), a chat-level card the chat root. Resolving the
|
|
486
488
|
// thread BEFORE the already-set check so "Already X" compares against the
|
|
487
489
|
// topic's effective value, not the chat root (2026-06-12 bug).
|
|
490
|
+
//
|
|
491
|
+
// Rich text follows session isolation like every other setting. Delivery
|
|
492
|
+
// resolves it per message with the real thread, but the agent's authoring
|
|
493
|
+
// hint is spawn-time state resolved with the SESSION's thread — which a
|
|
494
|
+
// chat without isolateTopics collapses to null. A topic-level write there
|
|
495
|
+
// would flip delivery while the hint never drifts: no respawn, and an ack
|
|
496
|
+
// promising a change that never arrives.
|
|
488
497
|
const { scope: writeScope, threadId: writeThreadId } =
|
|
489
|
-
getConfigWriteScope(chatConfig, callbackThreadIdEarly
|
|
490
|
-
allowNonIsolatedTopic: setting === 'richtext',
|
|
491
|
-
});
|
|
498
|
+
getConfigWriteScope(chatConfig, callbackThreadIdEarly);
|
|
492
499
|
// The persisted camel-case key differs from the lowercase callback
|
|
493
500
|
// label used alongside model and effort.
|
|
494
501
|
const configField = isRichText
|
|
@@ -543,13 +550,78 @@ function createHandleConfigCallback({
|
|
|
543
550
|
effortAdjustment = { oldValue: oldEffort, newValue: nextEffort };
|
|
544
551
|
}
|
|
545
552
|
}
|
|
553
|
+
// Rich text has no per-topic scope in a chat whose topics share one
|
|
554
|
+
// session: there is a single hint, so there can be a single value. Any
|
|
555
|
+
// topics[tid].richText there was written by the pre-fix behavior and can
|
|
556
|
+
// never be honored by the agent — but delivery resolves topic-first, so
|
|
557
|
+
// it still shadows the chat value in its own topic. Clearing every one of
|
|
558
|
+
// them is part of writing at chat level, not a separate cleanup.
|
|
559
|
+
const clearStaleTopicRichText = () => {
|
|
560
|
+
if (!isRichText || chatConfig?.isolateTopics === true) return [];
|
|
561
|
+
const cleared = [];
|
|
562
|
+
for (const [tid, topic] of Object.entries(chatConfig.topics || {})) {
|
|
563
|
+
if (
|
|
564
|
+
topic
|
|
565
|
+
&& typeof topic === 'object'
|
|
566
|
+
&& Object.hasOwn(topic, configField)
|
|
567
|
+
) {
|
|
568
|
+
cleared.push({ threadId: String(tid), scope: topic, value: topic[configField] });
|
|
569
|
+
delete topic[configField];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return cleared;
|
|
573
|
+
};
|
|
574
|
+
const restoreTopicRichText = (cleared) => {
|
|
575
|
+
for (const { scope, value } of cleared) scope[configField] = value;
|
|
576
|
+
};
|
|
577
|
+
// The user tapped one card; topics they never touched can change value
|
|
578
|
+
// with it. Record which ones and what they held, so a config that moved
|
|
579
|
+
// on its own is still explicable afterwards. Logged only once the sweep
|
|
580
|
+
// is persisted — an event for a rolled-back sweep is worse than none.
|
|
581
|
+
const logTopicRichTextSweep = (cleared) => {
|
|
582
|
+
if (!cleared.length) return;
|
|
583
|
+
dbWrite(() => {
|
|
584
|
+
db.logEvent('config-topic-override-swept', {
|
|
585
|
+
chat_id: chatId,
|
|
586
|
+
field: configField,
|
|
587
|
+
topics: cleared.map(({ threadId, value }) => ({
|
|
588
|
+
thread_id: threadId,
|
|
589
|
+
old_value: value,
|
|
590
|
+
})),
|
|
591
|
+
});
|
|
592
|
+
}, 'log topic-override sweep');
|
|
593
|
+
};
|
|
594
|
+
|
|
546
595
|
if (oldValue === parsedValue) {
|
|
596
|
+
// Nothing to write — but a stale override is exactly why this tap can
|
|
597
|
+
// be a no-op, and leaving it keeps delivery and the session's hint
|
|
598
|
+
// disagreeing. Normalize the scope and keep the value the ack states:
|
|
599
|
+
// the chat level is the only place the shared session reads.
|
|
600
|
+
const cleared = clearStaleTopicRichText();
|
|
601
|
+
if (cleared.length) {
|
|
602
|
+
const hadChatValue = Object.hasOwn(chatConfig, configField);
|
|
603
|
+
const previousChatValue = chatConfig[configField];
|
|
604
|
+
chatConfig[configField] = parsedValue;
|
|
605
|
+
try {
|
|
606
|
+
await saveConfig();
|
|
607
|
+
logTopicRichTextSweep(cleared);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
restoreTopicRichText(cleared);
|
|
610
|
+
if (hadChatValue) chatConfig[configField] = previousChatValue;
|
|
611
|
+
else delete chatConfig[configField];
|
|
612
|
+
logger.error?.(
|
|
613
|
+
`[${botName}] config-callback scope normalization failed: `
|
|
614
|
+
+ error.message,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
547
618
|
releaseConfigIntent();
|
|
548
619
|
await ctx.answerCallbackQuery({ text: `Already ${value}` })
|
|
549
620
|
.catch(() => {});
|
|
550
621
|
return;
|
|
551
622
|
}
|
|
552
623
|
|
|
624
|
+
const clearedTopicRichText = clearStaleTopicRichText();
|
|
553
625
|
writeScope[configField] = parsedValue;
|
|
554
626
|
if (effortAdjustment) {
|
|
555
627
|
writeScope.codexEffort = effortAdjustment.newValue;
|
|
@@ -561,6 +633,7 @@ function createHandleConfigCallback({
|
|
|
561
633
|
if (snapshot.present) writeScope[field] = snapshot.value;
|
|
562
634
|
else delete writeScope[field];
|
|
563
635
|
}
|
|
636
|
+
restoreTopicRichText(clearedTopicRichText);
|
|
564
637
|
logger.error?.(
|
|
565
638
|
`[${botName}] config-callback saveConfig failed: ${error.message}`,
|
|
566
639
|
);
|
|
@@ -571,6 +644,7 @@ function createHandleConfigCallback({
|
|
|
571
644
|
}).catch(() => {});
|
|
572
645
|
return;
|
|
573
646
|
}
|
|
647
|
+
logTopicRichTextSweep(clearedTopicRichText);
|
|
574
648
|
const cmdUserId = ctx.callbackQuery.from?.id || null;
|
|
575
649
|
const cmdUser = ctx.callbackQuery.from?.first_name
|
|
576
650
|
|| ctx.callbackQuery.from?.username
|
|
@@ -707,14 +781,11 @@ function createHandleConfigCallback({
|
|
|
707
781
|
logger.error?.(`[${botName}] config-card edit failed: ${err.message}`);
|
|
708
782
|
}
|
|
709
783
|
|
|
710
|
-
//
|
|
711
|
-
//
|
|
784
|
+
// Delivery changes immediately and the authoring hint follows on the
|
|
785
|
+
// user's next message: a hint drift reloads the session (conversation
|
|
786
|
+
// preserved), so there is no lag left worth a caveat.
|
|
712
787
|
const ackText = isRichText
|
|
713
|
-
?
|
|
714
|
-
parsedValue
|
|
715
|
-
? 'Rich text → on (delivery live; agent authors for it next session)'
|
|
716
|
-
: 'Rich text → off'
|
|
717
|
-
)
|
|
788
|
+
? `Rich text → ${parsedValue ? 'on' : 'off'}`
|
|
718
789
|
: codex
|
|
719
790
|
? (
|
|
720
791
|
`${setting} → ${value}`
|
|
@@ -4,23 +4,25 @@
|
|
|
4
4
|
* the runtime/model/effort callback re-render paths
|
|
5
5
|
* (lib/handlers/config-callback.js).
|
|
6
6
|
*
|
|
7
|
-
* Pure functions (no DB / fs)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* top-level export.
|
|
7
|
+
* Pure functions (no DB / fs). `formatConfigInfoText` is factory-wrapped
|
|
8
|
+
* only so a caller can supply the async runtime-view resolver; the
|
|
9
|
+
* keyboard builder is a top-level export.
|
|
11
10
|
*
|
|
12
11
|
* MODEL_VERSIONS_DESC bumps with each Claude release — see polygram's
|
|
13
12
|
* release notes for the verification step (`claude --model <alias>`
|
|
14
|
-
* + check the system:init event's `model` field).
|
|
13
|
+
* + check the system:init event's `model` field). Surfaced by the
|
|
14
|
+
* /model reply, not by the card.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
'use strict';
|
|
18
18
|
|
|
19
19
|
const MODEL_OPTIONS = ['opus', 'sonnet', 'haiku'];
|
|
20
20
|
const EFFORT_OPTIONS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
21
|
+
// Which runtimes the card offers. The sdk backend stays fully supported in
|
|
22
|
+
// code and config files — it is per-token and nobody picks it from a chat, so
|
|
23
|
+
// it has no button. A chat configured on it simply shows no ✓ on this row.
|
|
21
24
|
const RUNTIME_OPTIONS = Object.freeze([
|
|
22
|
-
Object.freeze({ backend: '
|
|
23
|
-
Object.freeze({ backend: 'cli', label: 'Claude CLI' }),
|
|
25
|
+
Object.freeze({ backend: 'cli', label: 'Claude' }),
|
|
24
26
|
Object.freeze({ backend: 'codex', label: 'Codex' }),
|
|
25
27
|
]);
|
|
26
28
|
|
|
@@ -44,22 +46,6 @@ function canonicalRuntimeBackend(runtimeView) {
|
|
|
44
46
|
return 'sdk';
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
function runtimeSelectionSource(runtimeView) {
|
|
48
|
-
const source = runtimeView?.selectionSource
|
|
49
|
-
?? runtimeView?.source
|
|
50
|
-
?? runtimeView?.sources?.pm;
|
|
51
|
-
return typeof source === 'string' && source.length > 0 ? source : null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function formatRuntimeLine(runtimeView) {
|
|
55
|
-
const backend = canonicalRuntimeBackend(runtimeView);
|
|
56
|
-
const label = backend === 'codex'
|
|
57
|
-
? 'Codex app-server'
|
|
58
|
-
: backend === 'cli' ? 'Claude CLI' : 'Claude SDK';
|
|
59
|
-
const source = runtimeSelectionSource(runtimeView);
|
|
60
|
-
return `Runtime: ${label}${source ? ` (source: ${source})` : ''}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
49
|
function uniqueStrings(values) {
|
|
64
50
|
if (!Array.isArray(values)) return [];
|
|
65
51
|
return [...new Set(values.filter(
|
|
@@ -118,13 +104,6 @@ function formatSettingsPair(settings) {
|
|
|
118
104
|
return `${settings.model}/${settings.effort}`;
|
|
119
105
|
}
|
|
120
106
|
|
|
121
|
-
function sameSettingsPair(left, right) {
|
|
122
|
-
return (
|
|
123
|
-
left?.model === right?.model
|
|
124
|
-
&& left?.effort === right?.effort
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
107
|
function formatCodexSettingsOutcome(result) {
|
|
129
108
|
const next = formatSettingsPair(result?.nextTurn);
|
|
130
109
|
switch (result?.outcome) {
|
|
@@ -301,18 +280,14 @@ function buildConfigKeyboard(
|
|
|
301
280
|
}
|
|
302
281
|
|
|
303
282
|
/**
|
|
304
|
-
* Factory for the card-body formatter.
|
|
305
|
-
* getClaudeSessionId fetcher.
|
|
283
|
+
* Factory for the card-body formatter.
|
|
306
284
|
*
|
|
307
|
-
* @param {object} deps
|
|
308
|
-
* @param {object} deps.
|
|
309
|
-
*
|
|
310
|
-
*
|
|
285
|
+
* @param {object} [deps]
|
|
286
|
+
* @param {(ctx: {sessionKey: string, chatId: string, threadId: ?string}) => object} [deps.resolveRuntimeView]
|
|
287
|
+
* — resolves the runtime view for callers that don't pass one; the view
|
|
288
|
+
* decides which runtime's help sections the card carries.
|
|
311
289
|
*/
|
|
312
290
|
function createFormatConfigInfoText({
|
|
313
|
-
pm,
|
|
314
|
-
db,
|
|
315
|
-
getClaudeSessionId,
|
|
316
291
|
resolveRuntimeView = null,
|
|
317
292
|
} = {}) {
|
|
318
293
|
function formatWithRuntimeView(
|
|
@@ -323,148 +298,18 @@ function createFormatConfigInfoText({
|
|
|
323
298
|
effectiveRichText,
|
|
324
299
|
runtimeView,
|
|
325
300
|
) {
|
|
326
|
-
const alive = pm.has(sessionKey) && !pm.get(sessionKey).closed;
|
|
327
301
|
const codex = isCodexRuntimeView(runtimeView);
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
// "Agent: shumabit" instead of its music-curation:music-curator override
|
|
333
|
-
// (2026-06-03). topicConfig defaults to null (chat-level) for callers with
|
|
334
|
-
// no active topic.
|
|
335
|
-
const model = configuredRuntimeValue(
|
|
336
|
-
chatConfig,
|
|
337
|
-
topicConfig,
|
|
338
|
-
runtimeView,
|
|
339
|
-
'model',
|
|
340
|
-
);
|
|
341
|
-
const effort = configuredRuntimeValue(
|
|
342
|
-
chatConfig,
|
|
343
|
-
topicConfig,
|
|
344
|
-
runtimeView,
|
|
345
|
-
'effort',
|
|
346
|
-
);
|
|
302
|
+
// The selected Codex model decides which reasoning efforts its help section
|
|
303
|
+
// may list — per-topic overrides win over chat-level, mirroring the spawn
|
|
304
|
+
// path. Nothing else about the configuration is read here: the buttons under
|
|
305
|
+
// the card carry it, each with its own ✓.
|
|
347
306
|
const codexModels = codex ? getCodexCatalogModels(runtimeView) : [];
|
|
307
|
+
const codexModel = codex
|
|
308
|
+
? configuredRuntimeValue(chatConfig, topicConfig, runtimeView, 'model')
|
|
309
|
+
: null;
|
|
348
310
|
const codexEfforts = codex
|
|
349
|
-
? getCodexCatalogEfforts(runtimeView,
|
|
311
|
+
? getCodexCatalogEfforts(runtimeView, codexModel)
|
|
350
312
|
: [];
|
|
351
|
-
const agent = (topicConfig && topicConfig.agent) || chatConfig.agent;
|
|
352
|
-
const codexModelEntry = codex
|
|
353
|
-
? codexModels
|
|
354
|
-
.find((entry) => entry.model === model)
|
|
355
|
-
: null;
|
|
356
|
-
const codexDisplayName = codexModelEntry?.displayName;
|
|
357
|
-
const ver = codex
|
|
358
|
-
? (
|
|
359
|
-
typeof codexDisplayName === 'string'
|
|
360
|
-
&& codexDisplayName.length > 0
|
|
361
|
-
&& codexDisplayName !== model
|
|
362
|
-
? codexDisplayName
|
|
363
|
-
: model
|
|
364
|
-
)
|
|
365
|
-
: MODEL_VERSIONS_DESC[model] || model;
|
|
366
|
-
const sess = codex
|
|
367
|
-
? 'managed by Codex'
|
|
368
|
-
: getClaudeSessionId(db, sessionKey)?.slice(0, 8) || 'new';
|
|
369
|
-
// Running vs configured: cli can't hot-swap model/effort, so a /model or
|
|
370
|
-
// /effort change is PENDING until the session reloads (on the next message).
|
|
371
|
-
// Show the truth — the live proc's spawn-time value (proc.model/proc.effort)
|
|
372
|
-
// vs the configured one — so the card never claims a model the session
|
|
373
|
-
// isn't actually running (the "says opus, runs sonnet" confusion). SDK
|
|
374
|
-
// applies live (its proc value tracks config) so no drift line ever shows.
|
|
375
|
-
const proc = alive ? pm.get(sessionKey) : null;
|
|
376
|
-
const runModel = proc && proc.model;
|
|
377
|
-
const runEffort = proc && proc.effort;
|
|
378
|
-
const modelLine = (runModel && runModel !== model)
|
|
379
|
-
? `Model: ${runModel} (running) → ${model} (pending — applies on your next message)`
|
|
380
|
-
: `Model: ${model} (${ver})`;
|
|
381
|
-
const effortLine = (runEffort && runEffort !== effort)
|
|
382
|
-
? `Effort: ${runEffort} (running) → ${effort} (pending — applies on your next message)`
|
|
383
|
-
: `Effort: ${effort}`;
|
|
384
|
-
// Delivery reads richText live, while the authoring hint is fixed at
|
|
385
|
-
// session spawn. Explain that lag separately from model/effort,
|
|
386
|
-
// which have concrete running and configured values to compare.
|
|
387
|
-
const richText = resolveDisplayRichText(chatConfig, topicConfig, effectiveRichText);
|
|
388
|
-
const richTextLine = richText
|
|
389
|
-
? 'Rich text: on (headings/tables/checklists on qualifying replies; agent authors for it once this session next (re)spawns)'
|
|
390
|
-
: 'Rich text: off';
|
|
391
|
-
const runtimeLine = formatRuntimeLine(runtimeView);
|
|
392
|
-
let head;
|
|
393
|
-
if (codex) {
|
|
394
|
-
const desired = runtimeView.desiredSettings ?? { model, effort };
|
|
395
|
-
const desiredPair = formatSettingsPair(desired)
|
|
396
|
-
?? 'incomplete Codex selection';
|
|
397
|
-
const activePair = formatSettingsPair(runtimeView.activeTurnSettings);
|
|
398
|
-
const nextTurn = runtimeView.nextTurnSettings;
|
|
399
|
-
const nextTurnPair = formatSettingsPair(nextTurn);
|
|
400
|
-
const observedPair = formatSettingsPair(
|
|
401
|
-
runtimeView.observedThreadSettings,
|
|
402
|
-
);
|
|
403
|
-
const processStatus = runtimeView.processStatus
|
|
404
|
-
?? (alive ? 'loaded' : 'not-loaded');
|
|
405
|
-
const settingsLines = [];
|
|
406
|
-
if (processStatus === 'loaded') {
|
|
407
|
-
if (activePair) {
|
|
408
|
-
settingsLines.push(`Current turn: ${activePair}`);
|
|
409
|
-
settingsLines.push(`Next turn: ${nextTurnPair ?? desiredPair}`);
|
|
410
|
-
} else {
|
|
411
|
-
settingsLines.push(
|
|
412
|
-
`Selected for next turn: ${nextTurnPair ?? desiredPair}`,
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
if (
|
|
416
|
-
nextTurnPair
|
|
417
|
-
&& !sameSettingsPair(nextTurn, desired)
|
|
418
|
-
) {
|
|
419
|
-
settingsLines.push(
|
|
420
|
-
`Saved selection awaiting live reconciliation: ${desiredPair}`,
|
|
421
|
-
);
|
|
422
|
-
}
|
|
423
|
-
} else if (processStatus === 'not-loaded') {
|
|
424
|
-
settingsLines.push(
|
|
425
|
-
`Selected for this chat's next session: ${desiredPair}`,
|
|
426
|
-
);
|
|
427
|
-
} else {
|
|
428
|
-
settingsLines.push(`Selected: ${desiredPair}`);
|
|
429
|
-
}
|
|
430
|
-
if (observedPair) {
|
|
431
|
-
settingsLines.push(`Observed thread: ${observedPair}`);
|
|
432
|
-
}
|
|
433
|
-
let processLine;
|
|
434
|
-
if (processStatus === 'daemon-busy') {
|
|
435
|
-
processLine = 'Process: not loaded; its next message may be busy';
|
|
436
|
-
} else if (processStatus === 'unavailable') {
|
|
437
|
-
processLine = 'Process: unavailable'
|
|
438
|
-
+ (
|
|
439
|
-
runtimeView.unavailableReason
|
|
440
|
-
? ` (${runtimeView.unavailableReason})`
|
|
441
|
-
: ''
|
|
442
|
-
);
|
|
443
|
-
} else if (processStatus === 'unknown') {
|
|
444
|
-
processLine = 'Process: live status unknown';
|
|
445
|
-
} else {
|
|
446
|
-
processLine = `Process: ${
|
|
447
|
-
processStatus === 'loaded' ? 'warm' : 'not loaded'
|
|
448
|
-
}`;
|
|
449
|
-
}
|
|
450
|
-
head = [
|
|
451
|
-
...settingsLines,
|
|
452
|
-
richTextLine,
|
|
453
|
-
runtimeLine,
|
|
454
|
-
processLine,
|
|
455
|
-
`Session: ${sess}`,
|
|
456
|
-
].join('\n');
|
|
457
|
-
} else {
|
|
458
|
-
head =
|
|
459
|
-
`${modelLine}\n` +
|
|
460
|
-
`${effortLine}\n` +
|
|
461
|
-
`${richTextLine}\n` +
|
|
462
|
-
`${runtimeLine}\n` +
|
|
463
|
-
`Agent: ${agent}\n` +
|
|
464
|
-
`Process: ${alive ? 'warm' : 'cold'}\n` +
|
|
465
|
-
`Session: ${sess}`;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
313
|
const modelHelp = codex
|
|
469
314
|
? codexModels.length > 0 ? [
|
|
470
315
|
'',
|
|
@@ -493,7 +338,7 @@ function createFormatConfigInfoText({
|
|
|
493
338
|
const effortHelp = codex
|
|
494
339
|
? codexEfforts.length > 0 ? [
|
|
495
340
|
'',
|
|
496
|
-
`**Effort** — authenticated options for ${
|
|
341
|
+
`**Effort** — authenticated options for ${codexModel}:`,
|
|
497
342
|
...codexEfforts.map((value) => `• **${value}**`),
|
|
498
343
|
'Model and effort changes apply to the next turn; an active turn is unchanged.',
|
|
499
344
|
].join('\n') : [
|
|
@@ -510,7 +355,12 @@ function createFormatConfigInfoText({
|
|
|
510
355
|
'• **xhigh** / **max** — heaviest. Hard reasoning, edge cases.',
|
|
511
356
|
].join('\n');
|
|
512
357
|
|
|
513
|
-
|
|
358
|
+
// The card is its buttons. Everything the body used to state — model,
|
|
359
|
+
// effort, rich text, runtime, agent, process, session — is already on a
|
|
360
|
+
// button below it, with a ✓ on the current value. What is left is the
|
|
361
|
+
// header (Telegram rejects an empty message) and the help that has no
|
|
362
|
+
// button to live on.
|
|
363
|
+
let body = '⚙️ Settings';
|
|
514
364
|
if (codex) {
|
|
515
365
|
body += '\n\nNative Codex beta: command network and web search are disabled. '
|
|
516
366
|
+ 'Product MCP tools and interactive approvals are unavailable. '
|
package/lib/session-key.js
CHANGED
|
@@ -107,17 +107,15 @@ function getTopicConfig(chatConfig, threadId) {
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
|
-
* Resolve the config object a per-chat/topic setting should be written to
|
|
111
|
-
*
|
|
112
|
-
*
|
|
110
|
+
* Resolve the config object a per-chat/topic setting should be written to:
|
|
111
|
+
* topic scope only where the session itself is topic-scoped, so a setting can
|
|
112
|
+
* never be stored somewhere the session that must honor it cannot see.
|
|
113
113
|
*
|
|
114
114
|
* @returns {{ scope: object, threadId: (string|null) }}
|
|
115
115
|
*/
|
|
116
|
-
function getConfigWriteScope(chatConfig, threadId
|
|
116
|
+
function getConfigWriteScope(chatConfig, threadId) {
|
|
117
117
|
const tid = (threadId == null || threadId === '') ? null : String(threadId);
|
|
118
|
-
|
|
119
|
-
// may explicitly keep topic scope because they are resolved per message.
|
|
120
|
-
if (tid && (chatConfig?.isolateTopics === true || allowNonIsolatedTopic)) {
|
|
118
|
+
if (tid && chatConfig?.isolateTopics === true) {
|
|
121
119
|
chatConfig.topics = chatConfig.topics || {};
|
|
122
120
|
// A topic can be stored in the legacy / hand-edited form where the entry
|
|
123
121
|
// is the bare name string (topics["329"] = "Advertising") rather than the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.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": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"dependencies": {
|
|
64
64
|
"@anthropic-ai/claude-agent-sdk": "0.2.123",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
66
|
-
"@shumkov/orchestra": "0.10.
|
|
66
|
+
"@shumkov/orchestra": "0.10.11",
|
|
67
67
|
"better-sqlite3": "^12.9.0",
|
|
68
68
|
"grammy": "^1.42.0",
|
|
69
69
|
"markdown-it": "^14.1.1",
|
package/polygram.js
CHANGED
|
@@ -896,6 +896,16 @@ async function buildSpawnContext(
|
|
|
896
896
|
// the inbound filter and the send() choke point use, so CliProcess's
|
|
897
897
|
// pre-check + system-prompt line can't drift from actual enforcement.
|
|
898
898
|
localApi: !!config.bot?.apiRoot,
|
|
899
|
+
// The display hint this chat should be running under, resolved fresh on
|
|
900
|
+
// every context build. It is spawn-time state (woven into the appended
|
|
901
|
+
// system prompt), so a warm session only picks up a rich-text toggle if the
|
|
902
|
+
// context carries the new hint for getOrSpawn to notice the drift against.
|
|
903
|
+
// MUST be built exactly like the factory-side resolver below — a difference
|
|
904
|
+
// in either argument is permanent drift, i.e. a respawn per message.
|
|
905
|
+
displayHint: buildPolygramDisplayHint(
|
|
906
|
+
resolveRichTextEnabled(config, chatId, threadId || null),
|
|
907
|
+
{ inlineMedia: true },
|
|
908
|
+
),
|
|
899
909
|
// Pre-resolve through the SAME backend-aware resolver origin's CliProcess used, so
|
|
900
910
|
// orchestra (which now uses opts.outboundCapOverride verbatim, without its own
|
|
901
911
|
// resolveFileCaps clamp) gets the backend default (2GB local / 50MB cloud) AND the
|
|
@@ -1113,9 +1123,9 @@ let handleSendOverIpc = null;
|
|
|
1113
1123
|
// formatToolInputForCard).
|
|
1114
1124
|
|
|
1115
1125
|
// Config card UI moved to lib/handlers/config-ui.js. polygram.js
|
|
1116
|
-
// keeps a thin formatConfigInfoText wrapper since it
|
|
1117
|
-
// runtime
|
|
1118
|
-
//
|
|
1126
|
+
// keeps a thin formatConfigInfoText wrapper since it supplies the
|
|
1127
|
+
// async runtime-view resolver; buildConfigKeyboard is pure and
|
|
1128
|
+
// re-exported.
|
|
1119
1129
|
const {
|
|
1120
1130
|
buildConfigKeyboard,
|
|
1121
1131
|
createFormatConfigInfoText,
|
|
@@ -4188,7 +4198,6 @@ async function main() {
|
|
|
4188
4198
|
// call time and captures the value (closure-by-value). v4 reviewer
|
|
4189
4199
|
// caught this as the same class as the v3 BLOCKER.
|
|
4190
4200
|
formatConfigInfoText = createFormatConfigInfoText({
|
|
4191
|
-
pm, db, getClaudeSessionId,
|
|
4192
4201
|
resolveRuntimeView: resolveSessionRuntimeView,
|
|
4193
4202
|
});
|
|
4194
4203
|
handleConfigCallback = createHandleConfigCallback({
|
|
@@ -5037,7 +5046,10 @@ async function main() {
|
|
|
5037
5046
|
// (rc.65+) AND a session_id to resume into.
|
|
5038
5047
|
if (o.text && savedSessionId) {
|
|
5039
5048
|
try {
|
|
5040
|
-
const entry = await pm.getOrSpawn(
|
|
5049
|
+
const entry = await pm.getOrSpawn(
|
|
5050
|
+
o.session_key,
|
|
5051
|
+
await buildSpawnContext(o.session_key),
|
|
5052
|
+
);
|
|
5041
5053
|
// 0.10.0 P0.4: route through Process.fireUserMessage so both
|
|
5042
5054
|
// SDK and tmux backends work. Pre-0.10.0-P0.4 reached into
|
|
5043
5055
|
// entry.inputController.push directly — broken on tmux.
|