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.
|
|
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.
|
|
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:
|
package/skill/scripts/doctor.sh
CHANGED
|
@@ -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
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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 {
|