peertable 0.8.28 → 0.8.30
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 +2 -2
- package/package.json +1 -1
- package/room/client.mjs +1 -1
- package/room/server.mjs +54 -10
- package/skill/scripts/wakeup-bridge.mjs +45 -39
package/README.ja.md
CHANGED
|
@@ -113,9 +113,9 @@ Windows工場hostはPowerShell 7(`pwsh.exe`)を前提とし、5.1しかな
|
|
|
113
113
|
|
|
114
114
|
動いており、**自分自身の開発に使っている**。2026-08-08 に end-to-end 検証済み——オーケストレーターなしの完全な一周(2 メンバーが相談し、claim し、インターフェースを交渉し、見つけた罠を共有して小さなプロジェクトを出荷)を**外部介入ゼロ**で完走。2026-08-13の実席ライフサイクルでは、作業席が親を通じてsession contextを保ったままmodel / effortを変更し、再起動後はroomと工程正本から再着任した。2026-08-14にはGrok 4.6席の着席、room参加、同一sessionの4.6↔4.5変更、DM起床を実機で確認した。2026-08-17にGrok席はidle待ち、broadcastは本文を残し、tmuxの無い親でbridge cursorが止まらないよう直した。
|
|
115
115
|
|
|
116
|
-
現在のnpm releaseは **peertable 0.
|
|
116
|
+
現在のnpm releaseは **peertable 0.8.30**。
|
|
117
117
|
|
|
118
|
-
設計文書と決定履歴(**
|
|
118
|
+
設計文書と決定履歴(**129 決定**)は [docs/plan.md](docs/plan.md)。
|
|
119
119
|
|
|
120
120
|
Claude Code channels はリサーチプレビューのため、フラグ・プロトコルは変わりうる。
|
|
121
121
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peertable",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.30",
|
|
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
|
@@ -14,7 +14,7 @@ import { boundedRecent, boundedUnread } from './message-bounds.mjs'
|
|
|
14
14
|
|
|
15
15
|
// client.mjs 側のハードコード版数。package.json の version と一致していることを
|
|
16
16
|
// diagnostics の version_consistency が見る(2 つの版数源の drift 検出。決定45)
|
|
17
|
-
const MCP_VERSION = '0.8.
|
|
17
|
+
const MCP_VERSION = '0.8.30'
|
|
18
18
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
19
19
|
|
|
20
20
|
const USAGE = `usage:
|
package/room/server.mjs
CHANGED
|
@@ -235,6 +235,40 @@ function effectiveStatus(member, bridges, now = Date.now()) {
|
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
// 状態点とは別に、閲覧者が「いま何をしているか」を読める短い表示をserverで一元生成する。
|
|
239
|
+
// idle/dead/blocked は過去のmissionや発言を現在形で再表示しない。busyだけ、本人の最新の
|
|
240
|
+
// [次の行動] / [claim] / [引受] / [作業中] を現在作業として採る。
|
|
241
|
+
const ACTIVITY_PREFIX = /^\[(?:次の行動|claim|引受|作業中)\]\s*/i
|
|
242
|
+
function compactActivity(body) {
|
|
243
|
+
const plain = String(body).replace(ACTIVITY_PREFIX, '').replace(/\s+/g, ' ').trim()
|
|
244
|
+
const chars = [...plain]
|
|
245
|
+
return chars.length <= 120 ? plain : `${chars.slice(0, 119).join('')}…`
|
|
246
|
+
}
|
|
247
|
+
function readActivityMessages(room) {
|
|
248
|
+
if (!existsSync(room.logPath)) return { messages: [], readable: true }
|
|
249
|
+
let readable = true
|
|
250
|
+
const messages = []
|
|
251
|
+
for (const line of readFileSync(room.logPath, 'utf8').split('\n').filter(Boolean)) {
|
|
252
|
+
try { messages.push(JSON.parse(line)) } catch { readable = false }
|
|
253
|
+
}
|
|
254
|
+
return { messages, readable }
|
|
255
|
+
}
|
|
256
|
+
function memberActivity(member, effective, activityLog) {
|
|
257
|
+
const status = effective.status_effective
|
|
258
|
+
if (status === 'idle') return { activity_text: '待機中', activity_at: null }
|
|
259
|
+
if (status === 'dead') return { activity_text: 'セッション停止', activity_at: null }
|
|
260
|
+
if (status === 'blocked') return { activity_text: '承認操作待ち', activity_at: null }
|
|
261
|
+
if (status !== 'busy') return { activity_text: '稼働状態を確認できません', activity_at: null }
|
|
262
|
+
for (let index = activityLog.messages.length - 1; index >= 0; index--) {
|
|
263
|
+
const message = activityLog.messages[index]
|
|
264
|
+
if (message.from !== member.name || typeof message.body !== 'string' || !ACTIVITY_PREFIX.test(message.body)) continue
|
|
265
|
+
const text = compactActivity(message.body)
|
|
266
|
+
if (text) return { activity_text: text, activity_at: message.ts ?? null }
|
|
267
|
+
}
|
|
268
|
+
if (!activityLog.readable) return { activity_text: '作業情報を取得できません', activity_at: null }
|
|
269
|
+
return { activity_text: '作業内容未報告', activity_at: null }
|
|
270
|
+
}
|
|
271
|
+
|
|
238
272
|
// ---- 配送状態の導出(決定102)-----------------------------------------------------
|
|
239
273
|
// receipt(wakeup-bridge の実投入記録)が正。無い宛先は member 台帳と bridge 台帳から
|
|
240
274
|
// pending / seat_unavailable / bridge_unavailable を導出する。room_saved だけでは配達と言わない。
|
|
@@ -353,12 +387,16 @@ http.createServer(async (req, res) => {
|
|
|
353
387
|
if (req.method === 'GET' && rest === 'members') {
|
|
354
388
|
const now = Date.now()
|
|
355
389
|
const bridges = bridgeHealth(room.name, now)
|
|
390
|
+
const activityLog = readActivityMessages(room)
|
|
356
391
|
return json(res, 200, {
|
|
357
|
-
members: listMembers(room.name).map(m =>
|
|
392
|
+
members: listMembers(room.name).map(m => {
|
|
393
|
+
const effective = effectiveStatus(m, bridges, now)
|
|
394
|
+
return { ...m, ...effective, ...memberActivity(m, effective, activityLog) }
|
|
395
|
+
}),
|
|
358
396
|
bridges,
|
|
359
397
|
capabilities: {
|
|
360
398
|
member_observation_v1: true, member_ledger_v1: true,
|
|
361
|
-
effective_status_v1: true, delivery_receipt_v1: true,
|
|
399
|
+
effective_status_v1: true, member_activity_v1: true, delivery_receipt_v1: true,
|
|
362
400
|
},
|
|
363
401
|
}, CORS)
|
|
364
402
|
}
|
|
@@ -524,12 +562,14 @@ const UI = room => `<!doctype html><html lang="ja"><head><meta charset="utf-8"><
|
|
|
524
562
|
<title>${esc(room)} · Peertable</title>${FAVICON}<style>${STYLE}
|
|
525
563
|
.top{position:sticky;top:0;z-index:2;background:var(--bg);border-bottom:1px solid var(--line);padding:12px 16px 0}
|
|
526
564
|
.top>div{max-width:760px;margin:0 auto}
|
|
527
|
-
.members{display:flex;gap:
|
|
565
|
+
.members{display:flex;gap:8px;overflow-x:auto;padding:10px 0;scrollbar-width:thin}
|
|
528
566
|
.bridgewarn{color:var(--busy);font-size:12px;font-weight:650;padding:0 0 8px}
|
|
529
567
|
.bridgewarn:empty{display:none}
|
|
530
568
|
.chip.has-meta{cursor:pointer}
|
|
531
569
|
/* 稼働状態の点。報告が途絶えたら unknown(中空)へ落として、古い状態を出し続けない */
|
|
532
570
|
.chip .nm{display:inline-flex;align-items:center;gap:5px}
|
|
571
|
+
.chip .state-label{font-size:10px;font-weight:700;color:var(--dim);white-space:nowrap}
|
|
572
|
+
.chip .activity{display:block;max-width:190px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dim);font-size:10px;font-weight:500}
|
|
533
573
|
.chip .st{flex:none;display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--dim)}
|
|
534
574
|
.chip .st.busy{background:var(--busy)}
|
|
535
575
|
.chip .st.idle{background:var(--idle)}
|
|
@@ -539,7 +579,7 @@ const UI = room => `<!doctype html><html lang="ja"><head><meta charset="utf-8"><
|
|
|
539
579
|
.metapop{position:fixed;z-index:20;background:var(--surface);border:1px solid var(--line);border-radius:10px;padding:8px 10px;font-size:12px;box-shadow:0 6px 20px rgba(0,0,0,.18);max-width:70vw}
|
|
540
580
|
.metapop .metaname{font-weight:600;margin-bottom:2px}
|
|
541
581
|
.metapop .metaline{color:var(--dim)}
|
|
542
|
-
.chip{position:relative;flex:none;display:flex;align-items:center;gap:7px;padding:
|
|
582
|
+
.chip{position:relative;flex:none;display:flex;align-items:center;gap:7px;min-width:150px;max-width:230px;padding:6px 11px 6px 6px;border:1px solid var(--line);border-radius:12px;background:var(--surface);font-size:12px;font-weight:600}
|
|
543
583
|
.chip .av{width:22px;height:22px;font-size:11px;flex:none}
|
|
544
584
|
.chip .id{display:flex;flex-direction:column;gap:1px;min-width:0;line-height:1.25}
|
|
545
585
|
.chip.recent{border-color:hsl(var(--h) var(--sat) var(--edge))}
|
|
@@ -755,8 +795,10 @@ async function refreshMembers(){
|
|
|
755
795
|
// 閲覧面ごとに独自の鮮度判定を持つと、MCP と Web UI で違う判定になり誤認が再発する
|
|
756
796
|
const st=m.status_effective??null
|
|
757
797
|
if(st)c.classList.add('is-'+st)
|
|
798
|
+
const stateText=st?({busy:'作業中',idle:'待機',dead:'停止',blocked:'承認待ち',unknown:'不明'}[st]??st):'不明'
|
|
758
799
|
const reason={status_unreported:'未報告(seat-status bridge が動いていない)',status_heartbeat_stale:'状態heartbeatが途絶えている',status_bridge_down:'状態bridgeが停止(status_bridge_down)',status_bridge_unreported:'状態bridgeが未登録',bridge_auth_failed:'bridge認証失敗(403)'}[m.status_reason]
|
|
759
|
-
meta.push(st?'状態 '+
|
|
800
|
+
meta.push(st?'状態 '+stateText+(reason?'('+reason+')':''):'状態 未取得(旧serverは実効状態を返さない)')
|
|
801
|
+
meta.push('現在 '+(m.activity_text??'未取得(旧server)'))
|
|
760
802
|
const usage=[]
|
|
761
803
|
const busyAge=m.busy_since?Date.now()-Date.parse(m.busy_since):NaN
|
|
762
804
|
if((st==='busy'||st==='blocked')&&Number.isFinite(busyAge)&&busyAge>=0)usage.push('継続 '+elapsed(busyAge))
|
|
@@ -766,12 +808,12 @@ async function refreshMembers(){
|
|
|
766
808
|
c.appendChild(el('span','av',initial(m.name)))
|
|
767
809
|
const id=el('span','id')
|
|
768
810
|
const nameRow=el('span','nm',m.name)
|
|
769
|
-
// チップの常設表示は「アバター・名前・◉」だけ。◉は状態が新鮮な時だけ色が付き、
|
|
770
|
-
// 途絶・未報告は中空リング=bridge が動いていないことがひと目で分かる。
|
|
771
|
-
// roles / settings / mission は title(ホバー)とタップ popover にだけ出す。
|
|
772
811
|
nameRow.appendChild(el('span','st '+(st??'unknown')))
|
|
812
|
+
nameRow.appendChild(el('span','state-label',stateText))
|
|
773
813
|
id.appendChild(nameRow)
|
|
814
|
+
id.appendChild(el('span','activity',m.activity_text??'現在作業未取得'))
|
|
774
815
|
c.appendChild(id)
|
|
816
|
+
c.setAttribute('aria-label',m.name+'、'+stateText+'、'+(m.activity_text??'現在作業未取得'))
|
|
775
817
|
// タップ環境には hover が無いので、押した時に同じ内容を出す(ホバーは title が担う)
|
|
776
818
|
if(meta.length){c.classList.add('has-meta');c.addEventListener('click',ev=>{ev.stopPropagation();showMeta(c,m,meta)})}
|
|
777
819
|
membersEl.appendChild(c)
|
|
@@ -821,6 +863,8 @@ function celebrate(name){
|
|
|
821
863
|
}
|
|
822
864
|
const BEAT=${HEARTBEAT_MS}
|
|
823
865
|
let lastSeq=0,lastBeat=Date.now(),es=null,emptyEl=null,firstLoad=true,catching=false,memberDebounce=null
|
|
866
|
+
const scheduleMemberRefresh=()=>{clearTimeout(memberDebounce);memberDebounce=setTimeout(refreshMembers,150)}
|
|
867
|
+
const isActivityMessage=m=>m.from!=='system'&&typeof m.body==='string'&&/^(?:\[(?:次の行動|claim|引受|作業中)\])/i.test(m.body)
|
|
824
868
|
// seq で二重描画を弾く。張り直し後の追いつきと SSE の新着が重なっても同じ発言は1回しか出ない
|
|
825
869
|
function apply(m,live=false){
|
|
826
870
|
if(m.seq<=lastSeq)return false
|
|
@@ -859,12 +903,12 @@ function connect(){
|
|
|
859
903
|
// 場合は、この差分だけが手掛かりになる
|
|
860
904
|
es.addEventListener('ping',e=>{lastBeat=Date.now();if(Number(e.data)>lastSeq)catchUp()})
|
|
861
905
|
// 稼働状態・素性の変化。既存の refreshMembers() を150msデバウンスで呼ぶ(部分更新は実装しない)
|
|
862
|
-
es.addEventListener('member',
|
|
906
|
+
es.addEventListener('member',scheduleMemberRefresh)
|
|
863
907
|
es.onmessage=e=>{
|
|
864
908
|
lastBeat=Date.now()
|
|
865
909
|
const m=JSON.parse(e.data),stick=nearBottom()
|
|
866
910
|
if(!apply(m,true))return
|
|
867
|
-
if(m.from==='system')refreshMembers();else{markActive(m.from);if(isCompletion(m))celebrate(m.from)}
|
|
911
|
+
if(m.from==='system')refreshMembers();else{markActive(m.from);if(isActivityMessage(m))scheduleMemberRefresh();if(isCompletion(m))celebrate(m.from)}
|
|
868
912
|
if(stick)window.scrollTo(0,document.body.scrollHeight)
|
|
869
913
|
syncToBottom()
|
|
870
914
|
}
|
|
@@ -347,50 +347,56 @@ async function wake(seat, msgs) {
|
|
|
347
347
|
// shell へ room の本文を send-keys すると、本文がそのまま shell コマンドとして実行される
|
|
348
348
|
// (実被弾 2026-08-22: codex 終了後の bash へ配達され `command not found` が走った。
|
|
349
349
|
// 本文次第では席の権限で任意コマンドになる)。配達は止め、毎周期 typed log で叫ぶ。
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
let
|
|
363
|
-
if (
|
|
364
|
-
const psOut = await run('/bin/ps', ['-
|
|
365
|
-
const children = new Map()
|
|
366
|
-
const commands = new Map()
|
|
350
|
+
// **打鍵が届く先は pane tty の前面プロセスグループだけ**なので、配達可否はそれで判定する。
|
|
351
|
+
// 「前面コマンド名がshellか」(実被弾 2026-08-22: bash 配下で生きる codex を誤遮断)でも、
|
|
352
|
+
// 「pane 子孫に agent CLI が実在するか」(実被弾 2026-08-29: SIGTSTP で停止した codex は
|
|
353
|
+
// 実在するが打鍵は bash が受け、room 本文がコマンド実行された)でも足りない。
|
|
354
|
+
// ps の STAT `+` は tty の前面プロセスグループを OS が直接教える印であり、
|
|
355
|
+
// 死亡・停止(T)・背面化のすべてを一つの条件で塞ぐ。
|
|
356
|
+
const ttyOut = await run('tmux', tmuxArgv(['display-message', '-p', '-t', observation.target, '#{pane_tty}'], { socket: observation.socket }))
|
|
357
|
+
const paneTty = String(ttyOut.stdout ?? '').trim().replace(/^\/dev\//u, '')
|
|
358
|
+
const SHELLS = ['bash', 'zsh', 'sh', 'dash', 'fish', 'tcsh', 'csh', 'ksh']
|
|
359
|
+
const ttyState = async () => {
|
|
360
|
+
let foreground = false
|
|
361
|
+
let stopped = false
|
|
362
|
+
let label = '(不明)'
|
|
363
|
+
if (paneTty) {
|
|
364
|
+
const psOut = await run('/bin/ps', ['-t', paneTty, '-o', 'stat=,command='], { env: { ...process.env, LC_ALL: 'C' } })
|
|
367
365
|
for (const line of String(psOut.stdout ?? '').split('\n')) {
|
|
368
|
-
const m = line.trim().match(/^(\
|
|
366
|
+
const m = line.trim().match(/^(\S+)\s+(.*)$/u)
|
|
369
367
|
if (!m) continue
|
|
370
|
-
const
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
const queue = [...(children.get(panePid) ?? [])]
|
|
376
|
-
while (queue.length) {
|
|
377
|
-
const pid = queue.pop()
|
|
378
|
-
const argv0 = String(commands.get(pid) ?? '').split(/\s+/u, 1)[0]
|
|
379
|
-
const base = argv0.split('/').pop()
|
|
380
|
-
if (base === harness || (harness === 'claude' && base === 'node' && String(commands.get(pid) ?? '').includes('claude'))) {
|
|
381
|
-
harnessAlive = true
|
|
382
|
-
break
|
|
368
|
+
const base = String(m[2]).split(/\s+/u, 1)[0].split('/').pop()
|
|
369
|
+
if (SHELLS.includes(base)) {
|
|
370
|
+
if (m[1].includes('+')) label = base
|
|
371
|
+
continue
|
|
383
372
|
}
|
|
384
|
-
|
|
373
|
+
if (m[1].includes('+')) foreground = true
|
|
374
|
+
if (m[1].startsWith('T')) stopped = true
|
|
385
375
|
}
|
|
386
376
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
377
|
+
return { foreground, stopped, label }
|
|
378
|
+
}
|
|
379
|
+
let tty = await ttyState()
|
|
380
|
+
if (!tty.foreground && tty.stopped) {
|
|
381
|
+
// Codex CLI の job control 欠陥(openai/codex#37088 系)で TUI が SIGTSTP/SIGTTIN 停止し、
|
|
382
|
+
// 打鍵が shell へ落ちる形が反復した(実被弾 2026-08-29 に2席×複数回)。配達の前提を
|
|
383
|
+
// 自動で回復する: pane の前面 shell へ `fg` を送って停止 job を前面へ戻し、成立を再観測する。
|
|
384
|
+
log(`SEAT_TUI_STOPPED: ${seat} の agent が停止(T)状態。fg で前面へ蘇生を試みる`)
|
|
385
|
+
await run('tmux', tmuxArgv(['send-keys', '-t', observation.target, 'C-u'], { socket: observation.socket }))
|
|
386
|
+
await run('tmux', tmuxArgv(['send-keys', '-l', '-t', observation.target, 'fg'], { socket: observation.socket }))
|
|
387
|
+
await run('tmux', tmuxArgv(['send-keys', '-t', observation.target, 'Enter'], { socket: observation.socket }))
|
|
388
|
+
await sleep(2000)
|
|
389
|
+
tty = await ttyState()
|
|
390
|
+
if (tty.foreground) log(`SEAT_TUI_RESUMED: ${seat} の agent を前面へ復帰させた`)
|
|
391
|
+
}
|
|
392
|
+
if (!tty.foreground) {
|
|
393
|
+
const foregroundLabel = tty.label
|
|
394
|
+
log(`SEAT_TUI_GONE: ${seat} の pane tty 前面が agent CLI でない(前面: ${foregroundLabel})=`
|
|
395
|
+
+ '打鍵は shell に落ちる(agent 死亡・SIGTSTP 停止・背面化)。'
|
|
396
|
+
+ 'shell へのコマンド実行を防ぐため配達しない。席を立て直すか leave-seat で畳むこと')
|
|
397
|
+
const error = new Error(`SEAT_TUI_GONE: ${seat}`)
|
|
398
|
+
error.code = 'SEAT_TUI_GONE'
|
|
399
|
+
throw error
|
|
394
400
|
}
|
|
395
401
|
if (await passKnownCodexDialog(member)) return 'deferred'
|
|
396
402
|
const pane = await run('tmux', tmuxArgv(['capture-pane', '-t', observation.target, '-p'], { socket: observation.socket }))
|