polygram 0.8.0-rc.50 → 0.8.0-rc.52
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/db.js +21 -0
- package/lib/history-preload.js +60 -0
- package/lib/prompt.js +10 -1
- package/package.json +1 -1
- package/polygram.js +60 -25
|
@@ -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.52",
|
|
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/lib/db.js
CHANGED
|
@@ -498,6 +498,27 @@ function wrap(db) {
|
|
|
498
498
|
// Treating ambiguous states as "replied" costs us occasional missed
|
|
499
499
|
// replies (recoverable: user resends) to prevent duplicates
|
|
500
500
|
// (irrecoverable: user has to mentally dedupe two answers).
|
|
501
|
+
// rc.51: stricter dedupe than hasOutboundReplyTo for boot-replay.
|
|
502
|
+
// A `turn_metrics` row is only inserted when a turn definitively
|
|
503
|
+
// completes (onResult callback). If no row exists for this inbound
|
|
504
|
+
// msg_id, the turn never finished — even if intermediate ack-bubbles
|
|
505
|
+
// were already sent. The rc.50 incident's lost msg 12158 had a
|
|
506
|
+
// partial "I'll write a quick inline script..." outbound but no
|
|
507
|
+
// turn_metrics, and was being silently skipped by replay-dedupe.
|
|
508
|
+
//
|
|
509
|
+
// Caveat: a row whose `error` is set (transient/aborted/timeout)
|
|
510
|
+
// does NOT count as complete — the turn started but failed. Boot
|
|
511
|
+
// replay should redispatch within window so the user gets a real
|
|
512
|
+
// answer.
|
|
513
|
+
hasCompletedTurnFor({ chat_id, msg_id }) {
|
|
514
|
+
const row = db.prepare(`
|
|
515
|
+
SELECT 1 FROM turn_metrics
|
|
516
|
+
WHERE chat_id = ? AND msg_id = ? AND error IS NULL
|
|
517
|
+
LIMIT 1
|
|
518
|
+
`).get(String(chat_id), msg_id);
|
|
519
|
+
return !!row;
|
|
520
|
+
},
|
|
521
|
+
|
|
501
522
|
hasOutboundReplyTo({ chat_id, msg_id }) {
|
|
502
523
|
const row = db.prepare(`
|
|
503
524
|
SELECT 1 FROM messages
|
package/lib/history-preload.js
CHANGED
|
@@ -151,8 +151,68 @@ function makeSessionStartHook({
|
|
|
151
151
|
};
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
+
/**
|
|
155
|
+
* rc.52: synchronous variant of the same preload, returning the
|
|
156
|
+
* `<polygram-history>` block as a string ready to prepend to the
|
|
157
|
+
* fresh-session user message. Replaces the SessionStart-hook path
|
|
158
|
+
* (the SDK's `Options.hooks.SessionStart` is a documented API that
|
|
159
|
+
* the runtime does not actually dispatch — verified by spike +
|
|
160
|
+
* SDK source grep, see rc.52 commit).
|
|
161
|
+
*
|
|
162
|
+
* Exclude the row corresponding to `excludeMsgId` from the preload
|
|
163
|
+
* — that's the user message we're about to send, no need to echo
|
|
164
|
+
* it back to itself in the history block.
|
|
165
|
+
*
|
|
166
|
+
* Returns '' (empty string) when there's nothing to inject — caller
|
|
167
|
+
* just skips the prepend.
|
|
168
|
+
*/
|
|
169
|
+
function buildHistoryBlock({
|
|
170
|
+
db,
|
|
171
|
+
chatId,
|
|
172
|
+
threadId = null,
|
|
173
|
+
excludeMsgId = null,
|
|
174
|
+
limit = DEFAULT_PRELOAD_LIMIT,
|
|
175
|
+
since = DEFAULT_PRELOAD_SINCE,
|
|
176
|
+
logger = console,
|
|
177
|
+
} = {}) {
|
|
178
|
+
if (!db?.raw || !chatId) return '';
|
|
179
|
+
let rows;
|
|
180
|
+
try {
|
|
181
|
+
rows = history.recent(db, {
|
|
182
|
+
chatId: String(chatId),
|
|
183
|
+
threadId: threadId ?? null,
|
|
184
|
+
limit,
|
|
185
|
+
since,
|
|
186
|
+
includeOutbound: true,
|
|
187
|
+
allowedChatIds: [String(chatId)],
|
|
188
|
+
}) || [];
|
|
189
|
+
} catch (err) {
|
|
190
|
+
logger?.error?.(`[history-preload] recent() failed: ${err?.message || err}`);
|
|
191
|
+
return '';
|
|
192
|
+
}
|
|
193
|
+
if (excludeMsgId != null) {
|
|
194
|
+
rows = rows.filter((r) => String(r.msg_id) !== String(excludeMsgId));
|
|
195
|
+
}
|
|
196
|
+
if (rows.length === 0) return '';
|
|
197
|
+
const lines = rows.map(formatRow).join('\n');
|
|
198
|
+
const attrs = `chat_id="${chatId}"${threadId ? ` thread_id="${threadId}"` : ''} preloaded="${rows.length}" since="${since}"`;
|
|
199
|
+
return [
|
|
200
|
+
`<polygram-history ${attrs}>`,
|
|
201
|
+
lines,
|
|
202
|
+
`</polygram-history>`,
|
|
203
|
+
'',
|
|
204
|
+
'— More history available via `node skills/history/scripts/query.js`:',
|
|
205
|
+
' recent <chat_id> [thread_id] --limit N (older than the preload window)',
|
|
206
|
+
' around --chat <id> --msg-id N (context window around a message)',
|
|
207
|
+
' search <term> [chat_id] (FTS5 across full transcript)',
|
|
208
|
+
' by-user <name> [chat_id] [thread_id]',
|
|
209
|
+
' Bot scope is auto-resolved from cwd; no admin flag needed.',
|
|
210
|
+
].join('\n');
|
|
211
|
+
}
|
|
212
|
+
|
|
154
213
|
module.exports = {
|
|
155
214
|
makeSessionStartHook,
|
|
215
|
+
buildHistoryBlock,
|
|
156
216
|
// Internals for tests
|
|
157
217
|
_formatRow: formatRow,
|
|
158
218
|
DEFAULT_PRELOAD_LIMIT,
|
package/lib/prompt.js
CHANGED
|
@@ -164,7 +164,7 @@ function buildVoiceTags(attachments) {
|
|
|
164
164
|
* @param {Array} params.attachments - downloaded attachments
|
|
165
165
|
* @param {Object} params.replyTo - input for buildReplyToBlock (optional)
|
|
166
166
|
*/
|
|
167
|
-
function buildPrompt({ msg, topicName = '', sessionCtx = '', attachments = [], replyTo = null }) {
|
|
167
|
+
function buildPrompt({ msg, topicName = '', sessionCtx = '', attachments = [], replyTo = null, polygramHistory = '' }) {
|
|
168
168
|
const chatId = msg.chat.id.toString();
|
|
169
169
|
const msgId = msg.message_id.toString();
|
|
170
170
|
const user = msg.from?.first_name || msg.from?.username || 'Unknown';
|
|
@@ -179,6 +179,15 @@ function buildPrompt({ msg, topicName = '', sessionCtx = '', attachments = [], r
|
|
|
179
179
|
if (sessionCtx) {
|
|
180
180
|
prompt += `<session-context>\n${sessionCtx}\n</session-context>\n\n`;
|
|
181
181
|
}
|
|
182
|
+
// rc.52: fresh-session history preload. The caller (polygram.js)
|
|
183
|
+
// populates polygramHistory ONLY when this is the first message of a
|
|
184
|
+
// fresh Claude session (no --resume), built via
|
|
185
|
+
// history-preload.buildHistoryBlock. Empty string for resume-path
|
|
186
|
+
// turns. Replaces the SDK SessionStart hook which the SDK runtime
|
|
187
|
+
// doesn't actually dispatch (rc.52 finding).
|
|
188
|
+
if (polygramHistory) {
|
|
189
|
+
prompt += polygramHistory + '\n\n';
|
|
190
|
+
}
|
|
182
191
|
prompt += `<polygram-info>${POLYGRAM_INFO}</polygram-info>\n\n`;
|
|
183
192
|
|
|
184
193
|
const replyBlock = buildReplyToBlock(replyTo);
|
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.52",
|
|
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
|
@@ -43,7 +43,7 @@ const {
|
|
|
43
43
|
formatToolInputForCard,
|
|
44
44
|
approvalCardText,
|
|
45
45
|
} = require('./lib/approval-ui');
|
|
46
|
-
const {
|
|
46
|
+
const { buildHistoryBlock } = require('./lib/history-preload');
|
|
47
47
|
const { formatContextReply, maybeContextFullHint } = require('./lib/context-format');
|
|
48
48
|
const { appendDisplayHint, appendDisplayHintCliArgs } = require('./lib/telegram-prompt');
|
|
49
49
|
const { createAbortGrace } = require('./lib/abort-grace');
|
|
@@ -698,17 +698,53 @@ function resolveReplyTo(msg) {
|
|
|
698
698
|
return { replyToId };
|
|
699
699
|
}
|
|
700
700
|
|
|
701
|
-
function formatPrompt(msg, sessionCtx, attachments = []) {
|
|
701
|
+
function formatPrompt(msg, sessionCtx, attachments = [], { sessionKey = null } = {}) {
|
|
702
702
|
const chatId = msg.chat.id.toString();
|
|
703
703
|
const threadId = msg.message_thread_id?.toString() || '';
|
|
704
704
|
const chatConfig = config.chats[chatId];
|
|
705
705
|
const topicName = threadId ? getTopicName(chatConfig, threadId) : '';
|
|
706
|
+
|
|
707
|
+
// rc.52: when the upcoming Query has no resume target (fresh
|
|
708
|
+
// session — daemon boot, /new, /reset, first-ever message in a
|
|
709
|
+
// chat/topic), prepend a `<polygram-history>` block so the fresh
|
|
710
|
+
// session has continuity instead of starting blank. Replaces the
|
|
711
|
+
// dead SessionStart hook (registered into `Options.hooks.SessionStart`
|
|
712
|
+
// since rc.21 but never fired — the SDK runtime doesn't dispatch
|
|
713
|
+
// user-defined hooks for that event, only CLI settings.json shell
|
|
714
|
+
// hooks).
|
|
715
|
+
let polygramHistory = '';
|
|
716
|
+
if (sessionKey && db) {
|
|
717
|
+
const existingSessionId = getClaudeSessionId(db, sessionKey);
|
|
718
|
+
if (!existingSessionId) {
|
|
719
|
+
try {
|
|
720
|
+
polygramHistory = buildHistoryBlock({
|
|
721
|
+
db,
|
|
722
|
+
chatId,
|
|
723
|
+
threadId: threadId || null,
|
|
724
|
+
excludeMsgId: msg.message_id,
|
|
725
|
+
logger: console,
|
|
726
|
+
});
|
|
727
|
+
if (polygramHistory) {
|
|
728
|
+
logEvent('history-preloaded', {
|
|
729
|
+
chat_id: chatId,
|
|
730
|
+
thread_id: threadId || null,
|
|
731
|
+
text_len: polygramHistory.length,
|
|
732
|
+
session_source: 'fresh',
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
} catch (err) {
|
|
736
|
+
console.error(`[history-preload] buildHistoryBlock failed: ${err?.message || err}`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
706
741
|
return buildPrompt({
|
|
707
742
|
msg,
|
|
708
743
|
topicName,
|
|
709
744
|
sessionCtx,
|
|
710
745
|
attachments,
|
|
711
746
|
replyTo: resolveReplyTo(msg),
|
|
747
|
+
polygramHistory,
|
|
712
748
|
});
|
|
713
749
|
}
|
|
714
750
|
|
|
@@ -903,21 +939,13 @@ function buildSdkOptions(sessionKey, ctx) {
|
|
|
903
939
|
// turn-end path (handleMessage finally + success branches —
|
|
904
940
|
// existing rc.38 cleanup).
|
|
905
941
|
|
|
906
|
-
//
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
db,
|
|
914
|
-
chatId: ctx.chatId,
|
|
915
|
-
threadId: ctx.threadId ?? null,
|
|
916
|
-
logEvent,
|
|
917
|
-
logger: console,
|
|
918
|
-
})
|
|
919
|
-
: null;
|
|
920
|
-
|
|
942
|
+
// rc.52: dropped the SDK SessionStart hook registration. The hook
|
|
943
|
+
// never fired in production (verified: zero history-preloaded events
|
|
944
|
+
// across both production DBs since rc.21; SDK runtime grep showed
|
|
945
|
+
// exactly one occurrence of "SessionStart" — in the type listing,
|
|
946
|
+
// not in dispatch logic). The history preload is now done inline at
|
|
947
|
+
// formatPrompt time when the upcoming Query has no resume target,
|
|
948
|
+
// via lib/history-preload.buildHistoryBlock. See formatPrompt above.
|
|
921
949
|
const baseOpts = {
|
|
922
950
|
model: chatConfig.model || config.defaults.model,
|
|
923
951
|
effort: chatConfig.effort || config.defaults.effort,
|
|
@@ -929,11 +957,7 @@ function buildSdkOptions(sessionKey, ctx) {
|
|
|
929
957
|
permissionMode: useCanUseTool ? 'default' : 'bypassPermissions',
|
|
930
958
|
allowDangerouslySkipPermissions: !useCanUseTool,
|
|
931
959
|
...(useCanUseTool && { canUseTool: makeCanUseTool(sessionKey) }),
|
|
932
|
-
hooks: {
|
|
933
|
-
...(sessionStartHook && {
|
|
934
|
-
SessionStart: [{ hooks: [sessionStartHook] }],
|
|
935
|
-
}),
|
|
936
|
-
},
|
|
960
|
+
hooks: {},
|
|
937
961
|
executable: 'node',
|
|
938
962
|
...(existingSessionId && { resume: existingSessionId }),
|
|
939
963
|
...(process.env.POLYGRAM_CLAUDE_BIN && {
|
|
@@ -2278,7 +2302,7 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2278
2302
|
chatId, msgId: msg.message_id, label, botApi: bot, threadId,
|
|
2279
2303
|
}) || { ackEmitted: false };
|
|
2280
2304
|
|
|
2281
|
-
const prompt = formatPrompt(msg, sessionCtx, downloaded);
|
|
2305
|
+
const prompt = formatPrompt(msg, sessionCtx, downloaded, { sessionKey });
|
|
2282
2306
|
const stopTyping = startTyping({
|
|
2283
2307
|
bot, chatId, threadId,
|
|
2284
2308
|
logger: { error: (m) => console.error(`[${label}] ${m}`) },
|
|
@@ -3977,8 +4001,19 @@ async function main() {
|
|
|
3977
4001
|
let replayed = 0;
|
|
3978
4002
|
let skipped = 0;
|
|
3979
4003
|
for (const row of candidates) {
|
|
3980
|
-
|
|
3981
|
-
|
|
4004
|
+
// rc.51: dedupe on turn_metrics (definitive turn completion),
|
|
4005
|
+
// NOT just on hasOutboundReplyTo. The latter trips on
|
|
4006
|
+
// intermediate ack-bubbles (e.g. "Catching up on history…",
|
|
4007
|
+
// "I'll write a quick inline script…") and silently skips the
|
|
4008
|
+
// replay even when the actual answer never arrived. The rc.50
|
|
4009
|
+
// EIO-orphan incident lost Ivan DM msg 12158 this way: an ack
|
|
4010
|
+
// bubble was sent at 13:20:36, the turn was killed mid-flight,
|
|
4011
|
+
// boot-replay saw the ack and assumed "answered."
|
|
4012
|
+
//
|
|
4013
|
+
// turn_metrics is only inserted by the SDK pm's onResult
|
|
4014
|
+
// callback, which fires only when the turn definitively
|
|
4015
|
+
// completes. No row → no completion → re-dispatch.
|
|
4016
|
+
if (db.hasCompletedTurnFor({ chat_id: row.chat_id, msg_id: row.msg_id })) {
|
|
3982
4017
|
db.setInboundHandlerStatus({
|
|
3983
4018
|
chat_id: row.chat_id, msg_id: row.msg_id, status: 'replied',
|
|
3984
4019
|
});
|