peertable 0.8.11 → 0.8.12

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.11",
3
+ "version": "0.8.12",
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.11'
16
+ const MCP_VERSION = '0.8.12'
17
17
  const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
18
18
 
19
19
  const USAGE = `usage:
@@ -27,7 +27,7 @@ import { execFileSync } from 'node:child_process'
27
27
  import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs'
28
28
  import { join } from 'node:path'
29
29
 
30
- import { STOP_DECLARATION, attributeJobSession, combineSeatLamp, hasActiveDescendant, subtreeCpuSeconds, patrolTargets, classifyPaneTail, decideBridgeContinuation, deriveMissingSession, isPaneProcessStopped, parsePaneTokenHint, resolveLatticeExecutable, resolvePostToken, resolveSeatObservation, resolveTmuxSocket, supportsMemberObservation, tmuxArgv, tmuxPanePid } from './seat-usage.mjs'
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
31
 
32
32
  const args = process.argv.slice(2)
33
33
  const proj = args[0]
@@ -114,9 +114,18 @@ function readSeat(member, previous, observedAt) {
114
114
  const tentativeStatus = classifyPaneTail(tail)
115
115
  // pane 自体は生きたまま中の CLI プロセスだけが停止する局面(Lattice pull run の accept hold 等)を
116
116
  // 拾う。画面の残像だけでは idle に誤判定するため、idle と読めた時だけ実プロセスの stat を見る。
117
- const status = tentativeStatus === 'idle' && isPaneProcessStopped(tmuxPanePid(target.socket, target.target))
118
- ? 'dead'
119
- : tentativeStatus
117
+ let status = tentativeStatus
118
+ if (tentativeStatus === 'idle') {
119
+ const panePid = tmuxPanePid(target.socket, target.target)
120
+ if (isPaneProcessStopped(panePid)) status = 'dead'
121
+ else {
122
+ // harness死亡でshellだけ残った席(実被弾 2026-08-26: mio)。画面の残像はidleに見える
123
+ try {
124
+ const rows = execFileSync('ps', ['-axo', 'pid=,ppid=,command='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).split('\n')
125
+ if (isPaneHarnessMissing(rows, Number(panePid))) status = 'dead'
126
+ } catch { /* psが使えない端末では従来判定のまま */ }
127
+ }
128
+ }
120
129
  const busySince = status === 'busy' || status === 'blocked'
121
130
  ? ((previous?.status === 'busy' || previous?.status === 'blocked') && previous.busySince ? previous.busySince : observedAt)
122
131
  : null
@@ -236,6 +236,28 @@ export function tmuxPanePid(socket, target, exec = execFileSync) {
236
236
  * (`pane_dead=0`)で、末尾の画面は停止直前のまま残るため、`classifyPaneTail` だけでは
237
237
  * `idle` と誤判定する。
238
238
  */
239
+ /**
240
+ * paneのshellだけが生きていて中のharnessが死んだ席をdeadと判定する素材(実被弾 2026-08-26:
241
+ * mioのCodexが落ちて素のbashが露出、ランプはidle表示のまま番犬DMがshellへ打鍵され続けた)。
242
+ * pane processがshell(bash/zsh/sh系)で、子プロセスが1つも居なければ、席のharnessは存在しない。
243
+ * pane processがharness本体(shellでない)の場合はこの判定の対象外——それが生きていれば席は生きている。
244
+ */
245
+ export function isPaneHarnessMissing(psRows, panePid) {
246
+ if (!panePid) return false
247
+ let command = null
248
+ let hasChild = false
249
+ for (const row of psRows) {
250
+ const m = /^\s*(\d+)\s+(\d+)\s+(.+)$/.exec(row)
251
+ if (!m) continue
252
+ if (m[1] === String(panePid)) command = m[3].trim()
253
+ if (m[2] === String(panePid)) hasChild = true
254
+ }
255
+ if (command === null) return false
256
+ const bin = command.split(/\s+/)[0].split('/').pop()
257
+ if (!/^(?:-?bash|-?zsh|-?sh|-?dash|-?ksh)$/.test(bin)) return false
258
+ return !hasChild
259
+ }
260
+
239
261
  export function isPaneProcessStopped(panePid, exec = execFileSync) {
240
262
  if (!panePid) return false
241
263
  try {