peertable 0.8.7 → 0.8.8
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 +1 -1
- package/room/client.mjs +1 -1
- package/skill/scripts/platform/windows/import-specifier.mjs +6 -0
- package/skill/scripts/platform/windows/resolve-lattice-contracts.mjs +9 -0
- package/skill/scripts/seat-status-bridge.mjs +24 -4
- package/skill/scripts/seat-usage.mjs +18 -3
- package/skill/scripts/todo-extraction-from-plan.mjs +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peertable",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.8",
|
|
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.8'
|
|
17
17
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
18
18
|
|
|
19
19
|
const USAGE = `usage:
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
/** Windows npm global shimまたはsource treeのlattice CLIから契約moduleを解決する。 */
|
|
5
|
+
export function resolveWindowsLatticeContracts(latticeCli) {
|
|
6
|
+
const npmGlobal = join(dirname(latticeCli), 'node_modules', '@quolu', 'lattice', 'src', 'todo-contracts.mjs')
|
|
7
|
+
if (existsSync(npmGlobal)) return npmGlobal
|
|
8
|
+
return join(dirname(dirname(latticeCli)), 'src', 'todo-contracts.mjs')
|
|
9
|
+
}
|
|
@@ -145,6 +145,7 @@ function observeJob(socket, name) {
|
|
|
145
145
|
const sessions = listed.split('\n').filter(s => s.startsWith(`peer-${name}-job`))
|
|
146
146
|
if (sessions.length === 0) return { alive: false, active: false }
|
|
147
147
|
let active = false
|
|
148
|
+
let psRows = null
|
|
148
149
|
for (const session of sessions) {
|
|
149
150
|
const pane = tmux(socket, 'capture-pane', '-t', session, '-p')
|
|
150
151
|
if (pane === null) continue
|
|
@@ -152,6 +153,18 @@ function observeJob(socket, name) {
|
|
|
152
153
|
const key = `${name}:${session}`
|
|
153
154
|
const prev = jobPaneHash.get(key)
|
|
154
155
|
if (!prev || prev.hash !== hash) { jobPaneHash.set(key, { hash }); active = true }
|
|
156
|
+
// 画面に何も出さず働くジョブ(checkpointだけ書く収集等)は画面hashでは稼働に見えない
|
|
157
|
+
// (実被弾 2026-08-25: 収集が毎分書き込み中なのにランプが点灯止まり)。jobセッションの
|
|
158
|
+
// プロセスツリーのCPU実働も稼働として合成する。セッション全体が預け仕事なので足場除外は不要。
|
|
159
|
+
if (!active) {
|
|
160
|
+
const jobPanePid = tmuxPanePid(socket, session)
|
|
161
|
+
if (jobPanePid) {
|
|
162
|
+
try {
|
|
163
|
+
psRows ??= execFileSync('ps', ['-axo', 'pid=,ppid=,pcpu=,etime='], { encoding: 'utf8' }).split('\n')
|
|
164
|
+
if (hasActiveDescendant(psRows, Number(jobPanePid), { minAgeGapSeconds: 0 })) active = true
|
|
165
|
+
} catch { /* psが使えない端末では画面hash判定だけで続行 */ }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
155
168
|
}
|
|
156
169
|
return { alive: true, active }
|
|
157
170
|
}
|
|
@@ -221,7 +234,8 @@ async function nudgeIfDropped(name, busySince) {
|
|
|
221
234
|
const PATROL_INTERVAL_MS = 30_000
|
|
222
235
|
const PATROL_NAG_INTERVAL_MS = 300_000 // 条件が続く席へは5分間隔で再吠えする(1回きりにしない)
|
|
223
236
|
const patrolLastNag = new Map() // seat -> epoch_ms
|
|
224
|
-
const busyStartedAt = new Map() // seat -> epoch_ms(このbridge
|
|
237
|
+
const busyStartedAt = new Map() // seat -> epoch_ms(このbridgeプロセスが観測した最後のターン開始・pane基準)
|
|
238
|
+
const paneLast = new Map() // seat -> 直前周期のpane生status(番犬系専用。表示合成とは分離)
|
|
225
239
|
let lastPatrolAt = 0
|
|
226
240
|
async function patrolClaims() {
|
|
227
241
|
if (setup.mode !== 'lattice' || !setup.plan_key) return
|
|
@@ -358,6 +372,10 @@ async function tick() {
|
|
|
358
372
|
const observation = readSeat(member, prev, observedAt)
|
|
359
373
|
// tmux 席を持たない member(親など)は一度も観測できないので送らない(deriveMissingSession が null を返す)
|
|
360
374
|
if (observation === null) { skipped++; continue }
|
|
375
|
+
// 番犬(ターン終了検知・busy履歴)はpaneの生状態だけを読む。表示用の合成ランプ(job込み)を
|
|
376
|
+
// ここへ流すと、静かなジョブの画面出力の間欠でランプがbusy⇄idleに揺れ、その揺れを
|
|
377
|
+
// 「ターン終了」と誤認して正当待機の席へ[継続]を撃つ(実被弾 2026-08-25 #175: mio誤起床)。
|
|
378
|
+
const paneStatus = observation.status
|
|
361
379
|
{
|
|
362
380
|
const target = resolveSeatObservation(member, null) ?? resolveSeatObservation(member, defaultSocket())
|
|
363
381
|
if (target !== null) {
|
|
@@ -367,7 +385,7 @@ async function tick() {
|
|
|
367
385
|
const panePid = tmuxPanePid(target.socket, target.target)
|
|
368
386
|
if (panePid) {
|
|
369
387
|
try {
|
|
370
|
-
const rows = execFileSync('ps', ['-axo', 'pid=,ppid=,pcpu='], { encoding: 'utf8' }).split('\n')
|
|
388
|
+
const rows = execFileSync('ps', ['-axo', 'pid=,ppid=,pcpu=,etime='], { encoding: 'utf8' }).split('\n')
|
|
371
389
|
if (hasActiveDescendant(rows, Number(panePid))) job.active = true
|
|
372
390
|
} catch { /* psが失敗する端末では子孫観測なしで続行(named session観測は生きている) */ }
|
|
373
391
|
}
|
|
@@ -385,8 +403,10 @@ async function tick() {
|
|
|
385
403
|
await send(name, observation, observedAt)
|
|
386
404
|
last.set(name, { ...observation, at: now })
|
|
387
405
|
sent++
|
|
388
|
-
|
|
389
|
-
if (
|
|
406
|
+
const prevPane = paneLast.get(name)
|
|
407
|
+
if ((paneStatus === 'busy' || paneStatus === 'blocked') && prevPane !== 'busy' && prevPane !== 'blocked') busyStartedAt.set(name, now)
|
|
408
|
+
if (prevPane === 'busy' && paneStatus === 'idle') await nudgeIfDropped(name, prev?.busySince ?? null)
|
|
409
|
+
paneLast.set(name, paneStatus)
|
|
390
410
|
if (changed) console.error(`seat-status-bridge: ${name} → ${observation.status}${prev ? `(${prev.status} から)` : ''}`)
|
|
391
411
|
} catch (e) {
|
|
392
412
|
failed++
|
|
@@ -335,21 +335,36 @@ export function combineSeatLamp(paneStatus, job) {
|
|
|
335
335
|
// tmuxセッションを作らずに走らせた預け仕事を、プロセスツリーの実観測で拾う(2026-08-25 オーナー裁定
|
|
336
336
|
//「子プロセスも見る」)。nohup等でツリーから切り離された仕事はここでは見えない——それは
|
|
337
337
|
// peer-<name>-job* セッション慣例の側が受け持つ(二本立て)。
|
|
338
|
-
export function hasActiveDescendant(psRows, rootPid, { minCpu = 1.0 } = {}) {
|
|
338
|
+
export function hasActiveDescendant(psRows, rootPid, { minCpu = 1.0, minAgeGapSeconds = 60 } = {}) {
|
|
339
|
+
const parseEtime = (raw) => {
|
|
340
|
+
// ps etime: [[dd-]hh:]mm:ss
|
|
341
|
+
const m = /^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/.exec(raw)
|
|
342
|
+
if (!m) return null
|
|
343
|
+
return (Number(m[1] ?? 0) * 86400) + (Number(m[2] ?? 0) * 3600) + (Number(m[3]) * 60) + Number(m[4])
|
|
344
|
+
}
|
|
339
345
|
const children = new Map()
|
|
340
346
|
const cpu = new Map()
|
|
347
|
+
const age = new Map()
|
|
341
348
|
for (const row of psRows) {
|
|
342
|
-
const m = /^\s*(\d+)\s+(\d+)\s+([\d.]+)/.exec(row)
|
|
349
|
+
const m = /^\s*(\d+)\s+(\d+)\s+([\d.]+)\s+(\S+)/.exec(row)
|
|
343
350
|
if (!m) continue
|
|
344
351
|
const [pid, ppid, pcpu] = [Number(m[1]), Number(m[2]), Number(m[3])]
|
|
345
352
|
if (!children.has(ppid)) children.set(ppid, [])
|
|
346
353
|
children.get(ppid).push(pid)
|
|
347
354
|
cpu.set(pid, pcpu)
|
|
355
|
+
age.set(pid, parseEtime(m[4]))
|
|
348
356
|
}
|
|
357
|
+
const rootAge = age.get(rootPid)
|
|
349
358
|
const queue = [...(children.get(rootPid) ?? [])]
|
|
350
359
|
while (queue.length) {
|
|
351
360
|
const pid = queue.pop()
|
|
352
|
-
|
|
361
|
+
// 席と同時に起動したプロセスは足場(CLI本体・MCP server群)であって預け仕事ではない。
|
|
362
|
+
// 足場はアイドルでも1%前後のCPUを食い、ランプを恒常的に点滅させる誤検知源になる
|
|
363
|
+
// (実被弾 2026-08-25: 監査待ちのkoharuが「ずっとアクティブ」に見えた)。
|
|
364
|
+
// 「paneより有意に後から生まれた」ことだけを預け仕事の観測条件にする——起動時刻は
|
|
365
|
+
// 観測できる事実であり、コマンド名の恣意的なリストを持たない。
|
|
366
|
+
const laterBorn = rootAge != null && age.get(pid) != null && rootAge - age.get(pid) >= minAgeGapSeconds
|
|
367
|
+
if (laterBorn && (cpu.get(pid) ?? 0) >= minCpu) return true
|
|
353
368
|
queue.push(...(children.get(pid) ?? []))
|
|
354
369
|
}
|
|
355
370
|
return false
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
import { accessSync, constants, existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'
|
|
21
21
|
import { execFileSync } from 'node:child_process'
|
|
22
22
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
23
|
+
import { windowsImportSpecifier } from './platform/windows/import-specifier.mjs'
|
|
24
|
+
import { resolveWindowsLatticeContracts } from './platform/windows/resolve-lattice-contracts.mjs'
|
|
23
25
|
|
|
24
26
|
function usage(message) {
|
|
25
27
|
if (message) console.error(`ERROR: ${message}`)
|
|
@@ -102,8 +104,13 @@ try {
|
|
|
102
104
|
: 'Peertable setupの正規手順で setup-state.json の lattice_cli を再生成する',
|
|
103
105
|
)
|
|
104
106
|
}
|
|
105
|
-
const latticePkgSrc =
|
|
106
|
-
|
|
107
|
+
const latticePkgSrc = process.platform === 'win32'
|
|
108
|
+
? resolveWindowsLatticeContracts(latticeCli)
|
|
109
|
+
: join(dirname(dirname(latticeCli)), 'src', 'todo-contracts.mjs')
|
|
110
|
+
const latticePkgSpecifier = process.platform === 'win32'
|
|
111
|
+
? windowsImportSpecifier(latticePkgSrc)
|
|
112
|
+
: latticePkgSrc
|
|
113
|
+
const { todoSelfDigest } = await import(latticePkgSpecifier)
|
|
107
114
|
|
|
108
115
|
const planPathFromRoot = relative(repoRoot, absolutePlanPath)
|
|
109
116
|
if (isAbsolute(planPathFromRoot) || planPathFromRoot.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) || planPathFromRoot === '..') {
|