polygram 0.10.0-rc.26 → 0.10.0-rc.27
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/tmux-process.js +124 -28
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "polygram",
|
|
4
|
-
"version": "0.10.0-rc.
|
|
4
|
+
"version": "0.10.0-rc.27",
|
|
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",
|
|
@@ -174,6 +174,20 @@ const DEFAULT_TURN_TIMEOUT_MS = 5 * 60_000;
|
|
|
174
174
|
const DEFAULT_POLL_MS = 250;
|
|
175
175
|
const DEFAULT_QUIESCE_MS = 500; // require READY for this long before declaring done
|
|
176
176
|
|
|
177
|
+
// B8 (slow-MCP readiness): how long the claude `--debug-file` log must
|
|
178
|
+
// have had NO new bytes appended before the startup is considered
|
|
179
|
+
// quiescent. During MCP cold-start the debug log is DENSELY written —
|
|
180
|
+
// the production log shows ~33 s of `MCP server "X": connecting/…
|
|
181
|
+
// connected` lines, then total silence once the TUI is idle. A genuine
|
|
182
|
+
// idle TUI's debug log is quiet for minutes. 1 s is comfortably longer
|
|
183
|
+
// than the gap between two consecutive MCP-startup log writes (verified
|
|
184
|
+
// against the production debug log) yet short enough to add only ~1 s
|
|
185
|
+
// to a clean startup. Used ONLY by `_waitForReady`, scoped to the
|
|
186
|
+
// startup wait — never reused mid-turn (the debug log keeps being
|
|
187
|
+
// written during a turn; quiescence-of-the-whole-log would wrongly
|
|
188
|
+
// block, but `_waitForReady` runs only at startup before any turn).
|
|
189
|
+
const DEFAULT_READY_DEBUG_QUIET_MS = 1000;
|
|
190
|
+
|
|
177
191
|
// R7: sentinel returned by _awaitTurnComplete when its poll loop is
|
|
178
192
|
// stopped by the caller's absolute-deadline abort (rather than by a
|
|
179
193
|
// real READY quiescence or its own internal timeout). _runTurn maps
|
|
@@ -197,6 +211,9 @@ class TmuxProcess extends Process {
|
|
|
197
211
|
* @param {number} [opts.turnTimeoutMs]
|
|
198
212
|
* @param {number} [opts.pollMs]
|
|
199
213
|
* @param {number} [opts.quiesceMs]
|
|
214
|
+
* @param {number} [opts.readyDebugQuietMs] — B8: `_waitForReady`
|
|
215
|
+
* requires the claude `--debug-file` log to have had no new bytes
|
|
216
|
+
* for this long (in addition to pane stability + ready hint).
|
|
200
217
|
*/
|
|
201
218
|
constructor({
|
|
202
219
|
sessionKey, chatId, threadId, label,
|
|
@@ -216,6 +233,14 @@ class TmuxProcess extends Process {
|
|
|
216
233
|
// presses before giving up loud.
|
|
217
234
|
submitConfirmMs = 1500,
|
|
218
235
|
submitConfirmRetries = 4,
|
|
236
|
+
// B8: `_waitForReady` gates startup on the claude `--debug-file`
|
|
237
|
+
// log going quiet (no new bytes for this long) — the signal that
|
|
238
|
+
// is NOT fooled by a byte-stable-but-still-loading TUI pane.
|
|
239
|
+
readyDebugQuietMs = DEFAULT_READY_DEBUG_QUIET_MS,
|
|
240
|
+
// Test seam: a fake `fs` forwarded to the readiness debug-log tail
|
|
241
|
+
// so a unit test can drive debug-log writes deterministically
|
|
242
|
+
// without touching the real filesystem.
|
|
243
|
+
fs: fsOverride = null,
|
|
219
244
|
} = {}) {
|
|
220
245
|
super({ sessionKey, chatId, threadId, label });
|
|
221
246
|
if (!runner) throw new TypeError('TmuxProcess: runner required');
|
|
@@ -241,6 +266,8 @@ class TmuxProcess extends Process {
|
|
|
241
266
|
this.turnTimeoutMs = turnTimeoutMs;
|
|
242
267
|
this.pollMs = pollMs;
|
|
243
268
|
this.quiesceMs = quiesceMs;
|
|
269
|
+
this.readyDebugQuietMs = readyDebugQuietMs;
|
|
270
|
+
this._fsOverride = fsOverride;
|
|
244
271
|
this.lateGraceMs = lateGraceMs;
|
|
245
272
|
this.queueCap = queueCap;
|
|
246
273
|
// Optional shared poll scheduler. When provided, the polling
|
|
@@ -1573,43 +1600,95 @@ class TmuxProcess extends Process {
|
|
|
1573
1600
|
return this._sleep(this.pollMs);
|
|
1574
1601
|
}
|
|
1575
1602
|
|
|
1603
|
+
/**
|
|
1604
|
+
* Synchronously probe the byte-size of the claude `--debug-file` log.
|
|
1605
|
+
* Returns the size in bytes, or `null` if the log does not exist yet
|
|
1606
|
+
* (claude takes ~100 ms to create it after spawn) or cannot be
|
|
1607
|
+
* stat'd. Never throws — a readiness gate must not hard-fail on a
|
|
1608
|
+
* missing debug-log signal.
|
|
1609
|
+
*
|
|
1610
|
+
* B8: this is the size-delta channel `_waitForReady` uses to detect
|
|
1611
|
+
* debug-log quiescence — the signal a byte-stable-but-still-loading
|
|
1612
|
+
* TUI pane cannot fool. During MCP cold-start claude DENSELY appends
|
|
1613
|
+
* to this log (`MCP server "X": connecting…` / `…connected in
|
|
1614
|
+
* NNNNms`); once the TUI is genuinely idle the log goes silent. This
|
|
1615
|
+
* mirrors `LogTail`'s approach (stat-based size-delta detection,
|
|
1616
|
+
* ENOENT-tolerant) without arming a second async tailer: the
|
|
1617
|
+
* readiness loop already polls on a timer, so a synchronous
|
|
1618
|
+
* `statSync` per poll is the simplest, fully-deterministic fit and
|
|
1619
|
+
* needs no fs.watch.
|
|
1620
|
+
*
|
|
1621
|
+
* `this._fsOverride` is a test seam — a fake `fs` lets a unit test
|
|
1622
|
+
* drive debug-log growth by hand.
|
|
1623
|
+
*/
|
|
1624
|
+
_probeDebugLogSize() {
|
|
1625
|
+
const fsImpl = this._fsOverride || require('fs');
|
|
1626
|
+
try {
|
|
1627
|
+
return fsImpl.statSync(this.debugLogPath).size;
|
|
1628
|
+
} catch {
|
|
1629
|
+
return null; // ENOENT (not created yet) / any stat error
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1576
1633
|
async _waitForReady() {
|
|
1577
1634
|
const deadline = this._now() + this.readyTimeoutMs;
|
|
1578
1635
|
let lastBuf = '';
|
|
1579
1636
|
// B6 (shumorobot 2026-05-18, Music topic, twice): a slow
|
|
1580
1637
|
// custom-agent spawn (`music-curation:music-curator` loading
|
|
1581
1638
|
// several MCP servers) leaves the claude TUI mid-startup for
|
|
1582
|
-
// SECONDS
|
|
1583
|
-
//
|
|
1584
|
-
//
|
|
1585
|
-
//
|
|
1586
|
-
//
|
|
1587
|
-
//
|
|
1639
|
+
// SECONDS. Throughout that window the TUI ALREADY renders its
|
|
1640
|
+
// ready hint (`? for shortcuts` / `bypass permissions on`) at the
|
|
1641
|
+
// bottom of its startup banner. The old `_waitForReady` returned
|
|
1642
|
+
// the INSTANT `READY_HINTS_RE` matched — on the first poll, while
|
|
1643
|
+
// MCP servers were still loading — so the first `send()` pasted
|
|
1644
|
+
// into a not-yet-ready TUI and the submitted Enter was dropped.
|
|
1588
1645
|
//
|
|
1589
|
-
//
|
|
1590
|
-
//
|
|
1591
|
-
// loading. `start()` resolved early; the first `send()` pasted
|
|
1592
|
-
// the prompt into a TUI still ingesting startup, and the
|
|
1593
|
-
// submitted Enter was dropped → the prompt sat unsubmitted → the
|
|
1594
|
-
// turn never began (a fake THINKING→STALL followed). On a
|
|
1595
|
-
// no-agent TUI the startup window is sub-poll so it never bit; a
|
|
1596
|
-
// slow custom-agent spawn opens a multi-second window every time.
|
|
1646
|
+
// B6's fix gated on pane QUIESCENCE: ready ⇔ the hint is present
|
|
1647
|
+
// AND the `capture-pane` is byte-stable across consecutive polls.
|
|
1597
1648
|
//
|
|
1598
|
-
//
|
|
1599
|
-
//
|
|
1600
|
-
//
|
|
1601
|
-
//
|
|
1602
|
-
//
|
|
1603
|
-
//
|
|
1649
|
+
// B8 (slow-MCP-startup, 2026-05-19): pane quiescence is NOT
|
|
1650
|
+
// enough. The production debug log for the Music topic shows MCP
|
|
1651
|
+
// cold-start spanning ~33 s (`plugin:serena:serena` 27.5 s,
|
|
1652
|
+
// peekaboo 9.3 s, …) — yet across that whole window the claude
|
|
1653
|
+
// pane is BYTE-STABLE: the REPL mounts and paints its ready hint
|
|
1654
|
+
// immediately, then MCP servers load entirely off-screen with the
|
|
1655
|
+
// pane unchanged. B6 reads "stable = ready", `start()` resolves
|
|
1656
|
+
// mid-MCP-load, the paste lands in a TUI that is not yet
|
|
1657
|
+
// interactive, and the Enter is dropped (the Music-topic break,
|
|
1658
|
+
// 5+ times). `isolateUserConfig` (rc.26) removes MCP servers for
|
|
1659
|
+
// the Music topic specifically, but the gate is still wrong for
|
|
1660
|
+
// any non-isolated topic that legitimately loads MCP servers.
|
|
1604
1661
|
//
|
|
1605
|
-
//
|
|
1606
|
-
//
|
|
1607
|
-
//
|
|
1608
|
-
//
|
|
1609
|
-
//
|
|
1610
|
-
//
|
|
1662
|
+
// The pane is fooled; the claude `--debug-file` log is NOT. During
|
|
1663
|
+
// MCP startup that log is ACTIVELY written; a genuinely-ready idle
|
|
1664
|
+
// TUI's debug log is quiet (verified against the production log —
|
|
1665
|
+
// dense writes for ~33 s, then silence for over an hour). So
|
|
1666
|
+
// `_waitForReady` now ALSO gates on debug-log quiescence:
|
|
1667
|
+
// ready ⇔ (ready hint present)
|
|
1668
|
+
// AND (pane byte-stable across consecutive polls)
|
|
1669
|
+
// AND (the --debug-file log has had no new bytes for
|
|
1670
|
+
// `readyDebugQuietMs`).
|
|
1671
|
+
// During MCP load the debug log is active → not ready. After load
|
|
1672
|
+
// → quiet → ready. Still bounded by `readyTimeoutMs`, so a
|
|
1673
|
+
// genuinely wedged spawn still throws TMUX_READY_TIMEOUT.
|
|
1674
|
+
//
|
|
1675
|
+
// Scope: the debug log keeps being written DURING normal turns, so
|
|
1676
|
+
// whole-log quiescence would wrongly block mid-turn — but
|
|
1677
|
+
// `_waitForReady` runs ONLY at startup, before any turn, so
|
|
1678
|
+
// startup-phase quiescence is exactly the right window. The
|
|
1679
|
+
// `readyDebugQuietMs` clock lives entirely inside this method and
|
|
1680
|
+
// is never consulted by `_awaitTurnComplete`.
|
|
1611
1681
|
let readySinceAt = null; // when the (hint + stable-pane) state began
|
|
1612
1682
|
let prevBuf = null; // last poll's capture, for the stability compare
|
|
1683
|
+
// B8 debug-log quiescence tracking. `prevDebugSize` is the
|
|
1684
|
+
// `--debug-file` byte-size seen on the PREVIOUS poll; `lastGrowthAt`
|
|
1685
|
+
// is when the size last increased. The debug log is "quiet" once it
|
|
1686
|
+
// has not grown for `readyDebugQuietMs`. The FIRST non-null size is
|
|
1687
|
+
// a baseline (no growth recorded for it) — claude actively writing
|
|
1688
|
+
// its MCP-startup burst makes the size keep climbing across the
|
|
1689
|
+
// next polls, which is what resets the clock.
|
|
1690
|
+
let prevDebugSize = null;
|
|
1691
|
+
let lastGrowthAt = null;
|
|
1613
1692
|
if (this.pollScheduler) this.pollScheduler.acquire();
|
|
1614
1693
|
try {
|
|
1615
1694
|
while (this._now() < deadline) {
|
|
@@ -1624,11 +1703,28 @@ class TmuxProcess extends Process {
|
|
|
1624
1703
|
// least two matching captures, which is the point.
|
|
1625
1704
|
const hintPresent = READY_HINTS_RE.test(lastBuf);
|
|
1626
1705
|
const paneStable = prevBuf !== null && lastBuf === prevBuf;
|
|
1627
|
-
|
|
1706
|
+
// B8: the --debug-file log must have stopped growing for
|
|
1707
|
+
// `readyDebugQuietMs`. A debug-log size increase since the last
|
|
1708
|
+
// poll = claude is still writing (MCP servers connecting) → not
|
|
1709
|
+
// quiet. The log never appearing at all (null size for the whole
|
|
1710
|
+
// wait — no MCP startup observed) reads as quiet, so the B6 pane
|
|
1711
|
+
// check still gates a no-agent / fast spawn.
|
|
1712
|
+
const debugSize = this._probeDebugLogSize();
|
|
1713
|
+
if (debugSize !== null && prevDebugSize !== null
|
|
1714
|
+
&& debugSize > prevDebugSize) {
|
|
1715
|
+
lastGrowthAt = this._now(); // log grew → claude still writing
|
|
1716
|
+
}
|
|
1717
|
+
if (debugSize !== null) prevDebugSize = debugSize;
|
|
1718
|
+
const debugQuiet = lastGrowthAt === null
|
|
1719
|
+
|| (this._now() - lastGrowthAt) >= this.readyDebugQuietMs;
|
|
1720
|
+
if (hintPresent && paneStable && debugQuiet) {
|
|
1628
1721
|
if (readySinceAt == null) readySinceAt = this._now();
|
|
1629
1722
|
if (this._now() - readySinceAt >= this.quiesceMs) return;
|
|
1630
1723
|
} else {
|
|
1631
|
-
|
|
1724
|
+
// pane moved / hint gone / debug log still being written →
|
|
1725
|
+
// reset the clock. A debug-log write during the quiesce
|
|
1726
|
+
// window means MCP startup is not finished.
|
|
1727
|
+
readySinceAt = null;
|
|
1632
1728
|
}
|
|
1633
1729
|
prevBuf = lastBuf;
|
|
1634
1730
|
await this._waitForNextTick();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.10.0-rc.
|
|
3
|
+
"version": "0.10.0-rc.27",
|
|
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": {
|