sintbot 1.4.17 → 1.4.18

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.
@@ -9,12 +9,13 @@ const { isDeepStrictEqual } = require('node:util')
9
9
  const { redact } = require('./redact')
10
10
  const { fmtStamp, normalizeTz } = require('./time')
11
11
  const { settleUsage, manifestPolicy, manifestHeader, manifestContext, manifestFeatures, manifestSkills,
12
- followupEnabled, systemAppendix, turnPrompt, engineTerminalStep, engineQueueStep } = require('../agent/privatecore')
12
+ followupEnabled, systemAppendix, turnPrompt, engineTerminalStep, engineQueueStep,
13
+ idleCompactPlan, idleCompactSettle, compactEvent, classifyError } = require('../agent/privatecore')
13
14
  const { taskId } = require('./enginecheckpoint')
14
15
  const { prepareCardTerminalPath } = require('./c2cardterminalpath')
15
16
  const { terminalReplyText } = require('./c2terminaleffectbridge')
16
17
  const { createC2TurnProjection } = require('./c2turnprojection')
17
- const { prepareC2RunnerInput } = require('./c2runnerinput')
18
+ const { prepareC2RunnerInput, prepareC2CompactionInput } = require('./c2runnerinput')
18
19
  const { continueC2Successor } = require('./c2successorbridge')
19
20
  const { rejectC2Input } = require('./c2inputrejection')
20
21
  const progress = require('./turnprogress')
@@ -119,7 +120,8 @@ const T = {
119
120
  quotaShow: (name, value) => `${name} 额度:${value}`,
120
121
  quotaMiss: name => `${name} 额度暂时没查到,稍后再试`,
121
122
  quotaNone: name => `模型「${name}」不支持查额度`,
122
- sessionLine: (turns, ctx, inTotal) => `【对话】第 ${turns} 轮 · ${ctx ? `上下文约 ${fmtWan(ctx)} token` : `累计处理约 ${fmtWan(inTotal)} token`}`,
123
+ sessionLine: (turns, ctx, inTotal, { idleAt = 0, lastAt = '' } = {}) => `【对话】第 ${turns} 轮 · ${ctx ? `上下文约 ${fmtWan(ctx)} token` : `累计处理约 ${fmtWan(inTotal)} token`}`
124
+ + (idleAt > 0 ? `(空闲时过 ${fmtWan(idleAt)} 先整理${lastAt ? ` · 上次整理 ${lastAt}` : ''})` : ''),
123
125
  noSession: '【对话】本段对话还没开始',
124
126
  issueUsage: '用法:/issue 后面跟一句话,说清哪里不对劲\n例:/issue 刚才发的图它没理我',
125
127
  issueLogged: id => `📝 已记下 #${id}`,
@@ -297,7 +299,8 @@ class Engine {
297
299
  durableV2SwitchProvider = null, durableV2SetBrain = null, durableV2ModelCatalog = null,
298
300
  durableV2ModelCard = null,
299
301
  durableV2UsageProbe = null, durableV2Issues = null,
300
- durableV2Version = '', durableV2DataDir = '', durableV2ConsoleEnabled = false } = options
302
+ durableV2Version = '', durableV2DataDir = '', durableV2ConsoleEnabled = false,
303
+ durableV2CompactSession = null, durableV2CapacitySnapshot = null } = options
301
304
  const sync = f => typeof f === 'function' && f.constructor?.name !== 'AsyncFunction'
302
305
  if (!cfg || !cfg.agent || !cfg.groups || !cfg.providers ||
303
306
  options.receiptJournal || options.unifiedStepHost ||
@@ -326,7 +329,9 @@ class Engine {
326
329
  durableV2ModelCard !== null && !sync(durableV2ModelCard) ||
327
330
  durableV2UsageProbe !== null && typeof durableV2UsageProbe !== 'function' ||
328
331
  durableV2Issues !== null && typeof durableV2Issues?.add !== 'function' ||
329
- typeof durableV2Version !== 'string') {
332
+ typeof durableV2Version !== 'string' ||
333
+ durableV2CompactSession !== null && typeof durableV2CompactSession !== 'function' ||
334
+ durableV2CapacitySnapshot !== null && !sync(durableV2CapacitySnapshot)) {
330
335
  throw new TypeError('dedicated Engine requires complete durable v2 authority')
331
336
  }
332
337
  Object.assign(this, { cfg, store, log, durableV2Runtime, durableV2Payloads,
@@ -335,7 +340,7 @@ class Engine {
335
340
  durableV2StreamFrames, durableV2AcquireCapacity, durableV2SealCapacity,
336
341
  durableV2SwitchProvider, durableV2SetBrain, durableV2ModelCatalog, durableV2ModelCard,
337
342
  durableV2UsageProbe, durableV2Issues, durableV2Version,
338
- durableV2DataDir, durableV2ConsoleEnabled })
343
+ durableV2DataDir, durableV2ConsoleEnabled, durableV2CompactSession, durableV2CapacitySnapshot })
339
344
  this.projectMemory = options.projectMemory || null
340
345
  this.repoInfo = options.repoInfo || null
341
346
  this.activity = options.activity || null
@@ -351,9 +356,11 @@ class Engine {
351
356
 
352
357
  shutdown() {
353
358
  this.durableV2Runtime.close()
359
+ this._closing = true // 关停中不再起空闲整理
354
360
  let aborted = 0
355
361
  for (const rt of this.rts.values()) {
356
362
  rt.v2AdmissionController?.abort()
363
+ this._stopIdleCompact(rt, '关停', { force: true }) // 整理一并停掉(不等压缩边界),不算被打断的轮次(它没有收据)
357
364
  if (rt.v2Controller) {
358
365
  try { rt.v2Controller.abort() } catch {}
359
366
  aborted++
@@ -372,14 +379,16 @@ async drainDurableV2(timeoutMs = 12_000) {
372
379
  if (active.some(rt => rt.v2NormalRouteFailure)) return false
373
380
  const runs = active.filter(rt => rt.v2RunnerStopPromise || rt.v2Controller)
374
381
  const routes = active.filter(rt => rt.v2NormalRouteActive)
375
- if (!runs.length && !routes.length) return true
382
+ const compacting = active.filter(rt => rt.v2Compacting?.exited)
383
+ if (!runs.length && !routes.length && !compacting.length) return true
376
384
  if (runs.some(rt => !rt.v2RunnerStopPromise) ||
377
385
  routes.some(rt => !rt.v2NormalRoutePromise)) return false
378
386
  let timer
379
387
  try {
380
388
  const results = await Promise.race([
381
389
  Promise.all([...runs.map(rt => rt.v2RunnerStopPromise),
382
- ...routes.map(rt => rt.v2NormalRoutePromise)]),
390
+ ...routes.map(rt => rt.v2NormalRoutePromise),
391
+ ...compacting.map(rt => rt.v2Compacting.exited.then(closed => closed === true, () => false))]),
383
392
  new Promise(resolve => { timer = setTimeout(() => resolve(null), timeoutMs) }),
384
393
  ])
385
394
  return Array.isArray(results) && results.every(value => value === true)
@@ -546,6 +555,9 @@ async _durableV2Command(msg, cmd, rest) {
546
555
  let locked = false
547
556
  try { locked = this.durableV2AssertWriterLock() === true } catch {}
548
557
  if (!locked) throw new Error('C2 v2 /new lost single-writer lock')
558
+ const stopping = this._stopIdleCompact(this.rts.get(msg.chatId), '开新会话')
559
+ if (stopping && await stopping !== true) return reject('new-busy', T.newBusy)
560
+ if (this.durableV2AssertWriterLock() !== true || this._durableV2GroupBusy(msg.chatId)) return reject('new-busy', T.newBusy)
549
561
  this.store.resetSessionAndStats(msg.chatId)
550
562
  this.log(`/new: ${msg.chatId} (by ${msg.senderName || 'unknown'})`)
551
563
  return reject('new-session', T.freshSession)
@@ -561,8 +573,12 @@ async _durableV2Command(msg, cmd, rest) {
561
573
  }
562
574
  const stats = this.store.getSessionStats(msg.chatId)
563
575
  const session = this.store.getSession(msg.chatId)
576
+ const hudRt = this.rts.get(msg.chatId)
577
+ const idleAt = provider && provider.runner === 'claude' && this._idleCompactCapability(hudRt, name) === true
578
+ ? idleCompactPlan({ threshold: this._idleCompactThreshold(provider), now: 0 }).at : 0
564
579
  lines.push(stats?.sessionId && stats.sessionId === session
565
- ? T.sessionLine(stats.turns, stats.ctx || 0, stats.in || 0) : T.noSession)
580
+ ? T.sessionLine(stats.turns, stats.ctx || 0, stats.in || 0,
581
+ { idleAt, lastAt: stats.compactedAt ? fmtStamp(stats.compactedAt, this.tz).slice(-5) : '' }) : T.noSession)
566
582
  return reject('hud', lines.join('\n'))
567
583
  }
568
584
  if (cmd === '/issue') {
@@ -790,6 +806,9 @@ async _durableV2ApplyModel(chatId, group, pw, { sender = 'unknown', said = '', s
790
806
  const current = brainLabel(name, this._brain(group, name, provider))
791
807
  if (!switching && model === undefined && effort === undefined) return { kind: 'same', text: T.modelSame(current), label: current }
792
808
  if (this._durableV2GroupBusy(chatId)) return reject('busy', T.modelBusy)
809
+ const stopping = this._stopIdleCompact(this.rts.get(chatId), '会话已重置')
810
+ if (stopping && await stopping !== true) return reject('busy', T.modelBusy)
811
+ if (this.durableV2AssertWriterLock() !== true || this._durableV2GroupBusy(chatId)) return reject('busy', T.modelBusy)
793
812
  let r
794
813
  try {
795
814
  if (this.durableV2SetBrain) {
@@ -1002,6 +1021,13 @@ async runDurableV2Message(msg, platform) {
1002
1021
  let release = null
1003
1022
  let failed = false
1004
1023
  try {
1024
+ // 空闲整理不占群锁:收据已登记,起轮前先停掉整理并等它退出(同一个会话文件不能两个进程同时写)。
1025
+ // 等待期间按「等并发名额」对外:后到的消息走 waitingForCapacity 那一支排队,而不是撞上已关闭的忙时路由。
1026
+ if (rt.v2Compacting && !(await this._awaitIdleCompactStop(rt))) {
1027
+ this._observeActivity('unknown', { chatId: msg.chatId })
1028
+ return { receiptId: accepted.receiptId, status: 'interruptedBeforeDispatch',
1029
+ queued: true, ingressClosed: true }
1030
+ }
1005
1031
  let receiptId = accepted.receiptId
1006
1032
  if (this.durableV2AcquireCapacity) {
1007
1033
  const controller = new AbortController()
@@ -1049,11 +1075,15 @@ async runDurableV2Message(msg, platform) {
1049
1075
  rt.v2NormalRouteFailure = true
1050
1076
  return false
1051
1077
  })
1078
+ let outcome = null
1052
1079
  try {
1053
- return await execution
1080
+ outcome = await execution
1081
+ return outcome
1054
1082
  } finally {
1055
1083
  rt.v2NormalRouteActive = false
1056
1084
  rt.v2NormalRoutePromise = null
1085
+ // 群已空闲、槽已还:看这一轮要不要接一次空闲整理(同步判定,起跑后才异步)。
1086
+ this._maybeIdleCompact(rt, outcome?.status)
1057
1087
  }
1058
1088
  }
1059
1089
 
@@ -2074,6 +2104,9 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
2074
2104
  onEvent: event => {
2075
2105
  if (event.type === 'system' && event.subtype === 'context')
2076
2106
  this._observeActivity('observe', { chatId, receiptId, generation, event })
2107
+ // CLI 在 init 里自报的可用命令:含 compact 才能空闲整理(按这一家 provider 记)。
2108
+ if (event.type === 'system' && event.subtype === 'init' && Array.isArray(event.slashCommands))
2109
+ rt.v2CanCompact = { provider: this._resolveProvider(chatId, job.group).name, ok: event.slashCommands.includes('compact') }
2077
2110
  },
2078
2111
  onTurnAccepted: value => {
2079
2112
  const ack = capability.onTurnAccepted(value)
@@ -2084,6 +2117,7 @@ async startDurableV2Runner({ chatId, receiptId, turnToken, effectId } = {}) {
2084
2117
  onSpawnFailed: value => capability.onSpawnFailed(value),
2085
2118
  onSteerReady, onSteerLanded,
2086
2119
  log: this.log })
2120
+ rt.v2Compacted = false // 「进度可能已折叠」只带一轮
2087
2121
  let started
2088
2122
  try { started = this.durableV2StartTurn(input) }
2089
2123
  catch (error) { started = Promise.reject(error) }
@@ -2506,11 +2540,177 @@ classifyDurableV2UnresolvedTurn({ chatId, receiptId, turnToken, effectId,
2506
2540
  // Reinject current supported context each turn. No legacy feature menu,
2507
2541
  // external-message commands or v1 manifest refresh scheduler is included.
2508
2542
  return turnPrompt({ lines: batch?.length > 1 ? batch.map(line) : [line(msg)],
2509
- cancelled: null, compacted: false, manifest, updated: false })
2543
+ cancelled: null, compacted: rt?.v2Compacted === true, manifest, updated: false })
2510
2544
  }
2511
2545
 
2512
2546
  handleMessage() { throw new Error('legacy Engine execution path is not enabled') }
2513
2547
 
2548
+ // ── 空闲整理(ABI 37:核心 idle_compact_plan / idle_compact_settle 定规则,这里只起停进程)──────────
2549
+ // 一轮正常收尾、群里没人在等时,对同一个 Claude 会话跑一次无头 /compact,把压缩从成员等着的轮次挪到间隙。
2550
+ // 没有收据的运行不进持久日志:只占一个并发槽、写 store 的会话统计、记守护进程日志。不占群锁(rt.v2Compacting
2551
+ // 只是句柄):新消息照常走空闲路径,起轮前先 _stopIdleCompact 并等进程退出;/new、换型号、关停同样先停。
2552
+ _idleCompactThreshold(provider) {
2553
+ const raw = Number(provider?.autocompactTokens) || 0
2554
+ return raw > 0 ? Math.min(1_000_000, Math.max(100_000, Math.floor(raw))) : 0
2555
+ }
2556
+
2557
+ _idleCompactCapability(rt, name) {
2558
+ return rt?.v2CanCompact && rt.v2CanCompact.provider === name ? rt.v2CanCompact.ok : null
2559
+ }
2560
+
2561
+ _maybeIdleCompact(rt, status) {
2562
+ if (!this.durableV2CompactSession || !this.durableV2CapacitySnapshot || !rt) return
2563
+ try {
2564
+ const chatId = rt.chatId
2565
+ const group = this.cfg.groups?.[chatId]
2566
+ if (!group) return
2567
+ const { name, provider } = this._resolveProvider(chatId, group)
2568
+ if (!provider || provider.runner !== 'claude') return
2569
+ const sid = this.store.getSession(chatId) || null
2570
+ const stats = this.store.getSessionStats(chatId) || null
2571
+ const cap = this.durableV2CapacitySnapshot()
2572
+ const freeSlots = cap && !cap.closed ? Math.max(0, cap.maxConcurrent - cap.active - cap.waiting) : 0
2573
+ const outcome = status === 'answered' ? 'ok' : status === 'silent' ? 'no-reply' : String(status || '')
2574
+ const plan = idleCompactPlan({ threshold: this._idleCompactThreshold(provider), ctx: stats?.ctx || 0,
2575
+ canCompact: this._idleCompactCapability(rt, name), outcome,
2576
+ busy: this._durableV2GroupBusy(chatId) || !!rt.v2Compacting,
2577
+ queued: rt.v2QueueReceiptIds?.length || 0,
2578
+ paused: group.paused === true, closing: this._closing === true,
2579
+ sessionMatch: !!(sid && stats && stats.sessionId === sid), freeSlots,
2580
+ restUntil: rt.v2CompactRest && rt.v2CompactRest.sid === sid ? rt.v2CompactRest.until : null,
2581
+ now: Date.now() })
2582
+ if (!plan.start) {
2583
+ if (plan.at > 0 && (stats?.ctx || 0) >= plan.at) this.log(`group ${chatId}: 空闲整理未起 — ${plan.reason}`)
2584
+ return
2585
+ }
2586
+ this._startIdleCompact(rt, { group, name, provider, sid, ctx: stats.ctx, plan })
2587
+ } catch (error) {
2588
+ this.log(`group ${rt.chatId}: 空闲整理判定出错(忽略): ${error.message}`)
2589
+ }
2590
+ }
2591
+
2592
+ _startIdleCompact(rt, { group, name, provider, sid, ctx, plan }) {
2593
+ const chatId = rt.chatId
2594
+ const brain = this._brain(group, name, provider) // 可能抛:要在挂句柄之前
2595
+ const controller = new AbortController()
2596
+ const rec = { controller, sid, provider: name, boundary: null, startedAt: Date.now(), why: '', lateKill: null, exited: null, skip: false, closed: false }
2597
+ rt.v2Compacting = rec
2598
+ this.log(`group ${chatId}: 空闲整理开始 — 上下文约 ${fmtWan(ctx)},过了整理线 ${fmtWan(plan.at)}`)
2599
+ const cap = setTimeout(() => { if (!rec.why) rec.why = '超时'; controller.abort() }, plan.maxMs)
2600
+ cap.unref?.()
2601
+ rec.exited = (async () => {
2602
+ let release = null, result = null, error = null, spawned = false, verified = false
2603
+ try {
2604
+ if (this.durableV2AcquireCapacity) release = await this.durableV2AcquireCapacity({ signal: controller.signal })
2605
+ if (controller.signal.aborted) {
2606
+ const stopped = new Error('idle compaction stopped before start'); stopped.code = 'IDLE_COMPACT_STOPPED'; throw stopped
2607
+ }
2608
+ // ⚠ 与平时轮次同一组启动参数(工具、MCP、系统提示附录、型号、深度、env)—— 缓存才命中。
2609
+ const input = prepareC2CompactionInput({ appendSystemPrompt: SYSTEM_APPENDIX, chatId, group, provider,
2610
+ config: this.cfg, brain: { model: brain.model, effort: brain.effort }, sessionId: sid,
2611
+ signal: controller.signal, extraEnv: this.baseSkillEnv(chatId), keep: plan.keep, log: this.log,
2612
+ onEvent: event => {
2613
+ if (event.type !== 'system') return
2614
+ if (event.subtype === 'init' && Array.isArray(event.slashCommands)) {
2615
+ rt.v2CanCompact = { provider: name, ok: event.slashCommands.includes('compact') }
2616
+ } else if (compactEvent(event.subtype || '')) {
2617
+ rec.boundary = { pre: event.preTokens || ctx, post: event.postTokens || 0 }
2618
+ }
2619
+ } })
2620
+ const started = this.durableV2CompactSession(input)
2621
+ if (started === null) {
2622
+ rt.v2CanCompact = { provider: name, ok: false }
2623
+ rec.skip = true
2624
+ this.log(`group ${chatId}: 空闲整理 — 这个后端不支持,只在轮内压缩`)
2625
+ return true
2626
+ }
2627
+ spawned = true
2628
+ result = await started
2629
+ verified = this.durableV2VerifyRunnerClosed({ result }) === true
2630
+ } catch (e) {
2631
+ error = e
2632
+ verified = !spawned || this.durableV2VerifyRunnerClosed({ error: e }) === true
2633
+ } finally {
2634
+ clearTimeout(cap)
2635
+ if (rec.lateKill) clearTimeout(rec.lateKill)
2636
+ rec.closed = !spawned || verified
2637
+ if (rec.closed) {
2638
+ try { this._settleIdleCompact(rt, rec, result, error) } catch (e) { this.log(`group ${chatId}: 空闲整理收尾出错(忽略): ${e.message}`) }
2639
+ if (release) { try { release() } catch (e) { this.log(`group ${chatId}: model permit release failed: ${e.message}`) } }
2640
+ if (rt.v2Compacting === rec) rt.v2Compacting = null
2641
+ } else {
2642
+ // Keep the handle as well as the permit: drain, recovery and the next
2643
+ // session writer must all see the same unresolved close evidence.
2644
+ rec.closeError = error || Object.assign(new Error('Compaction close remains unproven'), { code: 'IDLE_COMPACT_CLOSE_UNPROVEN' })
2645
+ this.log(`group ${chatId}: model permit kept until restart; compaction close unproven`)
2646
+ try { this.durableV2Runtime.close() } catch {}
2647
+ }
2648
+ }
2649
+ return rec.closed
2650
+ })()
2651
+ }
2652
+
2653
+ _settleIdleCompact(rt, rec, result, error) {
2654
+ if (rec.skip) return
2655
+ const chatId = rt.chatId
2656
+ const res = result?.result || null
2657
+ const now = Date.now()
2658
+ const live = this.store.getSession(chatId) === rec.sid
2659
+ const failureText = error ? String(error.message || error) : res?.isError ? String(res.text || res.subtype || '') : ''
2660
+ const quota = res?.quota || (failureText ? classifyError(failureText)?.quota : null)
2661
+ const out = idleCompactSettle({ boundary: rec.boundary, resultError: !!res?.isError, quota: !!quota,
2662
+ error: failureText,
2663
+ aborted: rec.controller.signal.aborted, why: rec.why,
2664
+ fails: rt.v2CompactFails && rt.v2CompactFails.sid === rec.sid ? rt.v2CompactFails.n : 0,
2665
+ resetAt: null, elapsedMs: now - rec.startedAt, now, live })
2666
+ if (out.status === 'compacted' && live) {
2667
+ rt.v2Compacted = true // 下一轮带「进度可能已折叠为摘要」
2668
+ let locked = false
2669
+ try { locked = this.durableV2AssertWriterLock() === true } catch {}
2670
+ const stats = this.store.getSessionStats(chatId)
2671
+ // 群忙(新消息刚停掉整理)时不碰统计:那一轮结算会写新值。
2672
+ if (locked && stats && stats.sessionId === rec.sid && !this._durableV2GroupBusy(chatId)) {
2673
+ const next = { ...stats, compactedAt: now }
2674
+ if (out.ctx) next.ctx = out.ctx; else delete next.ctx
2675
+ this.store.putSessionStats(chatId, next)
2676
+ }
2677
+ }
2678
+ rt.v2CompactFails = { sid: rec.sid, n: out.fails }
2679
+ rt.v2CompactRest = out.restUntil ? { sid: rec.sid, until: out.restUntil } : null
2680
+ this.log(`group ${chatId}: ${out.notice}`)
2681
+ }
2682
+
2683
+ // 同步发出停止信号,返回「进程退干净了」的 Promise(没有在跑 ⇒ null)。已收到压缩边界 ⇒ 不杀(压完了只差落盘),
2684
+ // 兜底 10 s 再杀;否则立刻 abort(与普通轮同一条两段式收尾)。
2685
+ _stopIdleCompact(rt, why, { force = false } = {}) {
2686
+ const rec = rt?.v2Compacting
2687
+ if (!rec) return null
2688
+ if (!rec.why) rec.why = why
2689
+ if (rec.boundary && !force) {
2690
+ if (!rec.lateKill) { rec.lateKill = setTimeout(() => { try { rec.controller.abort() } catch {} }, 10_000); rec.lateKill.unref?.() }
2691
+ } else { try { rec.controller.abort() } catch {} }
2692
+ return rec.exited
2693
+ }
2694
+
2695
+ // 控制台 / 配置写入是同步接口:只能发信号,所以不等边界、立刻杀,收尾认不到会话就不记统计。
2696
+ stopIdleCompact(chatId, why = '会话已重置') { return this._stopIdleCompact(this.rts.get(chatId), why, { force: true }) }
2697
+
2698
+ // 起轮前等整理退出。等待期间挂「等并发名额」的标志与控制器,让这期间到的消息按排队处理;
2699
+ // 关停会中止这个控制器 ⇒ 返回 false,调用方按「派发前被打断」收场。
2700
+ async _awaitIdleCompactStop(rt) {
2701
+ const stopping = this._stopIdleCompact(rt, '新消息')
2702
+ if (!stopping) return true
2703
+ const controller = new AbortController()
2704
+ rt.v2AdmissionController = controller
2705
+ rt.v2AdmissionPending = true
2706
+ let closed = false
2707
+ try { closed = await stopping === true } finally {
2708
+ if (rt.v2AdmissionController === controller) rt.v2AdmissionController = null
2709
+ }
2710
+ if (!closed || controller.signal.aborted) { rt.v2AdmissionPending = false; return false }
2711
+ return true
2712
+ }
2713
+
2514
2714
  stop(chatId) {
2515
2715
  const rt = this.rts.get(chatId)
2516
2716
  return rt?.v2NormalRouteActive ? this.requestDurableV2Stop(chatId) : false