peertable 0.8.55 → 0.8.57
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/README.ja.md +32 -33
- package/README.md +36 -37
- package/package.json +3 -1
- package/room/client.mjs +24 -33
- package/room/server.mjs +1 -1
- package/skill/SKILL.md +69 -94
- package/skill/scripts/aiterm-client.mjs +48 -0
- package/skill/scripts/aiterm-deliver.mjs +3 -1
- package/skill/scripts/change-seat.mjs +86 -0
- package/skill/scripts/change-seat.sh +3 -276
- package/skill/scripts/change-seat.test.mjs +129 -0
- package/skill/scripts/cli.mjs +77 -0
- package/skill/scripts/doctor.mjs +53 -0
- package/skill/scripts/doctor.sh +2 -213
- package/skill/scripts/ensure-all-bridges.mjs +42 -0
- package/skill/scripts/ensure-all-bridges.sh +3 -80
- package/skill/scripts/ensure-bridge.mjs +11 -0
- package/skill/scripts/ensure-bridge.sh +2 -95
- package/skill/scripts/ensure-codex-room-mcp.mjs +1 -1
- package/skill/scripts/ensure-project-runtime.mjs +85 -0
- package/skill/scripts/ensure-project-runtime.sh +2 -11
- package/skill/scripts/ensure-project-runtime.test.mjs +40 -0
- package/skill/scripts/install-skill.mjs +89 -0
- package/skill/scripts/install-skill.test.mjs +84 -0
- package/skill/scripts/launch-seat.mjs +163 -0
- package/skill/scripts/launch-seat.sh +3 -797
- package/skill/scripts/launch-seat.test.mjs +174 -0
- package/skill/scripts/leave-seat.mjs +44 -0
- package/skill/scripts/leave-seat.sh +3 -92
- package/skill/scripts/leave-seat.test.mjs +64 -0
- package/skill/scripts/legacy-entry.mjs +33 -0
- package/skill/scripts/platform/windows/resolve-lattice-command.mjs +12 -1
- package/skill/scripts/project-runtime.mjs +95 -0
- package/skill/scripts/project-runtime.test.mjs +29 -0
- package/skill/scripts/project-scaffold.mjs +179 -0
- package/skill/scripts/project-scaffold.test.mjs +129 -0
- package/skill/scripts/refresh-seat-identity.mjs +11 -8
- package/skill/scripts/remove-managed-room-mcp.mjs +9 -4
- package/skill/scripts/resume.sh +2 -225
- package/skill/scripts/room-api.mjs +28 -0
- package/skill/scripts/room-public-session.test.mjs +73 -0
- package/skill/scripts/runtime-contract.test.mjs +13 -146
- package/skill/scripts/runtime-launch-command.mjs +19 -0
- package/skill/scripts/runtime-launch-command.test.mjs +32 -0
- package/skill/scripts/seat-approval.mjs +24 -0
- package/skill/scripts/seat-approval.test.mjs +43 -0
- package/skill/scripts/seat-identity.mjs +2 -71
- package/skill/scripts/seat-launch-phase.mjs +11 -0
- package/skill/scripts/seat-observer.mjs +54 -0
- package/skill/scripts/seat-observer.test.mjs +58 -0
- package/skill/scripts/seat-session.mjs +17 -0
- package/skill/scripts/seat-status-bridge.mjs +33 -206
- package/skill/scripts/seat-usage.mjs +3 -334
- package/skill/scripts/setup.sh +3 -215
- package/skill/scripts/teardown.mjs +113 -0
- package/skill/scripts/teardown.sh +3 -410
- package/skill/scripts/teardown.test.mjs +62 -0
- package/skill/scripts/upgrade-team-assets.mjs +268 -0
- package/skill/scripts/upgrade-team-assets.sh +2 -282
- package/skill/scripts/wakeup-bridge.mjs +26 -32
- package/skill/scripts/wakeup-delivery.mjs +4 -14
- package/skill/templates/charter.md +1 -1
- package/skill/templates/member-standalone.md +1 -1
- package/skill/templates/member.md +3 -3
- package/skill/scripts/agent-pane-status.mjs +0 -6
- package/skill/scripts/aiterm-configure.mjs +0 -31
- package/skill/scripts/aiterm-launch.mjs +0 -64
- package/skill/scripts/aiterm-send.mjs +0 -22
- package/skill/scripts/claude-dialog.mjs +0 -26
- package/skill/scripts/codex-dialog.mjs +0 -67
- package/skill/scripts/platform/windows/build-bridge-command.mjs +0 -52
- package/skill/scripts/seat-input.mjs +0 -36
- package/skill/scripts/tmux-at.bash +0 -22
- package/skill/scripts/tmux-socket.mjs +0 -19
- package/skill/scripts/vendors/grok/pane-status.mjs +0 -30
|
@@ -1,33 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
// usage: seat-status-bridge.mjs <project_dir> [--interval <sec>] [--once]
|
|
4
|
-
// seat-status-bridge.mjs <project_dir> --stop
|
|
5
|
-
//
|
|
6
|
-
// AI は使わない。読むのは `tmux capture-pane -p` の末尾だけで、席へは1バイトも送らない。
|
|
7
|
-
//
|
|
8
|
-
// 判定(2026-08-08 実測。**推測でパターンを書かない**):
|
|
9
|
-
// busy = pane の末尾に `esc to interrupt` が在る。Claude 席のステータス行にも Codex 席の `Working (…)` にも
|
|
10
|
-
// 同じ文字列が入る。Grok はこれを出さず `Waiting for response` / `Responding…` / `[stop]` を出す
|
|
11
|
-
// (2026-08-21 実測。これを見ないと作業中の Grok 席が idle に見える)。実行中の動名詞
|
|
12
|
-
// (Cogitating/Coalescing/Effecting/Gallivanting/Fermenting/Symbioting…)は**毎回変わる**ので
|
|
13
|
-
// 判定に使わない——語で照合すると全席を idle と誤判定して、画面が嘘をつく
|
|
14
|
-
// blocked = 既知の承認ダイアログ文言が末尾に在る(`esc to interrupt` は消えている)。2026-08-08 実測:
|
|
15
|
-
// 確認ダイアログで停止した席が busy と `esc to interrupt` 不在から idle 側に誤判定され、
|
|
16
|
-
// 動けない席へ卓が代走を申し出るところまで進んだ。判定順は busy → blocked → idle
|
|
17
|
-
// (承認プロンプト表示中は `esc to interrupt` が消えるので、この順で正しい)
|
|
18
|
-
// dead = tmux セッションが無い/`pane_dead=1`/画面は idle 風だが中の CLI プロセスが停止
|
|
19
|
-
// (`ps` stat 先頭 `T`。Lattice pull run の accept hold が attach 済み worker へ
|
|
20
|
-
// SIGSTOP を送る局面等。2026-08-11 実測)
|
|
21
|
-
// idle = 生きていて busy でも blocked でもない
|
|
22
|
-
//
|
|
23
|
-
// 送信は「変化した時」+「変化が無くても心拍」の2本立て。変化時だけだと、**bridge が死んだのか状態が
|
|
24
|
-
// 変わっていないのかを server が区別できない**(決定58 の liveness と cursor の分離と同じ形)。
|
|
25
|
-
// server 側は最終受信からの経過で `unknown` へ落とす——古い状態を出し続けるのが最悪だから。
|
|
2
|
+
// Aitermの公開観測をroomの稼働表示と継続番犬へつなぐ。
|
|
26
3
|
import { execFileSync } from 'node:child_process'
|
|
27
4
|
import { existsSync, readFileSync, renameSync, writeFileSync, unlinkSync } from 'node:fs'
|
|
28
5
|
import { join } from 'node:path'
|
|
29
6
|
|
|
30
|
-
import { STOP_DECLARATION,
|
|
7
|
+
import { STOP_DECLARATION, patrolTargets, decideBridgeContinuation, resolveLatticeExecutable, resolvePostToken, supportsMemberObservation } from './seat-usage.mjs'
|
|
8
|
+
import { AitermClient } from './aiterm-client.mjs'
|
|
9
|
+
import { SeatObserver } from './seat-observer.mjs'
|
|
31
10
|
import { updateBridgeProgress } from './bridge-record-live.mjs'
|
|
32
11
|
|
|
33
12
|
const args = process.argv.slice(2)
|
|
@@ -52,10 +31,10 @@ if (stop) {
|
|
|
52
31
|
// SIGTERM 5秒 → SIGKILL 3秒(wakeup-bridge.mjs と同じ形)。**昇格が無いと、SIGTERM を無視する
|
|
53
32
|
// 常駐が居た時に teardown が `set -e` の2段目で即死して、[未実施] も [手当] も要約も出ない**
|
|
54
33
|
process.kill(pid, 'SIGTERM')
|
|
55
|
-
for (let i = 0; i < 50 && alive(pid); i++)
|
|
34
|
+
for (let i = 0; i < 50 && alive(pid); i++) await new Promise(resolve => setTimeout(resolve, 100))
|
|
56
35
|
if (alive(pid)) {
|
|
57
36
|
process.kill(pid, 'SIGKILL')
|
|
58
|
-
for (let i = 0; i < 30 && alive(pid); i++)
|
|
37
|
+
for (let i = 0; i < 30 && alive(pid); i++) await new Promise(resolve => setTimeout(resolve, 100))
|
|
59
38
|
}
|
|
60
39
|
if (alive(pid)) { console.error(`SEAT_STATUS_BRIDGE_STOP_FAILED: pid ${pid} が SIGKILL でも止まらない`); process.exit(1) }
|
|
61
40
|
}
|
|
@@ -80,132 +59,14 @@ const room = setup.room
|
|
|
80
59
|
const token = resolvePostToken(process.env)
|
|
81
60
|
writeFileSync(pidPath, JSON.stringify({ pid: process.pid, started_at: new Date().toISOString() }) + '\n')
|
|
82
61
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
let defaultSocketResolution = null
|
|
86
|
-
let resolutionLogged = false
|
|
62
|
+
const aiterm = new AitermClient()
|
|
63
|
+
const observer = new SeatObserver(aiterm, room)
|
|
87
64
|
let readyRecorded = false
|
|
88
|
-
const defaultSocket = () => {
|
|
89
|
-
if (defaultSocketResolution === null) defaultSocketResolution = resolveTmuxSocket(process.env)
|
|
90
|
-
return defaultSocketResolution.socket
|
|
91
|
-
}
|
|
92
|
-
const tmux = (socket, ...a) => { try { return execFileSync('tmux', tmuxArgv(a, { socket }), { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }) } catch { return null } }
|
|
93
65
|
|
|
94
|
-
// 監視するのは room の members に居る席だけ。tmux の `peer-*` を全部拾うと、同じマシンで走る別の卓を晒す
|
|
95
66
|
async function seats() {
|
|
96
67
|
const res = await fetch(`${url}/api/${encodeURIComponent(room)}/members`)
|
|
97
|
-
|
|
98
|
-
return members
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function readSeat(member, previous, observedAt) {
|
|
102
|
-
// まず記述子だけで解決する。socket が無い記述子/記述子なしの場合だけ既定を調べる。
|
|
103
|
-
const target = resolveSeatObservation(member, null) ?? resolveSeatObservation(member, defaultSocket())
|
|
104
|
-
if (target === null) return deriveMissingSession(previous)
|
|
105
|
-
const dead = tmux(target.socket, 'list-panes', '-t', target.target, '-F', '#{pane_dead}')
|
|
106
|
-
// セッションが見つからない。tmux 席を持たない member(親など)は一度も観測できないので送らない
|
|
107
|
-
// (`previous` が無い=一度も観測できていない)。過去に観測できていた席が消えたなら実際に落ちた
|
|
108
|
-
if (dead === null) return deriveMissingSession(previous)
|
|
109
|
-
if (dead.trim().split('\n')[0] === '1') {
|
|
110
|
-
return { status: 'dead', busySince: null, paneTokenHint: null }
|
|
111
|
-
}
|
|
112
|
-
const pane = tmux(target.socket, 'capture-pane', '-t', target.target, '-p')
|
|
113
|
-
if (pane === null) return { status: 'dead', busySince: null, paneTokenHint: null }
|
|
114
|
-
const tail = paneStatusTail(pane)
|
|
115
|
-
const tentativeStatus = classifyPaneTail(tail)
|
|
116
|
-
// pane 自体は生きたまま中の CLI プロセスだけが停止する局面(Lattice pull run の accept hold 等)を
|
|
117
|
-
// 拾う。画面の残像だけでは idle に誤判定するため、idle と読めた時だけ実プロセスの stat を見る。
|
|
118
|
-
let status = tentativeStatus
|
|
119
|
-
if (tentativeStatus === 'idle') {
|
|
120
|
-
const panePid = tmuxPanePid(target.socket, target.target)
|
|
121
|
-
if (isPaneProcessStopped(panePid)) status = 'dead'
|
|
122
|
-
else {
|
|
123
|
-
// harness死亡でshellだけ残った席(実被弾 2026-08-26: mio)。画面の残像はidleに見える
|
|
124
|
-
try {
|
|
125
|
-
const rows = execFileSync('ps', ['-axo', 'pid=,ppid=,command='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).split('\n')
|
|
126
|
-
if (isPaneHarnessMissing(rows, Number(panePid))) status = 'dead'
|
|
127
|
-
} catch { /* psが使えない端末では従来判定のまま */ }
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
const busySince = status === 'busy' || status === 'blocked'
|
|
131
|
-
? ((previous?.status === 'busy' || previous?.status === 'blocked') && previous.busySince ? previous.busySince : observedAt)
|
|
132
|
-
: null
|
|
133
|
-
return { status, busySince, paneTokenHint: parsePaneTokenHint(tail) }
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ---- 預け仕事(jobセッション)の観測(2026-08-25 オーナー裁定)----
|
|
137
|
-
// 席が常駐・長時間ジョブを `peer-<名前>-job*` の名前のtmuxセッションで走らせる取り決めを前提に、
|
|
138
|
-
// その実物を毎周期観測してランプへ合成する。登録・宣言は一切見ない——観測だけ。
|
|
139
|
-
// active判定は「画面内容が前周期から変わった」こと。出力を出さない計算中はidle表示になるが、
|
|
140
|
-
// それは観測できる事実の正直な表示であり、推測で点滅させない。
|
|
141
|
-
const jobPaneHash = new Map() // `${name}:${session}` -> { hash, changedAt }
|
|
142
|
-
const jobCpuSeconds = new Map() // `${name}:${session}` -> 前周期の累積CPU秒(root=job本体を含む)
|
|
143
|
-
let jobObserveFailureLogged = false
|
|
144
|
-
let envAttributionFailureLogged = false
|
|
145
|
-
function observeJob(socket, name, memberNames, envCache) {
|
|
146
|
-
const listed = tmux(socket, 'list-sessions', '-F', '#{session_name}')
|
|
147
|
-
if (listed === null) {
|
|
148
|
-
// 「jobなし」と「観測手段が壊れている」を同じ顔にしない(silent absence禁止・2026-08-25 オーナー裁定)。
|
|
149
|
-
// この端末でjob観測が成立しない場合、ランプ合成は席paneのみで動いていることを明示的に吠える。
|
|
150
|
-
if (!jobObserveFailureLogged) {
|
|
151
|
-
jobObserveFailureLogged = true
|
|
152
|
-
console.error('JOB_OBSERVE_UNAVAILABLE: tmux list-sessions が失敗するため、預け仕事のランプ合成はこの端末で無効(席paneのみで表示)。以後この警告は繰り返さない')
|
|
153
|
-
}
|
|
154
|
-
return { alive: false, active: false }
|
|
155
|
-
}
|
|
156
|
-
jobObserveFailureLogged = false
|
|
157
|
-
// 帰属はsession envのPEERTABLE_MEMBER(OSの継承をtmux update-environmentが写した実物)が正。
|
|
158
|
-
// 名前は自由——`-job` 規約は廃止(2026-08-26 オーナー裁定)。env読みは周期内cacheで1セッション1回。
|
|
159
|
-
const names = memberNames?.length ? memberNames : [name]
|
|
160
|
-
const envOf = (session) => {
|
|
161
|
-
if (envCache?.has(session)) return envCache.get(session)
|
|
162
|
-
// 変数名指定でなく全量listを読む: 「変数が無い」と「show-environment自体が使えない」
|
|
163
|
-
// (psmux等のtmux互換の欠け)を区別するため。使えない端末は名前fallbackで動き続けるが、
|
|
164
|
-
// その事実は黙らず一度だけ吠える(silent absence禁止)
|
|
165
|
-
const out = tmux(socket, 'show-environment', '-t', session)
|
|
166
|
-
if (out === null) {
|
|
167
|
-
if (!envAttributionFailureLogged) {
|
|
168
|
-
envAttributionFailureLogged = true
|
|
169
|
-
console.error('ENV_ATTRIBUTION_UNAVAILABLE: show-environment がこの端末のtmux/psmuxで失敗するため、預け仕事の帰属はsession envで判定できない(peer-<name>- 前置の名前fallbackのみで動作)')
|
|
170
|
-
}
|
|
171
|
-
envCache?.set(session, null)
|
|
172
|
-
return null
|
|
173
|
-
}
|
|
174
|
-
const m = /^PEERTABLE_MEMBER=(.+)$/m.exec(out)
|
|
175
|
-
const value = m ? m[1].trim() : null
|
|
176
|
-
envCache?.set(session, value)
|
|
177
|
-
return value
|
|
178
|
-
}
|
|
179
|
-
const sessions = listed.split('\n').filter(Boolean)
|
|
180
|
-
.filter(session => attributeJobSession({ session, memberNames: names, env: envOf(session) }) === name)
|
|
181
|
-
if (sessions.length === 0) return { alive: false, active: false }
|
|
182
|
-
let active = false
|
|
183
|
-
let psRows = null
|
|
184
|
-
for (const session of sessions) {
|
|
185
|
-
const pane = tmux(socket, 'capture-pane', '-t', session, '-p')
|
|
186
|
-
if (pane === null) continue
|
|
187
|
-
const hash = String(pane.length) + ':' + pane.slice(-256)
|
|
188
|
-
const key = `${name}:${session}`
|
|
189
|
-
const prev = jobPaneHash.get(key)
|
|
190
|
-
if (!prev || prev.hash !== hash) { jobPaneHash.set(key, { hash }); active = true }
|
|
191
|
-
// 画面に何も出さず働くジョブ(checkpointだけ書く収集等)は画面hashでは稼働に見えない
|
|
192
|
-
// (実被弾 2026-08-25: 収集が毎分書き込み中なのにランプが点灯止まり)。jobセッションの
|
|
193
|
-
// プロセスツリーのCPU実働も稼働として合成する。セッション全体が預け仕事なので足場除外は不要。
|
|
194
|
-
if (!active) {
|
|
195
|
-
const jobPanePid = tmuxPanePid(socket, session)
|
|
196
|
-
if (jobPanePid) {
|
|
197
|
-
try {
|
|
198
|
-
psRows ??= execFileSync('ps', ['-axo', 'pid=,ppid=,time=,etime='], { encoding: 'utf8' }).split('\n')
|
|
199
|
-
// pcpu%はIO待ち主体のジョブで0.0に丸まる。累積CPU時間の前周期差分で「変化」を見る
|
|
200
|
-
const seconds = subtreeCpuSeconds(psRows, Number(jobPanePid), { includeRoot: true })
|
|
201
|
-
const prevSeconds = jobCpuSeconds.get(key)
|
|
202
|
-
jobCpuSeconds.set(key, seconds)
|
|
203
|
-
if (prevSeconds !== undefined && seconds > prevSeconds) active = true
|
|
204
|
-
} catch { /* psが使えない端末では画面hash判定だけで続行 */ }
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
return { alive: true, active }
|
|
68
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
69
|
+
return (await res.json()).members
|
|
209
70
|
}
|
|
210
71
|
|
|
211
72
|
async function send(name, observation, observedAt) {
|
|
@@ -286,7 +147,6 @@ const patrolNagStreak = new Map() // seat -> 連続催促数(催促条件が
|
|
|
286
147
|
const patrolEscalated = new Set() // 同一縮退エピソードでの親通報は1回だけ
|
|
287
148
|
const busyStartedAt = new Map() // seat -> epoch_ms(このbridgeプロセスが観測した最後のターン開始・pane基準)
|
|
288
149
|
const paneLast = new Map() // seat -> 直前周期のpane生status(番犬系専用。表示合成とは分離)
|
|
289
|
-
const paneJobCpuSeconds = new Map() // seat -> 前周期の非足場子孫の累積CPU秒
|
|
290
150
|
let lastPatrolAt = 0
|
|
291
151
|
async function patrolClaims() {
|
|
292
152
|
if (setup.mode !== 'lattice' || !setup.plan_key) return
|
|
@@ -390,28 +250,6 @@ const tokenBucket = value => value === null ? null : Math.floor(value / 1_000)
|
|
|
390
250
|
// 数えないと、失敗を1行ずつ吐きながら永久に常駐する(2026-08-10 に4時間そうなった)
|
|
391
251
|
const NOTHING_ATTEMPTED = { attempted: 0, failed: 0 }
|
|
392
252
|
|
|
393
|
-
function hasDescriptor(member) {
|
|
394
|
-
return member?.observe && typeof member.observe === 'object'
|
|
395
|
-
&& typeof member.observe.tmux_target === 'string' && member.observe.tmux_target.length > 0
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function logResolution(members) {
|
|
399
|
-
if (resolutionLogged) return
|
|
400
|
-
resolutionLogged = true
|
|
401
|
-
const descriptors = members.filter(hasDescriptor).length
|
|
402
|
-
const legacy = members.length - descriptors
|
|
403
|
-
if (legacy === 0) {
|
|
404
|
-
console.error(`seat-status-bridge: 観測記述子 ${descriptors} 席、legacy 0 席、既定 socket は未使用`)
|
|
405
|
-
return
|
|
406
|
-
}
|
|
407
|
-
const resolved = defaultSocketResolution ?? resolveTmuxSocket(process.env)
|
|
408
|
-
defaultSocketResolution = resolved
|
|
409
|
-
const detail = resolved.error
|
|
410
|
-
? `${resolved.source} (${resolved.error.code}: ${resolved.candidates.join(', ')})`
|
|
411
|
-
: `${resolved.source} (${resolved.socket})`
|
|
412
|
-
console.error(`seat-status-bridge: 観測記述子 ${descriptors} 席、legacy ${legacy} 席、既定 socket=${detail}`)
|
|
413
|
-
}
|
|
414
|
-
|
|
415
253
|
function recordReady() {
|
|
416
254
|
if (readyRecorded) return
|
|
417
255
|
readyRecorded = true
|
|
@@ -432,51 +270,31 @@ async function tick() {
|
|
|
432
270
|
// 保持する版かどうかは GET で分かるので、**分かるまで投げない**。
|
|
433
271
|
if (supported === null) {
|
|
434
272
|
try { supported = await serverKeepsStatus() } catch { return NOTHING_ATTEMPTED } // 判定できない間は送らない
|
|
435
|
-
recordReady()
|
|
436
273
|
if (!supported) console.error('seat-status-bridge: この room サーバーは稼働状態を保持しない版(GET /members に status が無い)。送信すると保存されないうえに system 発言を撒くので、送信しない')
|
|
437
274
|
}
|
|
438
|
-
logResolution(members)
|
|
439
275
|
if (!supported) { console.error(`seat-status-bridge: ${members.length} 席を見たが、server が未対応なので送っていない`); return NOTHING_ATTEMPTED }
|
|
440
276
|
const now = Date.now()
|
|
441
277
|
const observedAt = new Date(now).toISOString()
|
|
442
|
-
const
|
|
443
|
-
|
|
278
|
+
const observations = await observer.cycle(members, last, observedAt)
|
|
279
|
+
recordReady()
|
|
444
280
|
let sent = 0
|
|
445
281
|
let skipped = 0
|
|
446
282
|
let failed = 0
|
|
447
283
|
for (const member of members) {
|
|
448
284
|
const { name } = member
|
|
449
285
|
const prev = last.get(name)
|
|
450
|
-
const observation =
|
|
451
|
-
//
|
|
452
|
-
if (observation
|
|
286
|
+
const observation = observations.get(name)
|
|
287
|
+
// Aiterm席を持たない member(親など)は一度も観測できないので送らない
|
|
288
|
+
if (!observation) { skipped++; continue }
|
|
289
|
+
if (observation.error) {
|
|
290
|
+
failed++
|
|
291
|
+
console.error(`seat-status-bridge: ${name} の観測失敗: ${observation.error.code}: ${observation.error.message}`)
|
|
292
|
+
continue
|
|
293
|
+
}
|
|
453
294
|
// 番犬(ターン終了検知・busy履歴)はpaneの生状態だけを読む。表示用の合成ランプ(job込み)を
|
|
454
295
|
// ここへ流すと、静かなジョブの画面出力の間欠でランプがbusy⇄idleに揺れ、その揺れを
|
|
455
296
|
// 「ターン終了」と誤認して正当待機の席へ[継続]を撃つ(実被弾 2026-08-25 #175: mio誤起床)。
|
|
456
|
-
const paneStatus = observation.
|
|
457
|
-
{
|
|
458
|
-
const target = resolveSeatObservation(member, null) ?? resolveSeatObservation(member, defaultSocket())
|
|
459
|
-
if (target !== null) {
|
|
460
|
-
const job = observeJob(target.socket, member.name, memberNames, envCache)
|
|
461
|
-
// 席paneの子孫プロセス(Codex内蔵background terminal等)の実働もjob稼働として合成する
|
|
462
|
-
if (!job.active && observation.status === 'idle') {
|
|
463
|
-
const panePid = tmuxPanePid(target.socket, target.target)
|
|
464
|
-
if (panePid) {
|
|
465
|
-
try {
|
|
466
|
-
const rows = execFileSync('ps', ['-axo', 'pid=,ppid=,time=,etime='], { encoding: 'utf8' }).split('\n')
|
|
467
|
-
// 足場(席と同時起動)を除外し、後から生まれた子孫の累積CPU秒の前周期差分で稼働判定
|
|
468
|
-
const seconds = subtreeCpuSeconds(rows, Number(panePid), {
|
|
469
|
-
includeChild: (childAge, rootAge) => rootAge != null && childAge != null && rootAge - childAge >= 60,
|
|
470
|
-
})
|
|
471
|
-
const prevSeconds = paneJobCpuSeconds.get(member.name)
|
|
472
|
-
paneJobCpuSeconds.set(member.name, seconds)
|
|
473
|
-
if (prevSeconds !== undefined && seconds > prevSeconds) job.active = true
|
|
474
|
-
} catch { /* psが失敗する端末では子孫観測なしで続行(named session観測は生きている) */ }
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
observation.status = combineSeatLamp(observation.status, job)
|
|
478
|
-
}
|
|
479
|
-
}
|
|
297
|
+
const paneStatus = observation.paneStatus
|
|
480
298
|
const changed = !prev || prev.status !== observation.status
|
|
481
299
|
|| prev.busySince !== observation.busySince
|
|
482
300
|
// token表示は実行中に細かく増える。1k未満の差で8秒ごとにPOSTせず、表示精度に合う粒度で送る。
|
|
@@ -498,7 +316,7 @@ async function tick() {
|
|
|
498
316
|
}
|
|
499
317
|
}
|
|
500
318
|
// 0件でも0件と言う(条件付きログにしない。沈黙する失敗を作らない・決定58)
|
|
501
|
-
console.error(`seat-status-bridge: ${members.length} 席を見て ${sent} 件送った(
|
|
319
|
+
console.error(`seat-status-bridge: ${members.length} 席を見て ${sent} 件送った(Aiterm席を持たず観測対象外: ${skipped})`)
|
|
502
320
|
await patrolClaims()
|
|
503
321
|
return { attempted: sent + failed, failed }
|
|
504
322
|
}
|
|
@@ -540,5 +358,14 @@ async function guardedTick() {
|
|
|
540
358
|
process.on('SIGTERM', () => { try { unlinkSync(pidPath) } catch {} process.exit(0) })
|
|
541
359
|
process.on('SIGINT', () => { try { unlinkSync(pidPath) } catch {} process.exit(0) })
|
|
542
360
|
|
|
543
|
-
|
|
544
|
-
|
|
361
|
+
try {
|
|
362
|
+
await guardedTick()
|
|
363
|
+
while (!once) {
|
|
364
|
+
await new Promise(resolve => setTimeout(resolve, interval * 1000))
|
|
365
|
+
await guardedTick()
|
|
366
|
+
}
|
|
367
|
+
} catch (error) {
|
|
368
|
+
die('SEAT_STATUS_BRIDGE_OBSERVATION_FAILED', error.message)
|
|
369
|
+
} finally {
|
|
370
|
+
await aiterm.close()
|
|
371
|
+
}
|