peertable 0.8.9 → 0.8.11

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.9",
3
+ "version": "0.8.11",
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.9'
16
+ const MCP_VERSION = '0.8.11'
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' }))
@@ -447,6 +447,14 @@ PY
447
447
  fi
448
448
  seat_tmux=$(tmux_at display-message -p -t "$sess" '#{socket_path}')
449
449
 
450
+ # 席が作る派生tmuxセッションへ PEERTABLE_MEMBER を自動継承させる(2026-08-26 オーナー裁定:
451
+ # 帰属は規約名でなくOSの継承機構で辿る)。tmuxは既定でclientのenvを剥ぐので、update-environmentへ
452
+ # 1回だけ載せる。これで席発のセッションは名前が何でも、session envのPEERTABLE_MEMBERで持ち主が読める
453
+ if ! tmux_at show-options -g update-environment 2>/dev/null | grep -q 'PEERTABLE_MEMBER'; then
454
+ tmux_at set-option -ga update-environment ' PEERTABLE_MEMBER' || \
455
+ echo "SEAT_ENV_STAMP_UNAVAILABLE: update-environment を設定できず、派生セッションの帰属はsession envに載らない" >&2
456
+ fi
457
+
450
458
  # Aiterm管理席の process 起動は公開launch receiptで確定している。旧direct CLI launch向けの
451
459
  # ヘッダ/trust dialog待機をここへ重ねると、brief turnでヘッダが画面外へ流れた正常席をrollbackする。
452
460
  # 必須room MCPの成立は、次の member登録readbackだけで判定する。
@@ -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, 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, 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]
@@ -131,7 +131,8 @@ function readSeat(member, previous, observedAt) {
131
131
  const jobPaneHash = new Map() // `${name}:${session}` -> { hash, changedAt }
132
132
  const jobCpuSeconds = new Map() // `${name}:${session}` -> 前周期の累積CPU秒(root=job本体を含む)
133
133
  let jobObserveFailureLogged = false
134
- function observeJob(socket, name) {
134
+ let envAttributionFailureLogged = false
135
+ function observeJob(socket, name, memberNames, envCache) {
135
136
  const listed = tmux(socket, 'list-sessions', '-F', '#{session_name}')
136
137
  if (listed === null) {
137
138
  // 「jobなし」と「観測手段が壊れている」を同じ顔にしない(silent absence禁止・2026-08-25 オーナー裁定)。
@@ -143,7 +144,30 @@ function observeJob(socket, name) {
143
144
  return { alive: false, active: false }
144
145
  }
145
146
  jobObserveFailureLogged = false
146
- const sessions = listed.split('\n').filter(s => s.startsWith(`peer-${name}-job`))
147
+ // 帰属はsession envのPEERTABLE_MEMBER(OSの継承をtmux update-environmentが写した実物)が正。
148
+ // 名前は自由——`-job` 規約は廃止(2026-08-26 オーナー裁定)。env読みは周期内cacheで1セッション1回。
149
+ const names = memberNames?.length ? memberNames : [name]
150
+ const envOf = (session) => {
151
+ if (envCache?.has(session)) return envCache.get(session)
152
+ // 変数名指定でなく全量listを読む: 「変数が無い」と「show-environment自体が使えない」
153
+ // (psmux等のtmux互換の欠け)を区別するため。使えない端末は名前fallbackで動き続けるが、
154
+ // その事実は黙らず一度だけ吠える(silent absence禁止)
155
+ const out = tmux(socket, 'show-environment', '-t', session)
156
+ if (out === null) {
157
+ if (!envAttributionFailureLogged) {
158
+ envAttributionFailureLogged = true
159
+ console.error('ENV_ATTRIBUTION_UNAVAILABLE: show-environment がこの端末のtmux/psmuxで失敗するため、預け仕事の帰属はsession envで判定できない(peer-<name>- 前置の名前fallbackのみで動作)')
160
+ }
161
+ envCache?.set(session, null)
162
+ return null
163
+ }
164
+ const m = /^PEERTABLE_MEMBER=(.+)$/m.exec(out)
165
+ const value = m ? m[1].trim() : null
166
+ envCache?.set(session, value)
167
+ return value
168
+ }
169
+ const sessions = listed.split('\n').filter(Boolean)
170
+ .filter(session => attributeJobSession({ session, memberNames: names, env: envOf(session) }) === name)
147
171
  if (sessions.length === 0) return { alive: false, active: false }
148
172
  let active = false
149
173
  let psRows = null
@@ -369,6 +393,8 @@ async function tick() {
369
393
  if (!supported) { console.error(`seat-status-bridge: ${members.length} 席を見たが、server が未対応なので送っていない`); return NOTHING_ATTEMPTED }
370
394
  const now = Date.now()
371
395
  const observedAt = new Date(now).toISOString()
396
+ const memberNames = members.map(m => m.name)
397
+ const envCache = new Map() // session env読みの周期内cache(帰属判定は全memberで同じ実物を見る)
372
398
  let sent = 0
373
399
  let skipped = 0
374
400
  let failed = 0
@@ -385,7 +411,7 @@ async function tick() {
385
411
  {
386
412
  const target = resolveSeatObservation(member, null) ?? resolveSeatObservation(member, defaultSocket())
387
413
  if (target !== null) {
388
- const job = observeJob(target.socket, member.name)
414
+ const job = observeJob(target.socket, member.name, memberNames, envCache)
389
415
  // 席paneの子孫プロセス(Codex内蔵background terminal等)の実働もjob稼働として合成する
390
416
  if (!job.active && observation.status === 'idle') {
391
417
  const panePid = tmuxPanePid(target.socket, target.target)
@@ -330,6 +330,21 @@ export function combineSeatLamp(paneStatus, job) {
330
330
  return paneStatus // 'dead' 等はそのまま
331
331
  }
332
332
 
333
+ // ---- 預け仕事セッションの帰属(2026-08-26 オーナー裁定: 規約名でなくOSの継承機構で辿る)----
334
+ // 正はsession envのPEERTABLE_MEMBER——席のシェルが持つ値をtmuxのupdate-environmentが作成時に
335
+ // 自動で写したもので、席は名前も申告も要らない。envが無い既存セッションだけ、旧来の
336
+ // `peer-<name>-` 前置の最長一致で拾う(後方互換。stamp導入前に立った常駐を殺さないため)。
337
+ // 席セッションそのもの(`peer-<name>` 完全一致)は預け仕事ではない。
338
+ export function attributeJobSession({ session, memberNames, env }) {
339
+ if (memberNames.includes(session.replace(/^peer-/, ''))) return null
340
+ if (env !== null && env !== undefined && memberNames.includes(env)) return env
341
+ let best = null
342
+ for (const n of memberNames) {
343
+ if (session.startsWith(`peer-${n}-`) && (best === null || n.length > best.length)) best = n
344
+ }
345
+ return best
346
+ }
347
+
333
348
  // paneプロセスの子孫に実働(CPU消費)が居るかの判定素材。psの `pid ppid pcpu` 出力を受け取り、
334
349
  // root配下の子孫(root自身は除く)に pcpu >= 閾値 が居れば true。席がCodex内蔵background terminal等、
335
350
  // tmuxセッションを作らずに走らせた預け仕事を、プロセスツリーの実観測で拾う(2026-08-25 オーナー裁定
@@ -21,7 +21,7 @@
21
21
 
22
22
  **kickoff・名指しの依頼DMには、まず `[引受] <要旨>` を room へ返してから着手する(決定104)。** 親はこの引受発言と配送 receipt が揃うまで依頼を未着手として扱う——黙って作業を始めると、依頼が届いていないと判定され再送・再依頼が起きる。
23
23
 
24
- **常駐・長時間ジョブは `peer-<自分の名前>-job<任意suffix>` という名前のtmuxセッションで走らせる(2026-08-25 オーナー裁定)。** `nohup`等で切り離すと観測不能になり、あなたのランプが「何もしていない」表示になる。jobセッションで走らせれば、稼働状況が自分のアクティブランプへ合成される(jobが動いていれば点滅)。複数ジョブは別suffixで並走してよい。ジョブ終了の目覚まし登録は従来どおり行う。
24
+ **常駐・長時間ジョブはtmuxセッションで走らせる。名前は自由(2026-08-26 オーナー裁定で命名規約は廃止)。** あなたのシェルからセッションを作れば、環境変数 `PEERTABLE_MEMBER` がOSの継承でセッションへ自動で刻まれ、観測装置がそれを読んで稼働をあなたのアクティブランプへ合成する(jobが動いていれば点滅)。`nohup`等でtmuxの外へ切り離すと観測不能になるので使わない。複数ジョブの並走は自由。ジョブ終了の目覚まし登録は従来どおり行う。
25
25
 
26
26
  **探索順は active → ready → 待機である。** まず自分の active 工程を完了させる。無ければ全PLAN横断の ready を自律的に claim する。実装も監査担当としての提出待ちも無い時だけ、最終手段として `[待機] ...` を親(bell 等、その卓の親名)だけへDMする。待機を `to: "all"` へ投稿しない。待機のあと自分へ `[次の行動]` を送らない。次に行う作業があるターンだけ自己DMする。空の終了通知は使わない。
27
27