dsh-tg-bridge 1.2.1 → 1.2.3

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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/bridge.mjs +59 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -157,6 +157,7 @@ Telegram → bridge.mjs → dsh --profile headless --json → ответ
157
157
  - [docs/ROADMAP.md](docs/ROADMAP.md) — что дальше
158
158
  - [docs/UX-ANALYSIS.md](docs/UX-ANALYSIS.md) — разбор интерактива
159
159
  - [docs/ACCEPTANCE.md](docs/ACCEPTANCE.md) — критерии приёмки
160
+ - [docs/COST.md](docs/COST.md) — сколько стоит один ход и как это уменьшить
160
161
  - [docs/CHANGELOG.md](docs/CHANGELOG.md) — история изменений
161
162
  - [docs/RELEASE-CRITERIA.md](docs/RELEASE-CRITERIA.md) — критерии готовности релиза
162
163
 
package/bridge.mjs CHANGED
@@ -73,6 +73,8 @@ const config = {
73
73
  };
74
74
  /** Turns currently running, keyed like sessions — `/stop` kills one. */
75
75
  const activeTurns = new Map();
76
+ /** Wall-clock start of the running turn per session key, for the queue notice. */
77
+ const turnStartedAt = new Map();
76
78
  try {
77
79
  Object.assign(config, JSON.parse(readFileSync(join(DIR, 'config.json'), 'utf8')));
78
80
  } catch {}
@@ -466,6 +468,37 @@ function commandOf(text) {
466
468
  return `/${match[1].toLowerCase().replace(/[.-]+/g, '')}`;
467
469
  }
468
470
 
471
+ /**
472
+ * Commands that must run *now*, not behind the turn they are about.
473
+ *
474
+ * Everything in a topic used to be queued, so `/stop` waited for the very turn
475
+ * it was supposed to kill — on 2026-10-24 a 26-minute turn swallowed `/stop`
476
+ * and the person got "ход уже идёт" instead of an interruption. These few
477
+ * commands touch no turn state, so they can run concurrently with one.
478
+ */
479
+ const IMMEDIATE_COMMANDS = new Set([
480
+ 'stop',
481
+ 'logs',
482
+ 'status',
483
+ 'queue',
484
+ 'help',
485
+ 'version',
486
+ 'doctor',
487
+ 'restart',
488
+ ]);
489
+
490
+ /** Canonical immediate command name for a message, or '' when it must queue. */
491
+ function immediateCommandOf(text) {
492
+ const trimmed = String(text ?? '').trim();
493
+ if (!trimmed.startsWith('/')) return '';
494
+ const head = trimmed.split(/\s+/)[0];
495
+ const collapsed = commandOf(head).replace(/^\//, '');
496
+ if (collapsed === 'apprestart' || collapsed === 'apprestartconfirm' || collapsed === 'restartconfirm') {
497
+ return 'restart';
498
+ }
499
+ return IMMEDIATE_COMMANDS.has(collapsed) ? collapsed : '';
500
+ }
501
+
469
502
  /**
470
503
  * Turn model markdown into plain readable text.
471
504
  *
@@ -1368,9 +1401,15 @@ function enqueue(key, task, onBusy) {
1368
1401
  const queued = queues.has(key);
1369
1402
  const run = prev
1370
1403
  .then(() => acquire())
1371
- .then(() => task())
1404
+ .then(() => {
1405
+ // Remember when the turn actually started: the queue notice has to say
1406
+ // "идёт уже 24 мин", not just "занято" — silence is what reads as a hang.
1407
+ turnStartedAt.set(key, Date.now());
1408
+ return task();
1409
+ })
1372
1410
  .catch((error) => log(`turn queue error ${key}: ${error.message}`))
1373
1411
  .finally(() => {
1412
+ turnStartedAt.delete(key);
1374
1413
  release();
1375
1414
  if (queues.get(key) === run) queues.delete(key);
1376
1415
  // A deferred restart waits for exactly this moment: the answer is out and
@@ -1649,6 +1688,17 @@ async function main() {
1649
1688
  }
1650
1689
  const queueKey = sessionKey(msg.chat.id, threadOf(msg));
1651
1690
  const thread = threadOf(msg);
1691
+ // `/stop` behind the turn it should kill is a no-op; run the few commands
1692
+ // that only read or kill state outside the per-topic queue.
1693
+ const immediate = immediateCommandOf(msg.text ?? '');
1694
+ if (immediate) {
1695
+ log(`command ${immediate} handled outside the queue`);
1696
+ Promise.resolve()
1697
+ .then(() => handleMessage(msg))
1698
+ .then(() => ack(update.update_id))
1699
+ .catch((error) => log(`command ${immediate} failed: ${error.message}`));
1700
+ return;
1701
+ }
1652
1702
  enqueue(
1653
1703
  queueKey,
1654
1704
  async () => {
@@ -1669,7 +1719,14 @@ async function main() {
1669
1719
  ? async () => {
1670
1720
  await new Promise((resolve) => setTimeout(resolve, Number(config.busyNoticeMs ?? 45000)));
1671
1721
  if (!queues.has(queueKey)) return; // already finished
1672
- await send(msg.chat.id, '⏳ В этой теме ещё идёт предыдущий ход — твоё сообщение в очереди.', thread);
1722
+ const started = turnStartedAt.get(queueKey);
1723
+ const mins = started ? Math.max(1, Math.round((Date.now() - started) / 60000)) : 0;
1724
+ const howLong = mins > 0 ? `идёт уже ${mins} мин` : 'ещё идёт';
1725
+ await send(
1726
+ msg.chat.id,
1727
+ `⏳ Предыдущий ход ${howLong} — твоё сообщение в очереди.\nЕсли это затянулось, /stop прервёт ход, и я сразу возьмусь за твоим.`,
1728
+ thread,
1729
+ );
1673
1730
  }
1674
1731
  : undefined,
1675
1732
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tg-bridge",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Telegram bridge for DeepSeek Harness: text, voice, photos and project-per-topic sessions without opening the desktop app",
5
5
  "type": "module",
6
6
  "main": "bridge.mjs",