peertable 0.8.10 → 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.10",
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.10'
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:
@@ -173,10 +173,20 @@ for (const [name, judge] of [['wakeup', judgeWakeup], ['seat-status', judgeSeatS
173
173
  try {
174
174
  const probe = spawnSync('tmux', ['list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' })
175
175
  if (probe.error) throw probe.error
176
- line('OK', `job観測: tmux list-sessions 実行可(ランプ合成は有効。jobは peer-<name>-job* セッションで走らせること)`)
176
+ line('OK', `job観測: tmux list-sessions 実行可(ランプ合成は有効。ジョブは席のシェルからtmuxセッションで走らせる——名前は自由)`)
177
177
  } catch (error) {
178
178
  line('NG', `job観測: tmux list-sessions が失敗——預け仕事のランプ合成はこの端末で無効(${String(error.message ?? error).split('\n')[0]})`)
179
179
  }
180
+ // env帰属(2026-08-26 オーナー裁定: 帰属は規約名でなくOSの環境変数継承): この端末のtmux/psmuxが
181
+ // update-environment と show-environment を持つか。持たない実装(psmuxの互換欠け等)では
182
+ // 名前fallback(peer-<name>- 前置)だけで動くので、その事実を機械判定で出す。
183
+ try {
184
+ const opts = execFileSync('tmux', tmuxArgv(['show-options', '-g', 'update-environment'], { socket: resolveTmuxSocket(process.env).socket }), { encoding: 'utf8' })
185
+ if (/PEERTABLE_MEMBER/.test(opts)) line('OK', 'env帰属: update-environment に PEERTABLE_MEMBER が載っている(新規セッションへ持ち主が自動継承される)')
186
+ else line('NG', 'env帰属: update-environment に PEERTABLE_MEMBER が無い——launch-seat をこの版で一度通すと設定される。それまで帰属は peer-<name>- 前置の名前fallbackのみ')
187
+ } catch (error) {
188
+ line('NG', `env帰属: show-options が失敗——この端末のtmux/psmuxはenv帰属を判定できず、名前fallbackのみで動作(${String(error.message ?? error).split('\n')[0]})`)
189
+ }
180
190
  // 読了ack: server members に read_seq 欄が観測できるか(席が一度もackしていない間は判定できない)
181
191
  try {
182
192
  const res = JSON.parse(execFileSync('curl', ['-s', `${url}/api/${encodeURIComponent(room)}/members`], { encoding: 'utf8' }))
@@ -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
@@ -131,6 +140,7 @@ function readSeat(member, previous, observedAt) {
131
140
  const jobPaneHash = new Map() // `${name}:${session}` -> { hash, changedAt }
132
141
  const jobCpuSeconds = new Map() // `${name}:${session}` -> 前周期の累積CPU秒(root=job本体を含む)
133
142
  let jobObserveFailureLogged = false
143
+ let envAttributionFailureLogged = false
134
144
  function observeJob(socket, name, memberNames, envCache) {
135
145
  const listed = tmux(socket, 'list-sessions', '-F', '#{session_name}')
136
146
  if (listed === null) {
@@ -148,8 +158,19 @@ function observeJob(socket, name, memberNames, envCache) {
148
158
  const names = memberNames?.length ? memberNames : [name]
149
159
  const envOf = (session) => {
150
160
  if (envCache?.has(session)) return envCache.get(session)
151
- const out = tmux(socket, 'show-environment', '-t', session, 'PEERTABLE_MEMBER')
152
- const m = out === null ? null : /^PEERTABLE_MEMBER=(.+)$/m.exec(out)
161
+ // 変数名指定でなく全量listを読む: 「変数が無い」と「show-environment自体が使えない」
162
+ // (psmux等のtmux互換の欠け)を区別するため。使えない端末は名前fallbackで動き続けるが、
163
+ // その事実は黙らず一度だけ吠える(silent absence禁止)
164
+ const out = tmux(socket, 'show-environment', '-t', session)
165
+ if (out === null) {
166
+ if (!envAttributionFailureLogged) {
167
+ envAttributionFailureLogged = true
168
+ console.error('ENV_ATTRIBUTION_UNAVAILABLE: show-environment がこの端末のtmux/psmuxで失敗するため、預け仕事の帰属はsession envで判定できない(peer-<name>- 前置の名前fallbackのみで動作)')
169
+ }
170
+ envCache?.set(session, null)
171
+ return null
172
+ }
173
+ const m = /^PEERTABLE_MEMBER=(.+)$/m.exec(out)
153
174
  const value = m ? m[1].trim() : null
154
175
  envCache?.set(session, value)
155
176
  return value
@@ -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 {