peertable 0.4.31 → 0.4.32

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.4.31",
3
+ "version": "0.4.32",
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.4.31'
16
+ const MCP_VERSION = '0.4.32'
17
17
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
18
18
 
19
19
  const USAGE = `usage:
@@ -349,6 +349,7 @@ async function runDiagnostics(asJson) {
349
349
  'scripts/parent-watch-logic.mjs',
350
350
  'scripts/seat-credential.mjs',
351
351
  'scripts/ensure-room-mcp.mjs',
352
+ 'scripts/bridge-record-live.mjs',
352
353
  'scripts/leave-seat.sh',
353
354
  'scripts/change-seat.sh',
354
355
  'scripts/set-mission.sh',
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // 常駐 bridge の record が「この pid のこの process」を指しているかの境界。
3
+ // Windows は pid を再利用する。alive な pid だけでは本人ではない
4
+ // (2026-08-21 実測: wakeup-bridge.json の pid が oracle-mcp に再利用され、
5
+ // 本物の配達は死んだまま ensure-bridge が起動済みと誤認した)。
6
+ import { readFileSync } from 'node:fs'
7
+ import { resolve } from 'node:path'
8
+ import { fileURLToPath } from 'node:url'
9
+
10
+ const STALE_MS = 90_000
11
+
12
+ export function bridgeRecordLive(record, now = Date.now()) {
13
+ if (!record || typeof record !== 'object') return false
14
+ const pid = Number(record.pid)
15
+ if (!Number.isInteger(pid) || pid <= 0) return false
16
+ try { process.kill(pid, 0) } catch { return false }
17
+ const stamp = record.last_progress_at || record.progress_at
18
+ if (typeof stamp !== 'string' || stamp.length === 0) return false
19
+ const at = Date.parse(stamp)
20
+ if (!Number.isFinite(at)) return false
21
+ return (now - at) <= STALE_MS
22
+ }
23
+
24
+ const invokedDirectly = Boolean(process.argv[1])
25
+ && resolve(fileURLToPath(import.meta.url)).toLowerCase() === resolve(process.argv[1]).toLowerCase()
26
+ if (invokedDirectly) {
27
+ const path = process.argv[2]
28
+ if (!path) process.exit(2)
29
+ let record
30
+ try { record = JSON.parse(readFileSync(path, 'utf8')) } catch { process.exit(1) }
31
+ process.exit(bridgeRecordLive(record) ? 0 : 1)
32
+ }
@@ -17,8 +17,7 @@ if [ $# -eq 0 ] && [ -f "$record" ]; then
17
17
  fi
18
18
  fi
19
19
  if [ -f "$record" ]; then
20
- pid=$(node -e 'try{process.stdout.write(String(require(process.argv[1]).pid||""))}catch{}' "$record")
21
- if [ -n "$pid" ] && node "$(dirname "$0")/pid-alive.mjs" "$pid"; then exit 0; fi
20
+ if node "$(dirname "$0")/bridge-record-live.mjs" "$record"; then exit 0; fi
22
21
  if ! "$force" && grep -q 'WRITE_DENIED' "$log" 2>/dev/null; then echo "${name}-bridge: 前回はWRITE_DENIEDで終了。--forceを指定すること" >&2; exit 1; fi
23
22
  fi
24
23
  # shellcheck disable=SC1091
@@ -18,6 +18,7 @@ import { join } from 'node:path'
18
18
  import { promisify } from 'node:util'
19
19
  import { resolveSeatObservation, tmuxArgv } from './seat-usage.mjs'
20
20
  import { BROADCAST_RECIPIENT, formatWakeNotice, isIdleSelfWake, isWakeupBridgeTarget, shouldDeferGrokWake } from './wakeup-delivery.mjs'
21
+ import { bridgeRecordLive } from './bridge-record-live.mjs'
21
22
 
22
23
  const run = promisify(execFile)
23
24
  const [proj, ...rest] = process.argv.slice(2)
@@ -64,10 +65,32 @@ async function acquireStartupLock() {
64
65
  }
65
66
  }
66
67
 
68
+ function readRecord() {
69
+ try { return JSON.parse(readFileSync(record, 'utf8')) } catch { return null }
70
+ }
71
+
72
+ function writeRecord(next) {
73
+ const temp = `${record}.${process.pid}.tmp`
74
+ writeFileSync(temp, JSON.stringify(next) + '\n')
75
+ renameSync(temp, record)
76
+ }
77
+
78
+ function touchProgress() {
79
+ if (!existsSync(record)) return
80
+ const current = readRecord()
81
+ if (!current || current.pid !== process.pid) return
82
+ writeRecord({ ...current, last_progress_at: new Date().toISOString() })
83
+ }
84
+
67
85
  async function stopRecorded() {
68
86
  if (!existsSync(record)) return
69
- const { pid } = JSON.parse(readFileSync(record, 'utf8'))
70
- if (!alive(pid)) { unlinkSync(record); log(`死んだ記録を掃除した(pid ${pid})`); return }
87
+ const saved = readRecord()
88
+ const pid = Number(saved?.pid)
89
+ if (!bridgeRecordLive(saved)) {
90
+ unlinkSync(record)
91
+ log(`他人の pid または死んだ記録を掃除した(pid ${Number.isInteger(pid) ? pid : '不明'})。kill しない`)
92
+ return
93
+ }
71
94
  process.kill(pid, 'SIGTERM')
72
95
  for (let i = 0; i < 25 && alive(pid); i++) await sleep(200)
73
96
  if (alive(pid)) {
@@ -101,9 +124,10 @@ const parentName = (() => {
101
124
  }
102
125
  })()
103
126
  const targetOpts = { parentName }
104
- writeFileSync(record, JSON.stringify({
105
- pid: process.pid, room, server_url: url, requested_seats: requestedSeats, started_at: new Date().toISOString(),
106
- }) + '\n')
127
+ writeRecord({
128
+ pid: process.pid, room, server_url: url, requested_seats: requestedSeats,
129
+ started_at: new Date().toISOString(), last_progress_at: new Date().toISOString(),
130
+ })
107
131
  releaseStartupLock()
108
132
 
109
133
  const cleanup = () => { if (existsSync(record)) unlinkSync(record); process.exit(0) }
@@ -167,10 +191,12 @@ async function refreshMembers() {
167
191
  reconcileSeats()
168
192
  await refreshMembers()
169
193
  function markReady() {
170
- const next = { ...JSON.parse(readFileSync(record, 'utf8')), ready_at: new Date().toISOString() }
171
- const temp = `${record}.tmp`
172
- writeFileSync(temp, JSON.stringify(next) + '\n')
173
- renameSync(temp, record)
194
+ const current = readRecord() || {}
195
+ writeRecord({
196
+ ...current,
197
+ ready_at: new Date().toISOString(),
198
+ last_progress_at: new Date().toISOString(),
199
+ })
174
200
  }
175
201
 
176
202
  function loadDeliveryState() {
@@ -436,6 +462,7 @@ for (;;) {
436
462
  let buf = ''
437
463
  for await (const chunk of res.body) {
438
464
  lastByteAt = Date.now()
465
+ touchProgress()
439
466
  buf += Buffer.from(chunk).toString('utf8')
440
467
  const parts = buf.split('\n\n')
441
468
  buf = parts.pop()