polygram 0.8.0-rc.45 → 0.8.0-rc.47
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/process-manager-sdk.js +29 -0
- package/lib/session-key.js +17 -1
- package/package.json +1 -1
- package/polygram.js +78 -2
|
@@ -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.47",
|
|
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",
|
|
@@ -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
|
@@ -28,4 +28,20 @@ function getChatIdFromKey(sessionKey) {
|
|
|
28
28
|
return sessionKey.split(':')[0];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Inverse of `getChatIdFromKey`: returns the thread_id portion of an
|
|
33
|
+
* isolated-topic sessionKey, or null when there's no thread suffix.
|
|
34
|
+
* Used by rc.47 autonomous-wakeup routing — when ScheduleWakeup
|
|
35
|
+
* fires inside a polygram-spawned Query without a corresponding
|
|
36
|
+
* pm.send, we derive (chat_id, thread_id) from sessionKey to route
|
|
37
|
+
* the autonomous output back to the right Telegram chat/topic.
|
|
38
|
+
*/
|
|
39
|
+
function getThreadIdFromKey(sessionKey) {
|
|
40
|
+
if (typeof sessionKey !== 'string' || !sessionKey) return null;
|
|
41
|
+
const idx = sessionKey.indexOf(':');
|
|
42
|
+
if (idx < 0) return null;
|
|
43
|
+
const thread = sessionKey.slice(idx + 1);
|
|
44
|
+
return thread || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { getSessionKey, getChatIdFromKey, getThreadIdFromKey };
|
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.47",
|
|
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,7 +195,7 @@ function isWellFormedMessage(msg) {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
// ─── Session key — moved to lib/session-key.js so tests can import it. ─
|
|
198
|
-
const { getSessionKey, getChatIdFromKey } = require('./lib/session-key');
|
|
198
|
+
const { getSessionKey, getChatIdFromKey, getThreadIdFromKey } = require('./lib/session-key');
|
|
199
199
|
|
|
200
200
|
function getTopicName(chatConfig, threadId) {
|
|
201
201
|
if (!threadId) return null;
|
|
@@ -1947,6 +1947,38 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
1947
1947
|
}
|
|
1948
1948
|
return;
|
|
1949
1949
|
}
|
|
1950
|
+
// 0.8.0-rc.46: /reload — close + respawn the SDK Query while
|
|
1951
|
+
// PRESERVING the persisted claude_session_id. The next user
|
|
1952
|
+
// message resumes the conversation (model has prior context via
|
|
1953
|
+
// SDK resume) but the spawn re-reads agent / skill / plugin files
|
|
1954
|
+
// from disk, so any local edits to ~/.claude/agents/<name>.md or
|
|
1955
|
+
// skill files take effect.
|
|
1956
|
+
//
|
|
1957
|
+
// Difference vs /new:
|
|
1958
|
+
// /new → resetSession clears session_id → fresh conversation
|
|
1959
|
+
// /reload → kill closes Query, session_id preserved → same
|
|
1960
|
+
// conversation continues with fresh agent/skill code
|
|
1961
|
+
//
|
|
1962
|
+
// Mechanism: pm.kill drains the pending queue (with code 'KILLED')
|
|
1963
|
+
// and closes the SDK Query. Crucially it does NOT call
|
|
1964
|
+
// db.clearSessionId — the contract test
|
|
1965
|
+
// 'pm.kill does NOT call db.clearSessionId' pins this invariant.
|
|
1966
|
+
// The next user message hits getOrSpawn → no warm Query → spawn
|
|
1967
|
+
// fresh with `resume: <session_id>` from sessions table → SDK
|
|
1968
|
+
// restores conversation history → fresh files loaded.
|
|
1969
|
+
if (botAllowsCommands && text === '/reload') {
|
|
1970
|
+
if (pm.has(sessionKey)) {
|
|
1971
|
+
try { await pm.kill(sessionKey); }
|
|
1972
|
+
catch (err) { console.error(`[${label}] kill on /reload: ${err.message}`); }
|
|
1973
|
+
}
|
|
1974
|
+
logEvent('session-reload-command', {
|
|
1975
|
+
chat_id: chatId, command: text,
|
|
1976
|
+
user: cmdUser, user_id: cmdUserId,
|
|
1977
|
+
});
|
|
1978
|
+
await sendReply('🔄 Reloaded. Next message picks up the conversation with fresh skills/agents.');
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1950
1982
|
if (botAllowsCommands && (text === '/new' || text === '/reset')) {
|
|
1951
1983
|
let drained = 0;
|
|
1952
1984
|
const target = pm.pickFor(sessionKey);
|
|
@@ -3633,6 +3665,50 @@ async function main() {
|
|
|
3633
3665
|
const r = head?.context?.reactor;
|
|
3634
3666
|
if (r && typeof r.heartbeat === 'function') r.heartbeat();
|
|
3635
3667
|
},
|
|
3668
|
+
// rc.47: autonomous wakeup forwarding. Fires when an SDK
|
|
3669
|
+
// assistant message arrives with no head pending — typical
|
|
3670
|
+
// ScheduleWakeup case where the agent self-fires without an
|
|
3671
|
+
// inbound user message. We derive chat_id (always) and thread_id
|
|
3672
|
+
// (when isolateTopics) from the sessionKey, then send the text
|
|
3673
|
+
// to that chat/topic. Subagent messages were already filtered
|
|
3674
|
+
// upstream (parent_tool_use_id != null check in pm-sdk).
|
|
3675
|
+
//
|
|
3676
|
+
// Best-effort send: failures are logged but don't propagate —
|
|
3677
|
+
// an autonomous turn that can't be delivered shouldn't crash
|
|
3678
|
+
// the daemon. Telemetry emitted as `autonomous-wakeup-message`
|
|
3679
|
+
// so we can audit how often these fire and whether any get lost.
|
|
3680
|
+
onAutonomousAssistantMessage: (sessionKey, msg /* , entry */) => {
|
|
3681
|
+
try {
|
|
3682
|
+
const text = extractAssistantText(msg);
|
|
3683
|
+
if (!text) return;
|
|
3684
|
+
const chatId = getChatIdFromKey(sessionKey);
|
|
3685
|
+
const threadIdRaw = getThreadIdFromKey(sessionKey);
|
|
3686
|
+
const threadId = threadIdRaw ? parseInt(threadIdRaw, 10) : null;
|
|
3687
|
+
if (!bot) {
|
|
3688
|
+
console.error(`[${BOT_NAME}] autonomous wakeup: bot not ready, dropping ${text.length} chars`);
|
|
3689
|
+
return;
|
|
3690
|
+
}
|
|
3691
|
+
const params = {
|
|
3692
|
+
chat_id: chatId,
|
|
3693
|
+
text,
|
|
3694
|
+
...(Number.isInteger(threadId) && { message_thread_id: threadId }),
|
|
3695
|
+
};
|
|
3696
|
+
// Don't `await` — keep the pm-sdk event loop unblocked. The
|
|
3697
|
+
// tg() wrapper handles its own retries / chunking.
|
|
3698
|
+
tg(bot, 'sendMessage', params,
|
|
3699
|
+
{ source: 'autonomous-wakeup', botName: BOT_NAME }).catch((err) => {
|
|
3700
|
+
console.error(`[${BOT_NAME}] autonomous wakeup send failed: ${err.message}`);
|
|
3701
|
+
});
|
|
3702
|
+
logEvent('autonomous-wakeup-message', {
|
|
3703
|
+
chat_id: chatId,
|
|
3704
|
+
session_key: sessionKey,
|
|
3705
|
+
thread_id: threadIdRaw,
|
|
3706
|
+
text_len: text.length,
|
|
3707
|
+
});
|
|
3708
|
+
} catch (err) {
|
|
3709
|
+
console.error(`[${BOT_NAME}] autonomous wakeup handler: ${err.message}`);
|
|
3710
|
+
}
|
|
3711
|
+
},
|
|
3636
3712
|
// rc.29 onThinking removed — replaced by simpler timer-based
|
|
3637
3713
|
// approach in handleMessage (post-QUEUED setState). The
|
|
3638
3714
|
// SDK-thinking-block detection added complexity (partial-message
|