polygram 0.10.0-rc.32 → 0.10.0-rc.34
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 +95 -16
- package/lib/sdk/callbacks.js +27 -4
- 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.34",
|
|
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",
|
|
@@ -726,13 +726,19 @@ class TmuxProcess extends Process {
|
|
|
726
726
|
// ~1-2KB → the claude TUI collapses it into a `[Pasted text #N]`
|
|
727
727
|
// placeholder whose single post-paste Enter can be absorbed
|
|
728
728
|
// mid-ingest, leaving the prompt UNSUBMITTED — the turn never
|
|
729
|
-
// starts. `
|
|
729
|
+
// starts. `_scheduleSubmitRetries` confirms the submit landed by
|
|
730
730
|
// waiting for this paste's correlation token to surface in a
|
|
731
731
|
// JSONL `user-message` (the ONLY reliable signal — capture-pane
|
|
732
732
|
// false-positives on the collapsed placeholder); it re-sends
|
|
733
733
|
// Enter on a miss and, after bounded retries, REJECTS with
|
|
734
734
|
// TMUX_SUBMIT_FAILED.
|
|
735
735
|
//
|
|
736
|
+
// 0.10.0 Commit 2: `_scheduleSubmitRetries` is `paste-parked`-
|
|
737
|
+
// aware. If the predicate observed our paste queued by a busy
|
|
738
|
+
// TUI (the C1 trace), it waits for the eventual user-message
|
|
739
|
+
// instead of re-sending Enter / failing loud. See the method
|
|
740
|
+
// doc.
|
|
741
|
+
//
|
|
736
742
|
// The confirm drives TWO derived promises below:
|
|
737
743
|
// submitConfirmP — rejects TMUX_SUBMIT_FAILED → a racer that
|
|
738
744
|
// fails the turn fast and loud; on success it never settles.
|
|
@@ -743,8 +749,10 @@ class TmuxProcess extends Process {
|
|
|
743
749
|
// prompt sitting unsubmitted reads as an idle (=="complete")
|
|
744
750
|
// pane, so the capture racer would win with TMUX_NO_JSONL_TEXT
|
|
745
751
|
// and mask the real TMUX_SUBMIT_FAILED cause.
|
|
752
|
+
// (submitConfirmP / submitOkP plumbing is retired in Commit 3's
|
|
753
|
+
// _runTurn race rewrite — kept here so Commit 2 stays surgical.)
|
|
746
754
|
const confirmP = turn.token
|
|
747
|
-
? this.
|
|
755
|
+
? this._scheduleSubmitRetries(turn.token, turn)
|
|
748
756
|
: Promise.resolve(); // no token — nothing to confirm
|
|
749
757
|
const submitConfirmP = confirmP.then(() => new Promise(() => {}));
|
|
750
758
|
const submitOkP = confirmP.then(() => true, () => new Promise(() => {}));
|
|
@@ -1021,6 +1029,13 @@ class TmuxProcess extends Process {
|
|
|
1021
1029
|
// without a terminal `result` (e.g. turnTimeoutMs) cannot leak
|
|
1022
1030
|
// its buffered message into turn N+1.
|
|
1023
1031
|
this._sessionLogTail?.flushParser?.();
|
|
1032
|
+
// Commit 2: clear any lingering submit-confirm waiter for this
|
|
1033
|
+
// turn's token. The parked branch of `_scheduleSubmitRetries`
|
|
1034
|
+
// races the turn's own settle promises so it normally self-cleans,
|
|
1035
|
+
// but a turn that ends via the hard W1 deadline (turnDeadlineP
|
|
1036
|
+
// rejects in `_runTurn`, never resolving `resultPromise`) would
|
|
1037
|
+
// otherwise leave a dangling Map entry. Defensive + cheap.
|
|
1038
|
+
if (turn?.token) this._submitConfirms.delete(turn.token);
|
|
1024
1039
|
const qi = this.pendingQueue.indexOf(turn);
|
|
1025
1040
|
if (qi >= 0) this.pendingQueue.splice(qi, 1);
|
|
1026
1041
|
this._dropFromActiveGroup(turn);
|
|
@@ -1674,7 +1689,7 @@ class TmuxProcess extends Process {
|
|
|
1674
1689
|
this._confirmPaste(tokens);
|
|
1675
1690
|
// B7: a user-message is the proof that a primary paste actually
|
|
1676
1691
|
// STARTED a turn (claude registered the prompt). Release any
|
|
1677
|
-
//
|
|
1692
|
+
// _scheduleSubmitRetries waiter for these tokens.
|
|
1678
1693
|
this._confirmSubmit(tokens);
|
|
1679
1694
|
let matched = [];
|
|
1680
1695
|
for (const tok of tokens) {
|
|
@@ -1829,7 +1844,7 @@ class TmuxProcess extends Process {
|
|
|
1829
1844
|
* TUI into a `[Pasted text #N]` placeholder whose single post-paste
|
|
1830
1845
|
* Enter can be absorbed mid-ingest, leaving the prompt unsubmitted —
|
|
1831
1846
|
* but that submit-confirmation runs as a concurrent racer in
|
|
1832
|
-
* `_runTurn` (`
|
|
1847
|
+
* `_runTurn` (`_scheduleSubmitRetries`), NOT here. Blocking
|
|
1833
1848
|
* `_pasteAndEnter` on the confirm would hold `_pasteLock` across the
|
|
1834
1849
|
* whole confirm window and stall every following paste — an autosteer
|
|
1835
1850
|
* that should fold into the primary turn could never paste.
|
|
@@ -1843,7 +1858,7 @@ class TmuxProcess extends Process {
|
|
|
1843
1858
|
// (it false-positived on `[Pasted text #N]`). The runner just
|
|
1844
1859
|
// pastes + Enter. Submit confirmation for a PRIMARY turn is
|
|
1845
1860
|
// JSONL-token-based and runs as a CONCURRENT racer in `_runTurn`
|
|
1846
|
-
// (`
|
|
1861
|
+
// (`_scheduleSubmitRetries`) — NOT here. Blocking `_pasteAndEnter`
|
|
1847
1862
|
// on the confirm would hold `_pasteLock` across the whole confirm
|
|
1848
1863
|
// window and stall every following paste (an autosteer that
|
|
1849
1864
|
// SHOULD fold into the primary turn could never paste). The
|
|
@@ -1869,17 +1884,33 @@ class TmuxProcess extends Process {
|
|
|
1869
1884
|
}
|
|
1870
1885
|
|
|
1871
1886
|
/**
|
|
1872
|
-
*
|
|
1887
|
+
* Confirm a primary paste actually submitted by waiting for its
|
|
1873
1888
|
* correlation `token` to surface in a JSONL `user-message`. On each
|
|
1874
1889
|
* miss, re-send Enter (the prior Enter was absorbed by the TUI's
|
|
1875
1890
|
* bracketed-paste ingest of a `[Pasted text #N]` block). After
|
|
1876
1891
|
* `submitConfirmRetries` exhausted misses, throw `TMUX_SUBMIT_FAILED`.
|
|
1877
1892
|
*
|
|
1878
|
-
*
|
|
1879
|
-
*
|
|
1880
|
-
*
|
|
1881
|
-
*
|
|
1882
|
-
*
|
|
1893
|
+
* 0.10.0 Commit 2 — `paste-parked`-aware (the C1 fix). The B7
|
|
1894
|
+
* predecessor (`_confirmSubmitViaJsonl`) re-sent Enter on every miss
|
|
1895
|
+
* and failed loud after 5, with NO way to tell "the Enter was
|
|
1896
|
+
* absorbed, the prompt is stuck" (genuine submit failure) apart from
|
|
1897
|
+
* "the TUI was busy and legitimately PARKED the paste in its queue"
|
|
1898
|
+
* (a paste that WILL submit when the prior turn finishes). The
|
|
1899
|
+
* 2026-05-20 C1 trace was the latter failing loud: a paste the TUI
|
|
1900
|
+
* queued got 5 spurious Enter re-sends then `TMUX_SUBMIT_FAILED`.
|
|
1901
|
+
*
|
|
1902
|
+
* The turn-phase predicate now distinguishes them: a
|
|
1903
|
+
* `queue-operation enqueue` carrying THIS turn's `corr-id` (or the
|
|
1904
|
+
* `Press up to edit queued messages` capture-pane fallback) sets
|
|
1905
|
+
* `turn.parked = true`. Once parked:
|
|
1906
|
+
* - STOP re-sending Enter — the paste is in the TUI queue; another
|
|
1907
|
+
* Enter could submit a DIFFERENT queued item or double-submit.
|
|
1908
|
+
* - Do NOT fail loud — the turn is legitimately in flight.
|
|
1909
|
+
* - Wait (unbounded here) for the eventual `user-message`. The
|
|
1910
|
+
* `_runTurn` turn deadline (W1) is the only floor; a paste that
|
|
1911
|
+
* is truly never released fails as `TMUX_TURN_TIMEOUT` (correct
|
|
1912
|
+
* attribution — the wedged thing is the prior turn, not our
|
|
1913
|
+
* submission), not `TMUX_SUBMIT_FAILED`.
|
|
1883
1914
|
*
|
|
1884
1915
|
* Runs as a concurrent racer in `_runTurn` (NOT a blocking gate in
|
|
1885
1916
|
* `_pasteAndEnter` — that would hold `_pasteLock` across the confirm
|
|
@@ -1888,17 +1919,37 @@ class TmuxProcess extends Process {
|
|
|
1888
1919
|
* racer already won, or the turn was killed) the retry loop bails so
|
|
1889
1920
|
* a stray retry Enter cannot land in an unrelated turn.
|
|
1890
1921
|
*/
|
|
1891
|
-
async
|
|
1922
|
+
async _scheduleSubmitRetries(token, turn = null) {
|
|
1892
1923
|
for (let attempt = 0; attempt <= this.submitConfirmRetries; attempt += 1) {
|
|
1924
|
+
// C1: parked → the paste is safely queued in the TUI. Wait for
|
|
1925
|
+
// the eventual user-message; never re-send Enter, never fail
|
|
1926
|
+
// loud. Checked at the TOP so a paste parked before the first
|
|
1927
|
+
// confirm-wait skips the wait entirely.
|
|
1928
|
+
if (turn && turn.parked) {
|
|
1929
|
+
this.emit('submit-parked', {
|
|
1930
|
+
token,
|
|
1931
|
+
turnId: turn.turnId,
|
|
1932
|
+
attempt,
|
|
1933
|
+
sessionId: this.claudeSessionId,
|
|
1934
|
+
backend: 'tmux',
|
|
1935
|
+
});
|
|
1936
|
+
await this._awaitSubmitOrTerminal(token, turn);
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1893
1939
|
const confirmed = await this._awaitSubmitConfirm(token);
|
|
1894
1940
|
if (confirmed) return; // submitted ✓
|
|
1895
1941
|
// The turn already settled some other way (result/capture/kill)
|
|
1896
1942
|
// — the submit clearly is no longer the open question. Stop:
|
|
1897
1943
|
// re-sending Enter or throwing now would be wrong.
|
|
1898
1944
|
if (turn && (turn.state === 'done' || turn.state === 'failed')) return;
|
|
1945
|
+
// The enqueue may have landed DURING the submitConfirmMs wait —
|
|
1946
|
+
// re-check before deciding to re-send Enter. The loop top then
|
|
1947
|
+
// handles the parked branch.
|
|
1948
|
+
if (turn && turn.parked) continue;
|
|
1899
1949
|
if (attempt === this.submitConfirmRetries) break; // out of retries
|
|
1900
|
-
// The tokened user-message never arrived
|
|
1901
|
-
// sitting in the input box as
|
|
1950
|
+
// The tokened user-message never arrived AND the paste was not
|
|
1951
|
+
// parked — the prompt is still sitting in the input box as
|
|
1952
|
+
// `[Pasted text #N]`. Re-send Enter.
|
|
1902
1953
|
this.logger.debug?.(
|
|
1903
1954
|
`[${this.label}] paste not submitted (no user-message for ${token}), `
|
|
1904
1955
|
+ `re-sending Enter (attempt ${attempt + 1})`,
|
|
@@ -1920,6 +1971,34 @@ class TmuxProcess extends Process {
|
|
|
1920
1971
|
);
|
|
1921
1972
|
}
|
|
1922
1973
|
|
|
1974
|
+
/**
|
|
1975
|
+
* Parked-branch wait (Commit 2): resolve when `token` surfaces in a
|
|
1976
|
+
* JSONL `user-message` (submit landed), or when the owning turn goes
|
|
1977
|
+
* terminal another way (result flushed / interrupted / killed). NO
|
|
1978
|
+
* timeout — the caller's `_runTurn` turn deadline (W1) is the floor.
|
|
1979
|
+
*
|
|
1980
|
+
* Racing the turn's own settle promises prevents a leaked
|
|
1981
|
+
* `_submitConfirms` entry on a turn that ends without ever
|
|
1982
|
+
* producing our user-message (e.g. the prior turn wedges and W1
|
|
1983
|
+
* fires).
|
|
1984
|
+
*/
|
|
1985
|
+
_awaitSubmitOrTerminal(token, turn) {
|
|
1986
|
+
return new Promise((resolve) => {
|
|
1987
|
+
let done = false;
|
|
1988
|
+
const finish = () => {
|
|
1989
|
+
if (done) return;
|
|
1990
|
+
done = true;
|
|
1991
|
+
this._submitConfirms.delete(token);
|
|
1992
|
+
resolve();
|
|
1993
|
+
};
|
|
1994
|
+
this._submitConfirms.set(token, finish); // user-message → finish
|
|
1995
|
+
// Bail if the turn settles via result / interrupt before the
|
|
1996
|
+
// user-message lands.
|
|
1997
|
+
turn?.resultPromise?.then(finish, finish);
|
|
1998
|
+
turn?.interruptP?.then(finish, finish);
|
|
1999
|
+
});
|
|
2000
|
+
}
|
|
2001
|
+
|
|
1923
2002
|
/**
|
|
1924
2003
|
* Resolve `true` once `token` surfaces in a JSONL `user-message`
|
|
1925
2004
|
* (via `_confirmSubmit`), or `false` after `submitConfirmMs`.
|
|
@@ -2663,12 +2742,12 @@ class TmuxProcess extends Process {
|
|
|
2663
2742
|
try { finish(); } catch { /* swallow */ }
|
|
2664
2743
|
}
|
|
2665
2744
|
// B7: release any pending submit-confirm waiters too — a
|
|
2666
|
-
// `
|
|
2745
|
+
// `_scheduleSubmitRetries` blocked on a tokened user-message from a
|
|
2667
2746
|
// now-dead session would otherwise burn its whole retry budget.
|
|
2668
2747
|
// Each waiter's stored fn resolves it as confirmed, so the confirm
|
|
2669
2748
|
// loop returns at once instead of retrying; the in-flight turn is
|
|
2670
2749
|
// already rejected by `drainQueue` above, so the turn settles loud
|
|
2671
|
-
// regardless. (`
|
|
2750
|
+
// regardless. (`_scheduleSubmitRetries` also bails on its own when
|
|
2672
2751
|
// the owning turn reaches a terminal state — this is belt-and-
|
|
2673
2752
|
// braces for a confirm whose turn ref it never received.)
|
|
2674
2753
|
for (const finish of [...this._submitConfirms.values()]) {
|
package/lib/sdk/callbacks.js
CHANGED
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
|
|
21
21
|
'use strict';
|
|
22
22
|
|
|
23
|
+
const { getTopicConfig } = require('../session-key');
|
|
24
|
+
const { pickBackend } = require('../process/factory');
|
|
25
|
+
|
|
23
26
|
function createSdkCallbacks({
|
|
24
27
|
db,
|
|
25
28
|
dbWrite,
|
|
@@ -97,15 +100,35 @@ function createSdkCallbacks({
|
|
|
97
100
|
|
|
98
101
|
return {
|
|
99
102
|
onInit: (sessionKey, event, entry) => {
|
|
103
|
+
// Resolve the spawn-time identity the SAME way the backends do
|
|
104
|
+
// (topic override merged over chat-level + factory's
|
|
105
|
+
// pickBackend) — must match what `buildSpawnContext` in
|
|
106
|
+
// polygram.js compares against, otherwise every spawn re-poisons
|
|
107
|
+
// the row with chat-level values and S2 drift fires forever.
|
|
108
|
+
//
|
|
109
|
+
// The shumorobot 2026-05-21 Music topic bug was this: chat-
|
|
110
|
+
// level agent='shumabit' + cwd=$HOME got written into the row
|
|
111
|
+
// every turn, but the topic-level resolved to
|
|
112
|
+
// music-curation:music-curator + .../Music/rekordbox. Next turn
|
|
113
|
+
// → drift → drop row → fresh sid → context lost. Forever.
|
|
114
|
+
//
|
|
115
|
+
// pm_backend MUST also be persisted explicitly; otherwise
|
|
116
|
+
// db.upsertSession defaults it to 'sdk' for every spawn,
|
|
117
|
+
// making historical telemetry meaningless.
|
|
118
|
+
const chatConfig = config.chats[entry.chatId] || {};
|
|
119
|
+
const topicConfig = getTopicConfig(chatConfig, entry.threadId || null);
|
|
100
120
|
dbWrite(() => db.upsertSession({
|
|
101
121
|
session_key: sessionKey,
|
|
102
122
|
chat_id: entry.chatId,
|
|
103
123
|
thread_id: entry.threadId,
|
|
104
124
|
claude_session_id: event.session_id,
|
|
105
|
-
agent:
|
|
106
|
-
cwd:
|
|
107
|
-
model:
|
|
108
|
-
effort:
|
|
125
|
+
agent: topicConfig.agent || chatConfig.agent || null,
|
|
126
|
+
cwd: topicConfig.cwd || chatConfig.cwd || null,
|
|
127
|
+
model: topicConfig.model || chatConfig.model || null,
|
|
128
|
+
effort: topicConfig.effort || chatConfig.effort || null,
|
|
129
|
+
pm_backend: pickBackend({
|
|
130
|
+
config, chatId: entry.chatId, threadId: entry.threadId || null,
|
|
131
|
+
}),
|
|
109
132
|
}), `upsert session ${sessionKey}`);
|
|
110
133
|
},
|
|
111
134
|
|
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.34",
|
|
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": {
|