polygram 0.10.0-rc.13 → 0.10.0-rc.14

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.
@@ -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.13",
4
+ "version": "0.10.0-rc.14",
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",
@@ -387,14 +387,18 @@ class TmuxProcess extends Process {
387
387
  try {
388
388
  // R2-F1: sanitization happens inside runner.pasteText; we also
389
389
  // log when chars get stripped.
390
- const result = await this.runner.pasteText(this.tmuxName, prompt);
390
+ // rc.13.1: pasteAndEnter holds a per-session async lock around
391
+ // paste + Enter so a concurrent injectUserMessage paste can't
392
+ // interleave keystrokes with this primary turn's prompt
393
+ // (shumorobot 2026-05-15 saw msg 696's prompt truncated at
394
+ // `chat_id="-1003` by msg 698's autosteer paste cutting in).
395
+ const result = await this._pasteAndEnter(prompt);
391
396
  if (result.stripped > 0) {
392
397
  this.logger.warn?.(
393
398
  `[${this.label}] stripped ${result.stripped} control chars from prompt`,
394
399
  );
395
400
  this.emit('prompt-sanitized', { stripped: result.stripped, source: 'send' });
396
401
  }
397
- await this.runner.sendControl(this.tmuxName, 'Enter');
398
402
 
399
403
  // Race: JSONL result event vs capture-pane quiescence fallback
400
404
  // vs hard timeout. JSONL is the primary signal (carries structured
@@ -814,6 +818,31 @@ class TmuxProcess extends Process {
814
818
  }
815
819
  }
816
820
 
821
+ /**
822
+ * rc.13.1: paste a prompt body AND press Enter as an atomic
823
+ * per-session operation. The production runner provides
824
+ * `pasteAndEnter(name, text)` which holds a per-session async lock
825
+ * across the paste+Enter pair so concurrent pasteText calls from
826
+ * a primary pm.send and a parallel injectUserMessage cannot
827
+ * interleave keystrokes in the TUI input box (root cause of the
828
+ * shumorobot 2026-05-15 reply-mis-attribution bug: msg 696's
829
+ * paste was truncated at `chat_id="-1003` when msg 698's autosteer
830
+ * paste cut in).
831
+ *
832
+ * Test runners without pasteAndEnter fall back to the sequential
833
+ * pasteText + sendControl(Enter) pair. Behaviour-equivalent for
834
+ * non-concurrent test scenarios; only production with a real tmux
835
+ * + concurrent injects exposes the race.
836
+ */
837
+ async _pasteAndEnter(text) {
838
+ if (typeof this.runner.pasteAndEnter === 'function') {
839
+ return this.runner.pasteAndEnter(this.tmuxName, text);
840
+ }
841
+ const result = await this.runner.pasteText(this.tmuxName, text);
842
+ await this.runner.sendControl(this.tmuxName, 'Enter');
843
+ return result;
844
+ }
845
+
817
846
  // ─── completion detection (§4.A capture-pane diff path — fallback) ──
818
847
 
819
848
  /**
@@ -1207,9 +1236,13 @@ class TmuxProcess extends Process {
1207
1236
  // intended extra-turn-reply path with reply_to=msg 686).
1208
1237
  const oneLine = safe.replace(/\r?\n/g, ' / ');
1209
1238
 
1210
- Promise.resolve()
1211
- .then(() => this.runner.pasteText(this.tmuxName, safe))
1212
- .then(() => this.runner.sendControl(this.tmuxName, 'Enter'))
1239
+ // rc.13.1: use pasteAndEnter so the autosteer's paste+Enter pair
1240
+ // serialises behind any in-flight pm.send paste+Enter. Without
1241
+ // the lock, the autosteer paste could interleave keystrokes into
1242
+ // the middle of a primary turn's prompt (caught on shumorobot
1243
+ // 2026-05-15: msg 696's prompt got truncated mid-attribute when
1244
+ // msg 698's autosteer paste cut in).
1245
+ this._pasteAndEnter(safe)
1213
1246
  .catch((err) => this.emit('inject-fail', { err: err.message }));
1214
1247
 
1215
1248
  // Tell the next assistant-chunk to open a fresh Telegram bubble
@@ -32,6 +32,7 @@ const childProcess = require('child_process');
32
32
  const crypto = require('crypto');
33
33
  const fs = require('fs');
34
34
  const path = require('path');
35
+ const { createAsyncLock } = require('../async-lock');
35
36
 
36
37
  // ─── Constants ───────────────────────────────────────────────────────
37
38
 
@@ -196,6 +197,32 @@ function createTmuxRunner({ logger = console, runFn = run } = {}) {
196
197
  await runFn('tmux', ['send-keys', '-t', name, key]);
197
198
  }
198
199
 
200
+ // rc.13.1: paste+Enter must be ATOMIC per session. Pre-rc.13.1 two
201
+ // concurrent pasteText+sendControl pairs could interleave in the
202
+ // TUI's bracketed-paste buffer — Ivan caught this on shumorobot
203
+ // 2026-05-15 (the 2233-char user JSONL entry contained one
204
+ // truncated polygram channel + a full nested polygram prompt for
205
+ // a different msg_id). Symptom: msg 696's paste was at byte
206
+ // `chat_id="-1003` when msg 698's autosteer paste cut in,
207
+ // concatenating two pastes into one TUI user message → the agent
208
+ // saw a malformed input → the reply attribution went sideways
209
+ // (msg 697 got msg 698's answer, msg 696 got served last).
210
+ //
211
+ // The async-lock is keyed by tmux session name, so different
212
+ // sessions don't block each other. Within one session, pasteText
213
+ // + sendControl(Enter) hold the lock atomically.
214
+ const inputLock = createAsyncLock();
215
+ async function pasteAndEnter(name, text) {
216
+ const release = await inputLock.acquire(name);
217
+ try {
218
+ const res = await pasteText(name, text);
219
+ await sendControl(name, 'Enter');
220
+ return res;
221
+ } finally {
222
+ release();
223
+ }
224
+ }
225
+
199
226
  /**
200
227
  * Push a multi-line text prompt into the pane.
201
228
  *
@@ -293,6 +320,7 @@ function createTmuxRunner({ logger = console, runFn = run } = {}) {
293
320
  spawn,
294
321
  sendControl,
295
322
  pasteText,
323
+ pasteAndEnter,
296
324
  capturePane,
297
325
  captureWide,
298
326
  sessionExists,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polygram",
3
- "version": "0.10.0-rc.13",
3
+ "version": "0.10.0-rc.14",
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": {