polygram 0.8.0-rc.46 → 0.8.0-rc.48
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/config.example.json +9 -1
- package/lib/agent-loader.js +32 -6
- package/lib/process-manager-sdk.js +29 -0
- package/lib/session-key.js +85 -1
- package/package.json +1 -1
- package/polygram.js +117 -18
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.8.0-rc.
|
|
4
|
+
"version": "0.8.0-rc.48",
|
|
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 and a history skill.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"telegram",
|
package/config.example.json
CHANGED
|
@@ -98,9 +98,17 @@
|
|
|
98
98
|
"cwd": "/Users/you/admin-agent",
|
|
99
99
|
"requireMention": true,
|
|
100
100
|
"isolateTopics": true,
|
|
101
|
+
"_comment_topics": "rc.48: each topic entry is EITHER a string (legacy: just a label) OR an object with optional fields {name, agent, cwd, model, effort, permissionMode}. Object form lets a topic override chat-level config. Per-topic permissionMode overrides chat-level — typical use: scope one topic to permissionMode:'default' (so settings.json gates apply) while the rest of the chat stays on bypassPermissions. Object form requires isolateTopics: true (each topic gets its own SDK Query); polygram emits a startup warning otherwise.",
|
|
101
102
|
"topics": {
|
|
102
103
|
"100": "Customer A",
|
|
103
|
-
"200":
|
|
104
|
+
"200": {
|
|
105
|
+
"name": "Customer B",
|
|
106
|
+
"agent": "customer-b-helper",
|
|
107
|
+
"cwd": "/Users/you/customer-b-projects",
|
|
108
|
+
"model": "opus",
|
|
109
|
+
"effort": "high",
|
|
110
|
+
"permissionMode": "default"
|
|
111
|
+
}
|
|
104
112
|
}
|
|
105
113
|
},
|
|
106
114
|
|
package/lib/agent-loader.js
CHANGED
|
@@ -283,15 +283,27 @@ function loadAgent(agentName, { homeDir = process.env.HOME, cwd = null, logger =
|
|
|
283
283
|
|
|
284
284
|
/**
|
|
285
285
|
* Compose a chat's final SdkOptions from defaults + agent + per-chat
|
|
286
|
-
* overrides. Precedence
|
|
286
|
+
* overrides + per-topic overrides. Precedence (highest to lowest):
|
|
287
|
+
* topicConfig > chatConfig > agent.raw > defaults
|
|
288
|
+
*
|
|
289
|
+
* rc.48 added the per-topic layer (`topicConfig` arg). Per-topic
|
|
290
|
+
* overrides are the principal rc.48 use case — typically loosening
|
|
291
|
+
* an agent's `bypassPermissions` default to `default` for a sensitive
|
|
292
|
+
* topic (so canUseTool prompts fire there) while keeping the rest of
|
|
293
|
+
* the chat in bypass mode. Per-topic permissionMode MUST override the
|
|
294
|
+
* chat-level one for this to work.
|
|
287
295
|
*
|
|
288
296
|
* @param {object} chatConfig — config.chats[chatId].
|
|
289
297
|
* @param {AgentBundle|null} agentBundle — null if chat has no agent.
|
|
290
298
|
* @param {object} defaults — config.defaults.
|
|
299
|
+
* @param {object} [topicConfig] — per-topic overrides from
|
|
300
|
+
* getTopicConfig(chatConfig, threadId). Empty object when there's
|
|
301
|
+
* no active topic, no override config, or topic uses legacy string
|
|
302
|
+
* form. Highest precedence — overrides chatConfig.
|
|
291
303
|
*
|
|
292
304
|
* @returns {object} SdkOptions for `query({ options: ... })`.
|
|
293
305
|
*/
|
|
294
|
-
function composeSdkOptions(chatConfig = {}, agentBundle = null, defaults = {}) {
|
|
306
|
+
function composeSdkOptions(chatConfig = {}, agentBundle = null, defaults = {}, topicConfig = {}) {
|
|
295
307
|
// Start with defaults — these are the lowest-priority.
|
|
296
308
|
const opts = { ...defaults };
|
|
297
309
|
|
|
@@ -302,16 +314,18 @@ function composeSdkOptions(chatConfig = {}, agentBundle = null, defaults = {}) {
|
|
|
302
314
|
if (agentBundle.mcpServers && Object.keys(agentBundle.mcpServers).length) {
|
|
303
315
|
opts.mcpServers = { ...(opts.mcpServers || {}), ...agentBundle.mcpServers };
|
|
304
316
|
}
|
|
305
|
-
// Agent-level model/effort/etc — only if chatConfig
|
|
306
|
-
// override.
|
|
317
|
+
// Agent-level model/effort/etc — only if chatConfig AND
|
|
318
|
+
// topicConfig don't override.
|
|
307
319
|
for (const key of ['model', 'effort', 'thinking', 'permissionMode']) {
|
|
308
|
-
if (agentBundle.raw?.[key] != null
|
|
320
|
+
if (agentBundle.raw?.[key] != null
|
|
321
|
+
&& chatConfig[key] == null
|
|
322
|
+
&& topicConfig?.[key] == null) {
|
|
309
323
|
opts[key] = agentBundle.raw[key];
|
|
310
324
|
}
|
|
311
325
|
}
|
|
312
326
|
}
|
|
313
327
|
|
|
314
|
-
// Chat-level overrides
|
|
328
|
+
// Chat-level overrides.
|
|
315
329
|
for (const [k, v] of Object.entries(chatConfig)) {
|
|
316
330
|
if (v == null) continue;
|
|
317
331
|
// Don't override the spread system-prompt with `agent` config
|
|
@@ -320,6 +334,18 @@ function composeSdkOptions(chatConfig = {}, agentBundle = null, defaults = {}) {
|
|
|
320
334
|
opts[k] = v;
|
|
321
335
|
}
|
|
322
336
|
|
|
337
|
+
// rc.48: per-topic overrides (highest priority). Same `agent` exclusion
|
|
338
|
+
// — `agent` here is a polygram name reference, NOT an SdkOptions
|
|
339
|
+
// field. polygram's spawn flow resolves topicConfig.agent into the
|
|
340
|
+
// correct agentBundle BEFORE calling composeSdkOptions, so by the
|
|
341
|
+
// time we get here, agentBundle already reflects the topic's agent
|
|
342
|
+
// choice and the `agent` string itself shouldn't leak into opts.
|
|
343
|
+
for (const [k, v] of Object.entries(topicConfig || {})) {
|
|
344
|
+
if (v == null) continue;
|
|
345
|
+
if (k === 'agent') continue;
|
|
346
|
+
opts[k] = v;
|
|
347
|
+
}
|
|
348
|
+
|
|
323
349
|
return opts;
|
|
324
350
|
}
|
|
325
351
|
|
|
@@ -193,6 +193,17 @@ class ProcessManagerSdk {
|
|
|
193
193
|
onStreamChunk = null,
|
|
194
194
|
onToolUse = null,
|
|
195
195
|
onAssistantMessageStart = null,
|
|
196
|
+
// rc.47: fires when an SDK assistant message arrives with NO head
|
|
197
|
+
// pending in entry.pendingQueue — i.e. an autonomous turn (typical
|
|
198
|
+
// ScheduleWakeup case where the agent self-fires without a
|
|
199
|
+
// corresponding pm.send). Polygram wires this to a Telegram-send
|
|
200
|
+
// function that derives chat_id (always) and thread_id (when
|
|
201
|
+
// isolateTopics) from the sessionKey via getChatIdFromKey /
|
|
202
|
+
// getThreadIdFromKey, then forwards the text to the right chat/
|
|
203
|
+
// topic. Pre-rc.47 these messages were silently dropped at the
|
|
204
|
+
// `&& head` gate in _handleEvent. Subagent messages
|
|
205
|
+
// (parent_tool_use_id != null) are still filtered upstream.
|
|
206
|
+
onAutonomousAssistantMessage = null,
|
|
196
207
|
onCompactBoundary = null,
|
|
197
208
|
onQueueDrop = null,
|
|
198
209
|
onThinking = null,
|
|
@@ -211,6 +222,7 @@ class ProcessManagerSdk {
|
|
|
211
222
|
this.onStreamChunk = onStreamChunk;
|
|
212
223
|
this.onToolUse = onToolUse;
|
|
213
224
|
this.onAssistantMessageStart = onAssistantMessageStart;
|
|
225
|
+
this.onAutonomousAssistantMessage = onAutonomousAssistantMessage;
|
|
214
226
|
this.onCompactBoundary = onCompactBoundary;
|
|
215
227
|
this.onQueueDrop = onQueueDrop;
|
|
216
228
|
this.onThinking = onThinking;
|
|
@@ -425,6 +437,23 @@ class ProcessManagerSdk {
|
|
|
425
437
|
return;
|
|
426
438
|
}
|
|
427
439
|
|
|
440
|
+
if (msg.type === 'assistant' && !head) {
|
|
441
|
+
// rc.47: autonomous assistant message — no user-initiated
|
|
442
|
+
// pm.send is in flight. Typical cause: ScheduleWakeup fired,
|
|
443
|
+
// the agent emitted a self-driven response. Pre-rc.47 these
|
|
444
|
+
// were silently dropped by the `&& head` gate. Now we route
|
|
445
|
+
// them via onAutonomousAssistantMessage so polygram can
|
|
446
|
+
// forward the text to the right Telegram chat/topic.
|
|
447
|
+
if (msg.parent_tool_use_id != null) return;
|
|
448
|
+
const text = extractAssistantText(msg);
|
|
449
|
+
if (!text) return;
|
|
450
|
+
if (this.onAutonomousAssistantMessage) {
|
|
451
|
+
try { this.onAutonomousAssistantMessage(entry.sessionKey, msg, entry); }
|
|
452
|
+
catch (err) { this.logger.error?.(`[${entry.label}] onAutonomousAssistantMessage: ${err.message}`); }
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
428
457
|
if (msg.type === 'assistant' && head) {
|
|
429
458
|
// Subagent filter (Phase 1 step 7): top-level only.
|
|
430
459
|
if (msg.parent_tool_use_id != null) return;
|
package/lib/session-key.js
CHANGED
|
@@ -16,8 +16,25 @@
|
|
|
16
16
|
* topic's conversation can't bleed into Billing topic's memory. This
|
|
17
17
|
* matches OpenClaw's model and is the right call when topics represent
|
|
18
18
|
* genuinely separate projects.
|
|
19
|
+
*
|
|
20
|
+
* rc.48: per-topic config overrides. `topics[threadId]` can be either a
|
|
21
|
+
* string (legacy: just a label) or an object with optional fields:
|
|
22
|
+
* { name, agent, cwd, model, effort, permissionMode }
|
|
23
|
+
* Object form lets a topic override chat-level config — typically used
|
|
24
|
+
* to scope a single topic to a different agent (e.g. music-curation),
|
|
25
|
+
* a different working dir, or to switch from `bypassPermissions` to
|
|
26
|
+
* `default` so canUseTool prompts fire for sensitive operations in
|
|
27
|
+
* that topic only. See getTopicConfig.
|
|
28
|
+
*
|
|
29
|
+
* Per-topic overrides only take effect when isolateTopics: true (each
|
|
30
|
+
* topic has its own SDK Query). With isolateTopics: false all topics
|
|
31
|
+
* share one Query; the Query's options are fixed at first-spawn time.
|
|
32
|
+
* polygram emits a one-time startup warning if topic overrides are
|
|
33
|
+
* configured on a non-isolating chat.
|
|
19
34
|
*/
|
|
20
35
|
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
21
38
|
function getSessionKey(chatId, threadId, chatConfig) {
|
|
22
39
|
const isolate = chatConfig?.isolateTopics === true;
|
|
23
40
|
if (threadId && isolate) return `${chatId}:${threadId}`;
|
|
@@ -28,4 +45,71 @@ function getChatIdFromKey(sessionKey) {
|
|
|
28
45
|
return sessionKey.split(':')[0];
|
|
29
46
|
}
|
|
30
47
|
|
|
31
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Inverse of `getChatIdFromKey`: returns the thread_id portion of an
|
|
50
|
+
* isolated-topic sessionKey, or null when there's no thread suffix.
|
|
51
|
+
* Used by rc.47 autonomous-wakeup routing — when ScheduleWakeup
|
|
52
|
+
* fires inside a polygram-spawned Query without a corresponding
|
|
53
|
+
* pm.send, we derive (chat_id, thread_id) from sessionKey to route
|
|
54
|
+
* the autonomous output back to the right Telegram chat/topic.
|
|
55
|
+
*/
|
|
56
|
+
function getThreadIdFromKey(sessionKey) {
|
|
57
|
+
if (typeof sessionKey !== 'string' || !sessionKey) return null;
|
|
58
|
+
const idx = sessionKey.indexOf(':');
|
|
59
|
+
if (idx < 0) return null;
|
|
60
|
+
const thread = sessionKey.slice(idx + 1);
|
|
61
|
+
return thread || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve the human-readable name for a topic. Handles both the legacy
|
|
66
|
+
* string form (`topics["100"] = "Orders"`) and the rc.48 object form
|
|
67
|
+
* (`topics["200"] = { name: "Music", agent: "music-curation" }`). Falls
|
|
68
|
+
* back to the threadId itself if the topic has no name field.
|
|
69
|
+
*/
|
|
70
|
+
function getTopicName(chatConfig, threadId) {
|
|
71
|
+
if (threadId == null || threadId === '') return null;
|
|
72
|
+
const t = chatConfig?.topics?.[threadId];
|
|
73
|
+
if (typeof t === 'string') return t;
|
|
74
|
+
if (t && typeof t === 'object' && typeof t.name === 'string' && t.name) {
|
|
75
|
+
return t.name;
|
|
76
|
+
}
|
|
77
|
+
return String(threadId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* rc.48: extract per-topic SdkOptions overrides. Returns the topic
|
|
82
|
+
* entry's overridable fields (model, effort, cwd, agent,
|
|
83
|
+
* permissionMode), excluding `name` (which is a polygram label, not
|
|
84
|
+
* an SdkOptions field — see getTopicName).
|
|
85
|
+
*
|
|
86
|
+
* Returns `{}` for:
|
|
87
|
+
* - missing threadId
|
|
88
|
+
* - missing chatConfig.topics
|
|
89
|
+
* - threadId not present in topics
|
|
90
|
+
* - legacy string topic entry (label-only, no overrides)
|
|
91
|
+
* - object with only `name`
|
|
92
|
+
*
|
|
93
|
+
* Callers (composeSdkOptions, polygram's spawn flow) merge this on
|
|
94
|
+
* top of chat-level config. Per-topic overrides take HIGHEST
|
|
95
|
+
* precedence — including overriding the chat-level permissionMode,
|
|
96
|
+
* which is the principal rc.48 use case (loosen one topic from the
|
|
97
|
+
* agent's `bypassPermissions` default to `default`).
|
|
98
|
+
*/
|
|
99
|
+
function getTopicConfig(chatConfig, threadId) {
|
|
100
|
+
if (threadId == null || threadId === '') return {};
|
|
101
|
+
const t = chatConfig?.topics?.[threadId];
|
|
102
|
+
if (!t || typeof t !== 'object') return {};
|
|
103
|
+
// Strip `name` — that's the label, not an override. Everything else
|
|
104
|
+
// in the entry is an override candidate.
|
|
105
|
+
const { name, ...overrides } = t;
|
|
106
|
+
return overrides;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
getSessionKey,
|
|
111
|
+
getChatIdFromKey,
|
|
112
|
+
getThreadIdFromKey,
|
|
113
|
+
getTopicName,
|
|
114
|
+
getTopicConfig,
|
|
115
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.8.0-rc.
|
|
3
|
+
"version": "0.8.0-rc.48",
|
|
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
|
@@ -30,7 +30,7 @@ const { ProcessManager } = require('./lib/process-manager');
|
|
|
30
30
|
// callbacks), so the rest of polygram.js doesn't branch beyond the
|
|
31
31
|
// pick-at-startup. Phase 4 deletes the CLI version after Phase 5
|
|
32
32
|
// soak proves SDK stable. See docs/0.8.0-architecture-decisions.md.
|
|
33
|
-
const { ProcessManagerSdk } = require('./lib/process-manager-sdk');
|
|
33
|
+
const { ProcessManagerSdk, extractAssistantText } = require('./lib/process-manager-sdk');
|
|
34
34
|
// rc.42: autosteer-buffer module deleted. Native SDK priority push
|
|
35
35
|
// (pm.injectUserMessage) replaces the buffer + PostToolBatch detour.
|
|
36
36
|
const { createAutosteeredRefs } = require('./lib/autosteered-refs');
|
|
@@ -195,12 +195,13 @@ function isWellFormedMessage(msg) {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
// ─── Session key — moved to lib/session-key.js so tests can import it. ─
|
|
198
|
-
const {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
const {
|
|
199
|
+
getSessionKey,
|
|
200
|
+
getChatIdFromKey,
|
|
201
|
+
getThreadIdFromKey,
|
|
202
|
+
getTopicName,
|
|
203
|
+
getTopicConfig,
|
|
204
|
+
} = require('./lib/session-key');
|
|
204
205
|
|
|
205
206
|
function getSessionLabel(chatConfig, threadId) {
|
|
206
207
|
const topic = getTopicName(chatConfig, threadId);
|
|
@@ -826,30 +827,52 @@ function spawnClaude(sessionKey, ctx) {
|
|
|
826
827
|
* mcpServers, optional resume sessionId for continuity.
|
|
827
828
|
*/
|
|
828
829
|
function buildSdkOptions(sessionKey, ctx) {
|
|
829
|
-
const { chatConfig, existingSessionId, label, chatId } = ctx;
|
|
830
|
+
const { chatConfig, existingSessionId, label, chatId, threadId } = ctx;
|
|
831
|
+
|
|
832
|
+
// rc.48: per-topic config overrides. When `topics[threadId]` is an
|
|
833
|
+
// object (not a legacy string label), these fields take precedence
|
|
834
|
+
// over chat-level config. Principal use case: scope a single topic
|
|
835
|
+
// to a different agent, cwd, or permissionMode (e.g. flip music
|
|
836
|
+
// topic from `bypassPermissions` to `default` so settings.json
|
|
837
|
+
// permissions.allow gates apply).
|
|
838
|
+
//
|
|
839
|
+
// Per-topic overrides only apply meaningfully when isolateTopics is
|
|
840
|
+
// true — each topic has its own SDK Query. With isolateTopics: false
|
|
841
|
+
// all topics share one Query whose options are fixed at first-spawn.
|
|
842
|
+
// Startup validation (validateTopicConfigs) emits a one-time warning
|
|
843
|
+
// when a chat has topic overrides + isolateTopics: false.
|
|
844
|
+
const topicConfig = getTopicConfig(chatConfig, threadId);
|
|
845
|
+
const effectiveAgent = topicConfig.agent || chatConfig.agent;
|
|
830
846
|
|
|
831
847
|
// Per-chat agent (D14): if pinned, load & compose. Failure is
|
|
832
848
|
// non-fatal — chat falls back to defaults; logged for ops.
|
|
833
849
|
let agentBundle = null;
|
|
834
|
-
if (
|
|
850
|
+
if (effectiveAgent) {
|
|
835
851
|
try {
|
|
836
|
-
agentBundle = agentLoader.loadAgent(
|
|
852
|
+
agentBundle = agentLoader.loadAgent(effectiveAgent, {
|
|
837
853
|
homeDir: CHILD_HOME,
|
|
838
854
|
// Pass cwd so the loader checks Claude Code's project-level
|
|
839
855
|
// path (`<cwd>/.claude/agents/<name>.md`) before the
|
|
840
|
-
// user-level path or polygram's directory convention.
|
|
841
|
-
cwd
|
|
856
|
+
// user-level path or polygram's directory convention. rc.48:
|
|
857
|
+
// topic-level cwd takes precedence so topic-scoped agents
|
|
858
|
+
// load from the topic's project dir.
|
|
859
|
+
cwd: topicConfig.cwd || chatConfig.cwd,
|
|
842
860
|
logger: console,
|
|
843
861
|
});
|
|
844
862
|
} catch (err) {
|
|
845
863
|
console.error(`[${label}] agent-loader: ${err.message}`);
|
|
846
864
|
logEvent('agent-load-failed', {
|
|
847
|
-
chat_id: chatId, agent:
|
|
865
|
+
chat_id: chatId, agent: effectiveAgent, error: err.message,
|
|
866
|
+
topic: threadId || null,
|
|
848
867
|
});
|
|
849
868
|
}
|
|
850
869
|
}
|
|
851
870
|
|
|
852
|
-
|
|
871
|
+
const effectiveModel = topicConfig.model || chatConfig.model;
|
|
872
|
+
const effectiveEffort = topicConfig.effort || chatConfig.effort;
|
|
873
|
+
const agentSuffix = effectiveAgent && effectiveAgent !== chatConfig.agent
|
|
874
|
+
? ` agent=${effectiveAgent}` : '';
|
|
875
|
+
console.log(`[${label}] Spawning SDK Query (${effectiveModel}/${effectiveEffort}${agentSuffix})`);
|
|
853
876
|
|
|
854
877
|
// Env: SHADOW semantics (gate 33) — must enumerate every var
|
|
855
878
|
// pollygram needs in the spawned worker.
|
|
@@ -916,10 +939,11 @@ function buildSdkOptions(sessionKey, ctx) {
|
|
|
916
939
|
}),
|
|
917
940
|
};
|
|
918
941
|
|
|
919
|
-
// Compose with agent overlay + chat-level config
|
|
920
|
-
// precedence: chatConfig > agent > defaults.
|
|
921
|
-
// we care about for SDK options are
|
|
922
|
-
// others (agent, chrome, isolateTopics)
|
|
942
|
+
// Compose with agent overlay + chat-level config + per-topic overrides.
|
|
943
|
+
// agent-loader precedence: topicConfig > chatConfig > agent > defaults.
|
|
944
|
+
// The chatConfig keys we care about for SDK options are
|
|
945
|
+
// model/effort/cwd/thinking; others (agent, chrome, isolateTopics)
|
|
946
|
+
// are polygram-only and stripped by composeSdkOptions.
|
|
923
947
|
const composed = agentLoader.composeSdkOptions(
|
|
924
948
|
{
|
|
925
949
|
// chat-level overrides — only the keys SDK understands.
|
|
@@ -930,8 +954,19 @@ function buildSdkOptions(sessionKey, ctx) {
|
|
|
930
954
|
},
|
|
931
955
|
agentBundle,
|
|
932
956
|
baseOpts,
|
|
957
|
+
topicConfig, // rc.48: highest precedence — overrides chat-level fields.
|
|
933
958
|
);
|
|
934
959
|
|
|
960
|
+
// rc.48: keep permissionMode + allowDangerouslySkipPermissions
|
|
961
|
+
// consistent. baseOpts sets allowDangerouslySkipPermissions=true when
|
|
962
|
+
// permissionMode='bypassPermissions'. If a topic flipped permissionMode
|
|
963
|
+
// to anything else (typically 'default' to gate via settings.json),
|
|
964
|
+
// also disable allowDangerouslySkipPermissions so the SDK actually
|
|
965
|
+
// honours the permission gates instead of skipping them.
|
|
966
|
+
if (composed.permissionMode && composed.permissionMode !== 'bypassPermissions') {
|
|
967
|
+
composed.allowDangerouslySkipPermissions = false;
|
|
968
|
+
}
|
|
969
|
+
|
|
935
970
|
// Append polygram's display constraints to the systemPrompt.
|
|
936
971
|
// Infrastructure-layer hint — the agent's own prompt covers
|
|
937
972
|
// business logic; polygram adds "your output renders in Telegram,
|
|
@@ -3665,6 +3700,50 @@ async function main() {
|
|
|
3665
3700
|
const r = head?.context?.reactor;
|
|
3666
3701
|
if (r && typeof r.heartbeat === 'function') r.heartbeat();
|
|
3667
3702
|
},
|
|
3703
|
+
// rc.47: autonomous wakeup forwarding. Fires when an SDK
|
|
3704
|
+
// assistant message arrives with no head pending — typical
|
|
3705
|
+
// ScheduleWakeup case where the agent self-fires without an
|
|
3706
|
+
// inbound user message. We derive chat_id (always) and thread_id
|
|
3707
|
+
// (when isolateTopics) from the sessionKey, then send the text
|
|
3708
|
+
// to that chat/topic. Subagent messages were already filtered
|
|
3709
|
+
// upstream (parent_tool_use_id != null check in pm-sdk).
|
|
3710
|
+
//
|
|
3711
|
+
// Best-effort send: failures are logged but don't propagate —
|
|
3712
|
+
// an autonomous turn that can't be delivered shouldn't crash
|
|
3713
|
+
// the daemon. Telemetry emitted as `autonomous-wakeup-message`
|
|
3714
|
+
// so we can audit how often these fire and whether any get lost.
|
|
3715
|
+
onAutonomousAssistantMessage: (sessionKey, msg /* , entry */) => {
|
|
3716
|
+
try {
|
|
3717
|
+
const text = extractAssistantText(msg);
|
|
3718
|
+
if (!text) return;
|
|
3719
|
+
const chatId = getChatIdFromKey(sessionKey);
|
|
3720
|
+
const threadIdRaw = getThreadIdFromKey(sessionKey);
|
|
3721
|
+
const threadId = threadIdRaw ? parseInt(threadIdRaw, 10) : null;
|
|
3722
|
+
if (!bot) {
|
|
3723
|
+
console.error(`[${BOT_NAME}] autonomous wakeup: bot not ready, dropping ${text.length} chars`);
|
|
3724
|
+
return;
|
|
3725
|
+
}
|
|
3726
|
+
const params = {
|
|
3727
|
+
chat_id: chatId,
|
|
3728
|
+
text,
|
|
3729
|
+
...(Number.isInteger(threadId) && { message_thread_id: threadId }),
|
|
3730
|
+
};
|
|
3731
|
+
// Don't `await` — keep the pm-sdk event loop unblocked. The
|
|
3732
|
+
// tg() wrapper handles its own retries / chunking.
|
|
3733
|
+
tg(bot, 'sendMessage', params,
|
|
3734
|
+
{ source: 'autonomous-wakeup', botName: BOT_NAME }).catch((err) => {
|
|
3735
|
+
console.error(`[${BOT_NAME}] autonomous wakeup send failed: ${err.message}`);
|
|
3736
|
+
});
|
|
3737
|
+
logEvent('autonomous-wakeup-message', {
|
|
3738
|
+
chat_id: chatId,
|
|
3739
|
+
session_key: sessionKey,
|
|
3740
|
+
thread_id: threadIdRaw,
|
|
3741
|
+
text_len: text.length,
|
|
3742
|
+
});
|
|
3743
|
+
} catch (err) {
|
|
3744
|
+
console.error(`[${BOT_NAME}] autonomous wakeup handler: ${err.message}`);
|
|
3745
|
+
}
|
|
3746
|
+
},
|
|
3668
3747
|
// rc.29 onThinking removed — replaced by simpler timer-based
|
|
3669
3748
|
// approach in handleMessage (post-QUEUED setState). The
|
|
3670
3749
|
// SDK-thinking-block detection added complexity (partial-message
|
|
@@ -3751,6 +3830,26 @@ async function main() {
|
|
|
3751
3830
|
console.log(`polygram (LRU cap=${cap}, SQLite source of truth)`);
|
|
3752
3831
|
console.log(`Chats: ${Object.entries(config.chats).map(([id, c]) => `${c.name} (${c.model}/${c.effort})`).join(', ')}`);
|
|
3753
3832
|
|
|
3833
|
+
// rc.48: validate per-topic config + isolateTopics relationship.
|
|
3834
|
+
// Per-topic SdkOptions overrides only take effect when each topic
|
|
3835
|
+
// gets its own SDK Query (isolateTopics: true). Without isolation
|
|
3836
|
+
// the Query is fixed at first-spawn time; subsequent topic-scoped
|
|
3837
|
+
// messages share that Query regardless of topic-level overrides.
|
|
3838
|
+
for (const [chatId, chatCfg] of Object.entries(config.chats)) {
|
|
3839
|
+
if (!chatCfg.topics || typeof chatCfg.topics !== 'object') continue;
|
|
3840
|
+
const overrideTopics = Object.entries(chatCfg.topics)
|
|
3841
|
+
.filter(([, t]) => t && typeof t === 'object'
|
|
3842
|
+
&& Object.keys(t).some((k) => k !== 'name'));
|
|
3843
|
+
if (overrideTopics.length === 0) continue;
|
|
3844
|
+
if (chatCfg.isolateTopics !== true) {
|
|
3845
|
+
const ids = overrideTopics.map(([id]) => id).join(', ');
|
|
3846
|
+
console.warn(`[${BOT_NAME}] WARN: chat ${chatId} (${chatCfg.name}) has topic overrides on topic_ids=${ids} but isolateTopics is not true — overrides will be IGNORED. Set isolateTopics: true to make per-topic config take effect.`);
|
|
3847
|
+
logEvent('topic-override-without-isolation', {
|
|
3848
|
+
chat_id: chatId, name: chatCfg.name, topic_ids: ids,
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3754
3853
|
bot = createBot(config.bot.token);
|
|
3755
3854
|
|
|
3756
3855
|
// Graceful shutdown: stop accepting new inbound, drain in-flight pendings
|