polygram 0.8.0-rc.37 → 0.8.0-rc.38
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/autosteer-buffer.js +13 -1
- package/lib/process-manager.js +34 -1
- package/package.json +1 -1
- package/polygram.js +19 -0
|
@@ -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.38",
|
|
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/autosteer-buffer.js
CHANGED
|
@@ -120,7 +120,19 @@ function makePostToolBatchHook({ buffer, sessionKey, logEvent = null, chatId = n
|
|
|
120
120
|
} catch { /* logger errors must not break the hook */ }
|
|
121
121
|
}
|
|
122
122
|
if (typeof onDrained === 'function') {
|
|
123
|
-
|
|
123
|
+
// rc.38: async-safe. onDrained may return a Promise (it does
|
|
124
|
+
// today — clearAutosteeredReactions is async). A bare
|
|
125
|
+
// synchronous try/catch only catches throws, not rejections;
|
|
126
|
+
// an unhandled rejection escaping the hook would land on the
|
|
127
|
+
// process-level handler as misleading noise. Detect a
|
|
128
|
+
// thenable and attach .catch so async failures are logged at
|
|
129
|
+
// the same site, not as out-of-band unhandledRejection.
|
|
130
|
+
try {
|
|
131
|
+
const r = onDrained(sessionKey, drained.length);
|
|
132
|
+
if (r && typeof r.then === 'function') {
|
|
133
|
+
r.catch((err) => logger?.error?.(`[${sessionKey}] onDrained async: ${err?.message || err}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
124
136
|
catch (err) { logger?.error?.(`[${sessionKey}] onDrained: ${err?.message || err}`); }
|
|
125
137
|
}
|
|
126
138
|
return {
|
package/lib/process-manager.js
CHANGED
|
@@ -298,6 +298,14 @@ class ProcessManager {
|
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
async shutdown() {
|
|
301
|
+
// rc.38: mark "we're shutting down" so the proc.on('close') handler
|
|
302
|
+
// suppresses the misleading `resume-fail` event for signal-driven
|
|
303
|
+
// exits (SIGHUP from tmux pty close, SIGTERM from our own kill,
|
|
304
|
+
// SIGKILL from the kill-timeout escalator). Pre-rc.38 every deploy
|
|
305
|
+
// logged a `resume-fail` for every CLI-pm chat AND cleared the
|
|
306
|
+
// saved session_id, forcing a fresh resume on the next user turn
|
|
307
|
+
// — slower first turn, fresh context — for no real reason.
|
|
308
|
+
this._shuttingDown = true;
|
|
301
309
|
const keys = Array.from(this.procs.keys());
|
|
302
310
|
for (const key of keys) await this.kill(key);
|
|
303
311
|
}
|
|
@@ -542,7 +550,22 @@ class ProcessManager {
|
|
|
542
550
|
this.procs.delete(sessionKey);
|
|
543
551
|
// A slot freed up → maybe an LRU waiter can run now.
|
|
544
552
|
this._maybeSignalLruWaiter();
|
|
545
|
-
|
|
553
|
+
// rc.38: only fire `resume-fail` for UNEXPECTED non-zero exits.
|
|
554
|
+
// Signal-driven exits during planned shutdown (SIGHUP from tmux
|
|
555
|
+
// pty close on `tmux kill-session`, SIGTERM from our own kill(),
|
|
556
|
+
// SIGKILL from the kill-timeout escalator) are NOT resume
|
|
557
|
+
// failures — the saved session_id is still valid, we'd just be
|
|
558
|
+
// clearing it for nothing and logging misleading noise on every
|
|
559
|
+
// deploy. The real signal we care about is "the CLI rejected a
|
|
560
|
+
// stale or corrupt resume id at startup with a non-zero exit
|
|
561
|
+
// while polygram is healthy."
|
|
562
|
+
const isPlannedShutdown = this._shuttingDown
|
|
563
|
+
|| code === null // killed without an exit code
|
|
564
|
+
|| code === 129 // SIGHUP (tmux pty close on deploy kickstart)
|
|
565
|
+
|| code === 143 // SIGTERM (our own kill())
|
|
566
|
+
|| code === 137; // SIGKILL (kill-timeout escalation)
|
|
567
|
+
if (code !== 0 && ctx.existingSessionId && this.db?.clearSessionId
|
|
568
|
+
&& !isPlannedShutdown) {
|
|
546
569
|
this._logEvent('resume-fail', { session_key: sessionKey, session_id: ctx.existingSessionId, code });
|
|
547
570
|
try { this.db.clearSessionId(sessionKey); } catch (err) {
|
|
548
571
|
this.logger.error(`[${entry.label}] clearSessionId failed: ${err.message}`);
|
|
@@ -551,6 +574,16 @@ class ProcessManager {
|
|
|
551
574
|
if (this.onClose) this.onClose(sessionKey, code, entry);
|
|
552
575
|
});
|
|
553
576
|
|
|
577
|
+
// rc.38: stdin error listener. Async EIO writes (the kernel reports
|
|
578
|
+
// them after the subprocess pipe closed during shutdown) had no
|
|
579
|
+
// listener pre-rc.38 → bubbled to the global uncaughtException
|
|
580
|
+
// handler → emitted misleading `uncaught-exception: write EIO`
|
|
581
|
+
// events on every deploy. Listening swallows that path; runtime
|
|
582
|
+
// stdin errors (rare; usually a real problem) still log here.
|
|
583
|
+
proc.stdin?.on?.('error', (err) => {
|
|
584
|
+
this.logger.error(`[${entry.label}] stdin error: ${err.message}`);
|
|
585
|
+
});
|
|
586
|
+
|
|
554
587
|
proc.on('error', (err) => {
|
|
555
588
|
this.logger.error(`[${entry.label}] proc error: ${err.message}`);
|
|
556
589
|
entry.closed = true;
|
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.38",
|
|
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
|
@@ -2532,6 +2532,14 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2532
2532
|
// applyChain — so it serializes after any in-flight
|
|
2533
2533
|
// QUEUED apply and lands as the final visible reaction.
|
|
2534
2534
|
await reactor.setState('AUTOSTEERED');
|
|
2535
|
+
// rc.38: stop the reactor's STALL/TIMEOUT timers. Pre-rc.38
|
|
2536
|
+
// the timers stayed armed, holding setTimeout handles for
|
|
2537
|
+
// up to 30s and pinning the closure (and the bot/chatId
|
|
2538
|
+
// captures) until they fired. AUTOSTEERED is terminal — no
|
|
2539
|
+
// further state changes — so the timers serve no purpose
|
|
2540
|
+
// and just delay GC. One-line patch; small steady-state
|
|
2541
|
+
// heap relief in busy chats.
|
|
2542
|
+
reactor.stop();
|
|
2535
2543
|
markReplied();
|
|
2536
2544
|
return;
|
|
2537
2545
|
}
|
|
@@ -2868,6 +2876,17 @@ async function handleMessage(sessionKey, chatId, msg, bot) {
|
|
|
2868
2876
|
} finally {
|
|
2869
2877
|
stopTyping();
|
|
2870
2878
|
reactor.stop();
|
|
2879
|
+
// rc.38: defensive clear-on-exit for ✍ reactions. Pre-rc.38 only
|
|
2880
|
+
// the success path (line ~2622), the abort path (line ~2858), and
|
|
2881
|
+
// the tool-only-completion path (line ~2681) cleared
|
|
2882
|
+
// autosteeredRefs. The plain error path (`if (result.error)` →
|
|
2883
|
+
// throw at ~2612), the empty-response fallback failure (~2714),
|
|
2884
|
+
// and the streamer-overflow path could all leave ✍ reactions
|
|
2885
|
+
// stuck on follow-ups whose buffer entries had never been
|
|
2886
|
+
// drained by PostToolBatch. The clear is idempotent (the second
|
|
2887
|
+
// call returns 0 against an already-emptied map) so adding it
|
|
2888
|
+
// here covers ALL exit paths without double-clearing harm.
|
|
2889
|
+
clearAutosteeredReactions(sessionKey).catch(() => {});
|
|
2871
2890
|
}
|
|
2872
2891
|
}
|
|
2873
2892
|
|