peertable 0.8.36 → 0.8.38

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peertable",
3
- "version": "0.8.36",
3
+ "version": "0.8.38",
4
4
  "description": "A round table of peer agents. No orchestrator at the head. Turn Claude Code, Codex, and Grok sessions into a team of equal, long-lived peers.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/room/client.mjs CHANGED
@@ -14,7 +14,7 @@ import { boundedRecent, boundedUnread } from './message-bounds.mjs'
14
14
 
15
15
  // client.mjs 側のハードコード版数。package.json の version と一致していることを
16
16
  // diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
17
- const MCP_VERSION = '0.8.36'
17
+ const MCP_VERSION = '0.8.38'
18
18
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
19
19
 
20
20
  const USAGE = `usage:
@@ -226,6 +226,7 @@ setInterval(beatBridge, 5_000) // 送信自体は BEAT_MS で間引く。失敗
226
226
  // 席ごとに未配達を溜めて、2秒ごとにまとめて1回起こす(連投で席を何度も起こさない)。
227
227
  // pending は room message の宛先名から作る。起動引数や harness は候補集合を決めない。
228
228
  const pending = new Map() // name -> Map<seq, message>
229
+ const stuckStreaks = new Map() // name -> { key: 'seq,seq', count }(DELIVERY_STUCK連続数。5回で打ち切り)
229
230
  const deliveryStates = new Map() // seq -> { message, targets, delivered }
230
231
  let seats = []
231
232
  let members = new Map()
@@ -485,7 +486,19 @@ async function wake(seat, msgs) {
485
486
  } else {
486
487
  await run('tmux', tmuxArgv(['send-keys', '-t', observation.target, 'C-u'], { socket: observation.socket }))
487
488
  await sleep(100)
488
- await run('tmux', tmuxArgv(['send-keys', '-l', '-t', observation.target, text], { socket: observation.socket }))
489
+ // 素打ち(send-keys -l)は長文で連続keystrokeとして届き、TUIのpaste burst検知を
490
+ // 半端な状態に落として composer が submit 不能に固まる形を繰り返した(実被弾
491
+ // 2026-08-29: Codex席が同日3回、掃除も効かない入力破損)。正規の bracketed paste
492
+ // (load-buffer→paste-buffer -p)で「1回の貼り付け」として届ける。
493
+ const pasteBuf = `wakeup-${process.pid}-${Date.now()}`
494
+ const pasteFile = join(proj, '.team', `wakeup-paste-${process.pid}.txt`)
495
+ writeFileSync(pasteFile, text)
496
+ try {
497
+ await run('tmux', tmuxArgv(['load-buffer', '-b', pasteBuf, pasteFile], { socket: observation.socket }))
498
+ await run('tmux', tmuxArgv(['paste-buffer', '-p', '-d', '-b', pasteBuf, '-t', observation.target], { socket: observation.socket }))
499
+ } finally {
500
+ try { unlinkSync(pasteFile) } catch { /* 一時fileの掃除失敗は配達判定に影響しない */ }
501
+ }
489
502
  await sleep(750)
490
503
  await run('tmux', tmuxArgv(['send-keys', '-t', observation.target, 'Enter'], { socket: observation.socket }))
491
504
  }
@@ -557,7 +570,16 @@ async function wake(seat, msgs) {
557
570
  || (marker.length >= 8 && tailSquashed.includes(marker) && !queuedOrRunning))
558
571
  if (!stuck) break
559
572
  if (attempt === 2) {
560
- log(`DELIVERY_STUCK: ${seat} の入力欄に本文が残ったまま送信できない(popup/入力詰まり)。受領を確定せず次周期に再試行する`)
573
+ // 蓄積戦争の禁止: 次周期は束ねる本文が変わり得て残存照合が外れ、全文再打鍵が
574
+ // composer を多重本文で埋める(実被弾 2026-08-29 に2回)。STUCKで周期を明け渡す前に
575
+ // composer を行数分の C-u で空にし、次周期が常に「空の入力欄へ1部だけ」から始まる
576
+ // 構造にする。
577
+ const wipePane = await run('tmux', tmuxArgv(['capture-pane', '-t', observation.target, '-p'], { socket: observation.socket }))
578
+ const wipeLines = Math.min(120, String(wipePane.stdout ?? '').split('\n').length + 10)
579
+ for (let i = 0; i < wipeLines; i++) {
580
+ await run('tmux', tmuxArgv(['send-keys', '-t', observation.target, 'C-u'], { socket: observation.socket }))
581
+ }
582
+ log(`DELIVERY_STUCK: ${seat} の入力欄に本文が残ったまま送信できない(popup/入力詰まり)。入力欄を掃除し、受領を確定せず次周期に再試行する`)
561
583
  const error = new Error(`DELIVERY_STUCK: ${seat}`)
562
584
  error.code = 'DELIVERY_STUCK'
563
585
  throw error
@@ -693,6 +715,22 @@ async function flushSeat(seat) {
693
715
  // 再試行が成立すれば同じ (seq, recipient) を delivered で上書きする
694
716
  const result = ['SEAT_TUI_GONE', 'MEMBER_MISSING', 'DESCRIPTOR_MISSING'].includes(code) ? 'seat_unavailable' : 'failed'
695
717
  for (const msg of msgs) await postReceipt(msg.seq, seat, result, code)
718
+ // 同一集合のSTUCKが5周期続いたら再試行を打ち切る。failed receiptと親宛[配達失敗]DMは
719
+ // 既に出ており、無限の打鍵再試行は席のcomposerを汚し続けるだけで誰も救わない。
720
+ if (code === 'DELIVERY_STUCK') {
721
+ const key = msgs.map((msg) => msg.seq).join(',')
722
+ const prev = stuckStreaks.get(seat)
723
+ const count = prev?.key === key ? prev.count + 1 : 1
724
+ stuckStreaks.set(seat, { key, count })
725
+ if (count >= 5) {
726
+ const queue = pending.get(seat)
727
+ if (queue) for (const msg of msgs) queue.delete(msg.seq)
728
+ stuckStreaks.delete(seat)
729
+ log(`DELIVERY_ABANDONED: ${seat} への seq ${key} は5周期連続STUCKのため再試行を打ち切る(receipt=failed・親へ通知済み)`)
730
+ }
731
+ } else {
732
+ stuckStreaks.delete(seat)
733
+ }
696
734
  } finally {
697
735
  flushing.delete(seat)
698
736
  }