peertable 0.8.18 → 0.8.20

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.18",
3
+ "version": "0.8.20",
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
@@ -13,7 +13,7 @@ import { findModelsDoc, resolveSeatIdentity } from '../skill/scripts/resolve-sea
13
13
 
14
14
  // client.mjs 側のハードコード版数。package.json の version と一致していることを
15
15
  // diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
16
- const MCP_VERSION = '0.8.18'
16
+ const MCP_VERSION = '0.8.20'
17
17
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
18
18
 
19
19
  const USAGE = `usage:
@@ -22,6 +22,7 @@ import { promisify } from 'node:util'
22
22
 
23
23
  import { resolvePostToken } from './seat-usage.mjs'
24
24
  import { observePidCommand } from './seat-identity.mjs'
25
+ import { updateBridgeProgress } from './bridge-record-live.mjs'
25
26
 
26
27
  const run = promisify(execFile)
27
28
  const [proj, flag] = process.argv.slice(2)
@@ -57,9 +58,10 @@ if (!token) { console.error('ALARM_BRIDGE_TOKEN_MISSING: 書込トークンが
57
58
  mkdirSync(alarmsDir, { recursive: true })
58
59
 
59
60
  const self = observePidCommand(process.pid)
61
+ const startedAt = new Date().toISOString()
60
62
  writeFileSync(record, JSON.stringify({
61
63
  pid: process.pid, start_identity: self.started_identity, room, server_url: url,
62
- started_at: new Date().toISOString(), ready_at: new Date().toISOString(),
64
+ started_at: startedAt, ready_at: startedAt, last_progress_at: startedAt,
63
65
  }) + '\n')
64
66
  const cleanup = () => { try { unlinkSync(record) } catch {} process.exit(0) }
65
67
  process.on('SIGTERM', cleanup)
@@ -117,5 +119,6 @@ async function tick() {
117
119
  log(`alarm-bridge start: room=${room} project=${proj} pid=${process.pid}`)
118
120
  for (;;) {
119
121
  try { await tick() } catch (e) { log(`tick 失敗(続行): ${e.message}`) }
122
+ updateBridgeProgress(record)
120
123
  await sleep(2000)
121
124
  }
@@ -3,7 +3,7 @@
3
3
  // Windows は pid を再利用する。alive な pid だけでは本人ではない
4
4
  // (2026-08-21 実測: wakeup-bridge.json の pid が oracle-mcp に再利用され、
5
5
  // 本物の配達は死んだまま ensure-bridge が起動済みと誤認した)。
6
- import { readFileSync } from 'node:fs'
6
+ import { readFileSync, renameSync, writeFileSync } from 'node:fs'
7
7
  import { resolve } from 'node:path'
8
8
  import { fileURLToPath } from 'node:url'
9
9
 
@@ -21,6 +21,13 @@ export function bridgeRecordLive(record, now = Date.now()) {
21
21
  return (now - at) <= STALE_MS
22
22
  }
23
23
 
24
+ export function updateBridgeProgress(path, now = new Date()) {
25
+ const record = JSON.parse(readFileSync(path, 'utf8'))
26
+ const temporary = `${path}.${process.pid}.tmp`
27
+ writeFileSync(temporary, JSON.stringify({ ...record, last_progress_at: now.toISOString() }) + '\n')
28
+ renameSync(temporary, path)
29
+ }
30
+
24
31
  const invokedDirectly = Boolean(process.argv[1])
25
32
  && resolve(fileURLToPath(import.meta.url)).toLowerCase() === resolve(process.argv[1]).toLowerCase()
26
33
  if (invokedDirectly) {
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { resolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ export function keysForClaudeMcpPane(screen) {
6
+ const text = String(screen ?? '')
7
+ if (text.includes('New MCP server found in this project: room')) {
8
+ return { kind: 'single-mcp-consent', keys: ['Down', 'Enter'] }
9
+ }
10
+ if (/new MCP servers found in this project/iu.test(text) && text.includes('Enable selected')) {
11
+ const selected = text.split(/\r?\n/u).filter(line => /\[✔\]/u.test(line))
12
+ if (!selected.some(line => /\broom\b/u.test(line))) return null
13
+ return { kind: 'multi-mcp-consent', keys: [...selected.map(() => 'Down'), 'Enter'] }
14
+ }
15
+ return null
16
+ }
17
+
18
+ const isMain = Boolean(process.argv[1]) && resolve(process.argv[1]) === fileURLToPath(import.meta.url)
19
+ if (isMain) {
20
+ const chunks = []
21
+ for await (const chunk of process.stdin) chunks.push(chunk)
22
+ process.stdout.write(`${JSON.stringify(keysForClaudeMcpPane(Buffer.concat(chunks).toString('utf8')))}\n`)
23
+ }
@@ -25,6 +25,10 @@ fi
25
25
  sock=$(node "$(dirname "$0")/tmux-socket.mjs")
26
26
  room=$(node -e 'process.stdout.write(require(process.argv[1]).room)' "$team/setup-state.json")
27
27
  session="peertable-${name}-${room}"
28
+ # **session が在るだけでは常駐が生きている証拠にならない**(中の node だけ死んで殻が残る)。
29
+ # pid/進捗の生存確認を通れずここへ来たsessionは、Windowsでlogを開いたままの可能性がある。
30
+ # log切詰めより先に畳み、EBUSYを発生させない。
31
+ tmux_at kill-session -t "$session" 2>/dev/null || true
28
32
  # **死んだ記録をここで消す。** 残すと下の loop が前回の `ready_at` を読んで、
29
33
  # 新しい bridge が1バイトも動いていない段階で success を返す——「起動していないのに
30
34
  # 起動したと言う」=この supervisor が塞ぐはずの穴そのものになる。
@@ -41,9 +45,6 @@ for v in PEERTABLE_TMUX_SOCKET PEERTABLE_POST_TOKEN PEERTABLE_CREDENTIAL_FILE PE
41
45
  eval "val=\${$v:-}"
42
46
  [ -n "$val" ] && env_prefix="$env_prefix $v=$(printf '%q' "$val")"
43
47
  done
44
- # **session が在るだけでは常駐が生きている証拠にならない**(中の node だけ死んで殻が残る)。
45
- # 上で pid 生存を確かめて here まで来た=生きていないので、殻は畳んでから立て直す。
46
- tmux_at kill-session -t "$session" 2>/dev/null || true
47
48
  if [ "$(node -p 'process.platform')" = "win32" ]; then
48
49
  # psmuxが起動する正規shellはPowerShell 7。POSIXの`env`とMSYS pathを文字列のまま
49
50
  # 渡すとnodeへ到達する前に終了するため、Windows nodeに各pathをargvとして変換させ、
@@ -474,6 +474,7 @@ codex_update_accepted=false
474
474
  codex_trust_accepted=false
475
475
  claude_trust_accepted=false
476
476
  codex_dialog_helper="$peertable_script_dir/codex-dialog.mjs"
477
+ claude_dialog_helper="$peertable_script_dir/claude-dialog.mjs"
477
478
  pass_codex_pane() {
478
479
  local json key
479
480
  json=$(printf '%s' "$1" | node "$codex_dialog_helper" || true)
@@ -489,6 +490,18 @@ print("\n".join(a.get("keys") or []) if isinstance(a, dict) else "")' <<<"$json"
489
490
  codex_pane_blocks_ready() {
490
491
  printf '%s' "$1" | node "$codex_dialog_helper" --ready-ok
491
492
  }
493
+ pass_claude_mcp_pane() {
494
+ local json key
495
+ json=$(printf '%s' "$1" | node "$claude_dialog_helper" || true)
496
+ [ -n "$json" ] && [ "$json" != "null" ] || return 1
497
+ while IFS= read -r key; do
498
+ [ -n "$key" ] || continue
499
+ tmux_at send-keys -t "$sess" "$key" || return 1
500
+ done < <(python3 -c 'import json,sys
501
+ a=json.loads(sys.stdin.read() or "null")
502
+ print("\n".join(a.get("keys") or []) if isinstance(a, dict) else "")' <<<"$json")
503
+ return 0
504
+ }
492
505
  while [ $SECONDS -lt "$room_ready_deadline" ]; do
493
506
  # Grok Build は初めて開く作業treeで、room MCPを初期化する前にworkspace trustを尋ねる。
494
507
  # Peertableが正式に着席させるtreeなので、この既知文言だけを一度通す。未知の確認画面を
@@ -551,17 +564,11 @@ while [ $SECONDS -lt "$room_ready_deadline" ]; do
551
564
  # 同意が member 登録より前に出る。選択肢2(this and all future)だけを通す。
552
565
  if [ "$harness" = claude ] && [ "$mcp_consent_accepted" != true ]; then
553
566
  claude_screen=$(tmux_at capture-pane -t "$sess" -p 2>/dev/null || true)
554
- case "$claude_screen" in
555
- *"New MCP server found in this project: room"*)
556
- if ! tmux_at send-keys -t "$sess" Down || ! tmux_at send-keys -t "$sess" Enter; then
557
- echo "SEAT_CLAUDE_MCP_CONSENT_FAILED: room MCP 同意へ応答できない" >&2
558
- exit 1
559
- fi
560
- mcp_consent_accepted=true
561
- room_ready_deadline=$((SECONDS + 90))
562
- echo "claude room MCP consent: accepted"
563
- ;;
564
- esac
567
+ if pass_claude_mcp_pane "$claude_screen"; then
568
+ mcp_consent_accepted=true
569
+ room_ready_deadline=$((SECONDS + 90))
570
+ echo "claude room MCP consent: accepted"
571
+ fi
565
572
  fi
566
573
  room_members=$(curl -sf "$url/api/$room/members" 2>/dev/null || true)
567
574
  if printf '%s' "$room_members" | python3 -c 'import json,sys; name=sys.argv[1]; members=json.load(sys.stdin).get("members",[]); raise SystemExit(0 if any(m.get("name") == name for m in members) else 1)' "$name"; then
@@ -28,6 +28,7 @@ import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from
28
28
  import { join } from 'node:path'
29
29
 
30
30
  import { STOP_DECLARATION, attributeJobSession, combineSeatLamp, isPaneHarnessMissing, hasActiveDescendant, subtreeCpuSeconds, patrolTargets, classifyPaneTail, decideBridgeContinuation, deriveMissingSession, isPaneProcessStopped, parsePaneTokenHint, resolveLatticeExecutable, resolvePostToken, resolveSeatObservation, resolveTmuxSocket, supportsMemberObservation, tmuxArgv, tmuxPanePid } from './seat-usage.mjs'
31
+ import { updateBridgeProgress } from './bridge-record-live.mjs'
31
32
 
32
33
  const args = process.argv.slice(2)
33
34
  const proj = args[0]
@@ -381,7 +382,8 @@ function recordReady() {
381
382
  readyRecorded = true
382
383
  const record = JSON.parse(readFileSync(pidPath, 'utf8'))
383
384
  const temporary = `${pidPath}.${process.pid}.tmp`
384
- writeFileSync(temporary, JSON.stringify({ ...record, ready_at: new Date().toISOString() }) + '\n')
385
+ const readyAt = new Date().toISOString()
386
+ writeFileSync(temporary, JSON.stringify({ ...record, ready_at: readyAt, last_progress_at: readyAt }) + '\n')
385
387
  renameSync(temporary, pidPath)
386
388
  }
387
389
 
@@ -484,6 +486,7 @@ let provenWritable = false
484
486
  async function guardedTick() {
485
487
  await beatBridge()
486
488
  const { attempted, failed } = await tick() ?? NOTHING_ATTEMPTED
489
+ updateBridgeProgress(pidPath)
487
490
  const decided = decideBridgeContinuation({ attempted, failed, provenWritable, failedTicks, limit: FAILED_TICK_LIMIT })
488
491
  provenWritable = decided.provenWritable
489
492
  failedTicks = decided.failedTicks