peertable 0.8.39 → 0.8.40
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/.github/og.png +0 -0
- package/README.ja.md +2 -2
- package/README.md +5 -5
- package/package.json +10 -1
- package/room/client.mjs +1 -1
- package/room/server.mjs +35 -6
- package/skill/02_models.snapshot.md +31 -96
- package/skill/SKILL.md +3 -3
- package/skill/scripts/platform/windows/observe-pid-command.mjs +3 -1
- package/skill/scripts/platform/windows/resolve-lattice-command.mjs +22 -9
- package/skill/scripts/resolve-seat-placement.mjs +3 -5
- package/skill/scripts/seat-identity.mjs +3 -1
- package/skill/scripts/seat-usage.mjs +3 -3
- package/skill/scripts/teardown.sh +11 -0
- package/skill/scripts/todo-extraction-from-plan.mjs +1 -1
- package/skill/scripts/wakeup-bridge.mjs +35 -12
package/.github/og.png
ADDED
|
Binary file
|
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.8.
|
|
116
|
+
現在のnpm releaseは **peertable 0.8.40**。
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
製品の現行契約は [docs/current-design.md](https://github.com/kitepon/peertable/blob/main/docs/current-design.md)。完了計画と累積decision logは`docs/archive/`へ置き、現行文書の地図は [docs/00_overview.md](https://github.com/kitepon/peertable/blob/main/docs/00_overview.md) を正とする。
|
|
119
119
|
|
|
120
120
|
Claude Code channels はリサーチプレビューのため、フラグ・プロトコルは変わりうる。
|
|
121
121
|
|
package/README.md
CHANGED
|
@@ -85,8 +85,8 @@ room/ room server (zero-dependency Node) + per-session MCP channel client
|
|
|
85
85
|
skill/ "peertable" skill for Claude Code: setup / disband (teardown) of a full table,
|
|
86
86
|
plus the seat launcher and the wake-up / seat-state / run bridges
|
|
87
87
|
deploy/ compose + Caddy snippet for running the room server as a resident service
|
|
88
|
-
docs/
|
|
89
|
-
plus one plan_*.md per campaign
|
|
88
|
+
docs/ current-design.md — the current product contract (Japanese),
|
|
89
|
+
plus one plan_*.md per active campaign and archived history
|
|
90
90
|
evidence/ per-task completion evidence referenced by the Lattice plan store
|
|
91
91
|
experiments/ verification harnesses — one per pitfall we actually hit, each pinning the
|
|
92
92
|
behaviour so it cannot silently regress (channels, Lattice concurrency, the full
|
|
@@ -129,7 +129,7 @@ export PEERTABLE_URL=http://localhost:8790 PEERTABLE_ROOM=myproject PEERTABLE_ME
|
|
|
129
129
|
claude --dangerously-load-development-channels server:room
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
**Do not pass it via `--mcp-config`.** Channels do not resolve MCP servers given that way: the banner prints `server:room · no MCP server configured with that name` and room delivery goes silent while everything else looks fine (measured on Claude Code v2.1.226; decision 44
|
|
132
|
+
**Do not pass it via `--mcp-config`.** Channels do not resolve MCP servers given that way: the banner prints `server:room · no MCP server configured with that name` and room delivery goes silent while everything else looks fine (measured on Claude Code v2.1.226; [archived decision 44](https://github.com/kitepon/peertable/blob/main/docs/archive/plan.md)). The skill handles this for you and reverts the file on teardown.
|
|
133
133
|
|
|
134
134
|
The member gets five tools — `post`, `read_unread`, `read_log`, `members`, `delivery_status` — and a channel that wakes it whenever teammates address it. `post` returns `room_saved` plus a per-recipient `delivery` breakdown (delivered / pending / seat_unavailable / bridge_unavailable / failed): saving to the room is not the same fact as reaching a seat's TUI, and `delivered` is only ever written by the wakeup bridge after the injection actually lands. `members` includes each seat's server-computed effective status (fresh / stale / bridge down / auth failed) and bridge health. (`--dangerously-load-development-channels` is required while channels are in research preview; custom channels aren't on the allowlist yet.)
|
|
135
135
|
|
|
@@ -149,9 +149,9 @@ It interviews you, names the members, scaffolds `.team/` (charter + roles, isola
|
|
|
149
149
|
|
|
150
150
|
Working, and used to build itself. First verified end-to-end on 2026-08-08 with a full no-orchestrator loop: two members consulted, claimed, negotiated an interface, shared a discovered pitfall, and shipped a small project with **zero external intervention**. A 2026-08-13 real-seat lifecycle verified in-place model/effort changes and restart recovery. On 2026-08-14, a Grok 4.6 seat joined the room, changed 4.6↔4.5 in the same session, and woke on a direct message in a live acceptance run. On 2026-08-17 the wake-up path was corrected so Grok seats wait for idle, broadcasts keep their body, and a parent without a tmux seat cannot stall the bridge cursor.
|
|
151
151
|
|
|
152
|
-
The current npm release is **peertable 0.
|
|
152
|
+
The current npm release is **peertable 0.8.40**.
|
|
153
153
|
|
|
154
|
-
The design
|
|
154
|
+
The current product contract is [docs/current-design.md](https://github.com/kitepon/peertable/blob/main/docs/current-design.md). Completed plans and the cumulative decision log are kept under `docs/archive/`; the current document map is [docs/00_overview.md](https://github.com/kitepon/peertable/blob/main/docs/00_overview.md).
|
|
155
155
|
|
|
156
156
|
Depends on Claude Code **channels**, currently a research preview — flags and protocol may change.
|
|
157
157
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peertable",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.40",
|
|
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",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
28
|
"verify:release-commit": "node scripts/verify-release-commit.mjs",
|
|
29
|
+
"test:docs": "node --test scripts/docs-contract.test.mjs",
|
|
29
30
|
"prepublishOnly": "node scripts/prepublish.mjs"
|
|
30
31
|
},
|
|
31
32
|
"bin": {
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"peertable-client": "room/client.mjs"
|
|
34
35
|
},
|
|
35
36
|
"files": [
|
|
37
|
+
".github/og.png",
|
|
36
38
|
"room/server.mjs",
|
|
37
39
|
"room/client.mjs",
|
|
38
40
|
"room/message-bounds.mjs",
|
|
@@ -46,5 +48,12 @@
|
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
48
50
|
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"parse-srcset": "1.0.2",
|
|
54
|
+
"parse5": "^8.0.1",
|
|
55
|
+
"remark-gfm": "^4.0.1",
|
|
56
|
+
"remark-parse": "^11.0.0",
|
|
57
|
+
"unified": "^11.0.5"
|
|
49
58
|
}
|
|
50
59
|
}
|
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.40'
|
|
18
18
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
19
19
|
|
|
20
20
|
const USAGE = `usage:
|
package/room/server.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Peertable room サーバー。チャットルームの正本を所有し、Web UI を内蔵する。
|
|
3
3
|
// 起動: node server.mjs(PEERTABLE_PORT / PEERTABLE_DATA / PEERTABLE_POST_TOKEN で設定)
|
|
4
4
|
import http from 'node:http'
|
|
5
|
-
import { mkdirSync, readFileSync, appendFileSync, existsSync, rmSync, readdirSync, renameSync } from 'node:fs'
|
|
5
|
+
import { mkdirSync, readFileSync, appendFileSync, existsSync, rmSync, readdirSync, renameSync, writeFileSync } from 'node:fs'
|
|
6
6
|
import { dirname, join } from 'node:path'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
import { DatabaseSync } from 'node:sqlite'
|
|
@@ -129,6 +129,12 @@ function normalizeMemberMeta(meta) {
|
|
|
129
129
|
// room 状態はプロセスが所有する(member 正本は上の SQLite、ログは append-only file)
|
|
130
130
|
const rooms = new Map() // name -> { seq, streams: Set<res> }
|
|
131
131
|
|
|
132
|
+
// archive 状態の正本は room dir 内の marker file。卓の teardown が付け、次の setup の
|
|
133
|
+
// member 登録が自動で外す。archived room は公開一覧(/api/rooms・トップページの主一覧)から
|
|
134
|
+
// 外れるだけで、ログ・member 台帳・個別ページはそのまま読める——過去卓の記録は消さない。
|
|
135
|
+
const archiveMarker = name => join(DATA, name, 'archived.json')
|
|
136
|
+
const isArchived = name => existsSync(archiveMarker(name))
|
|
137
|
+
|
|
132
138
|
// create=true は書込系だけが渡す。読み取りは room を作らない
|
|
133
139
|
function loadRoom(name, create = false) {
|
|
134
140
|
const cached = rooms.get(name)
|
|
@@ -369,11 +375,17 @@ http.createServer(async (req, res) => {
|
|
|
369
375
|
|
|
370
376
|
// API: /api/rooms — 公開されている全room名。状態は既存のroom別summaryから読む。
|
|
371
377
|
if (req.method === 'GET' && seg.length === 2 && seg[0] === 'api' && seg[1] === 'rooms') {
|
|
372
|
-
const
|
|
378
|
+
const allRooms = readdirSync(DATA, { withFileTypes: true })
|
|
373
379
|
.filter(entry => entry.isDirectory())
|
|
374
380
|
.map(entry => entry.name)
|
|
375
381
|
.sort((a, b) => a.localeCompare(b))
|
|
376
|
-
|
|
382
|
+
// rooms は現役卓だけ。archived を混ぜると、公開トップの live 窓が「最後に配達失敗が
|
|
383
|
+
// 流れた死卓」を最新活動として選び続ける(2026-08-30 実測)。archived は別欄で返す
|
|
384
|
+
return json(res, 200, {
|
|
385
|
+
schema: 'peertable.rooms.v1',
|
|
386
|
+
rooms: allRooms.filter(name => !isArchived(name)),
|
|
387
|
+
archived: allRooms.filter(isArchived),
|
|
388
|
+
}, CORS)
|
|
377
389
|
}
|
|
378
390
|
|
|
379
391
|
// API: /api/<room>/...
|
|
@@ -468,6 +480,15 @@ http.createServer(async (req, res) => {
|
|
|
468
480
|
// delivered は wakeup-bridge の投入成立 receipt だけが作る。sent の一語で両者を混ぜない
|
|
469
481
|
return json(res, 200, { ...saved, room_saved: true, delivery: deliveryPlanFor(room.name, saved, bridgeHealth(room.name)) })
|
|
470
482
|
}
|
|
483
|
+
// 卓の解散(teardown)が room を公開一覧から外す。復活は下の members 登録が自動で行う
|
|
484
|
+
if (req.method === 'POST' && rest === 'archive') {
|
|
485
|
+
writeFileSync(archiveMarker(room.name), JSON.stringify({ archived_at: new Date().toISOString() }) + '\n')
|
|
486
|
+
return json(res, 200, { ok: true })
|
|
487
|
+
}
|
|
488
|
+
if (req.method === 'DELETE' && rest === 'archive') {
|
|
489
|
+
rmSync(archiveMarker(room.name), { force: true })
|
|
490
|
+
return json(res, 200, { ok: true })
|
|
491
|
+
}
|
|
471
492
|
// bridge 心拍(決定103)。seat-status / wakeup が 30 秒ごとに送る。正常心拍は 403 観測を消す
|
|
472
493
|
if (req.method === 'POST' && rest === 'bridges') {
|
|
473
494
|
const { kind, pid, state, detail } = JSON.parse(body)
|
|
@@ -508,6 +529,9 @@ http.createServer(async (req, res) => {
|
|
|
508
529
|
if (normalized.observe == null && known?.observe) delete normalized.observe
|
|
509
530
|
const merged = { name, joined_at: known?.joined_at ?? new Date().toISOString(), ...known, ...normalized }
|
|
510
531
|
putMember(room.name, merged)
|
|
532
|
+
// 同じ room 名で次の卓が立ったら archive を自動解除する。setup も席の client も
|
|
533
|
+
// member 登録から始まるので、ここが「卓が現役へ戻った」の実測点になる
|
|
534
|
+
rmSync(archiveMarker(room.name), { force: true })
|
|
511
535
|
// **system 発言は本当に新規の時だけ**。欄の更新で「参加した」を流すと、状態を数秒ごとに
|
|
512
536
|
// 送る消費者が卓の全席を起こし続ける(既存メンバーへの再 POST で実測・room [285])
|
|
513
537
|
if (!known) post(room, 'system', name, `${name} が参加した`)
|
|
@@ -536,7 +560,7 @@ http.createServer(async (req, res) => {
|
|
|
536
560
|
if (req.method === 'GET' && seg.length === 0) {
|
|
537
561
|
const list = readdirSync(DATA, { withFileTypes: true }).filter(d => d.isDirectory()).map(d => d.name)
|
|
538
562
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
539
|
-
return res.end(INDEX(list))
|
|
563
|
+
return res.end(INDEX(list.filter(name => !isArchived(name)), list.filter(isArchived)))
|
|
540
564
|
}
|
|
541
565
|
if (req.method === 'GET' && seg.length === 1) {
|
|
542
566
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
|
|
@@ -926,15 +950,20 @@ setInterval(()=>{if(Date.now()-lastBeat>BEAT*2.5)connect()},BEAT/2)
|
|
|
926
950
|
setInterval(refreshMembers,30000) // 退席(member DELETE)は発言を出さないので定期に取り直す
|
|
927
951
|
</script></body></html>`
|
|
928
952
|
|
|
929
|
-
const INDEX = list => `<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
953
|
+
const INDEX = (list, archived = []) => `<!doctype html><html lang="ja"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
930
954
|
<title>Peertable</title>${FAVICON}<style>${STYLE}
|
|
931
955
|
.wrap{max-width:560px;margin:0 auto;padding:56px 16px}
|
|
932
956
|
.tag{color:var(--dim);margin:6px 0 22px}
|
|
933
957
|
.rooms{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px}
|
|
934
958
|
.rooms a{display:flex;align-items:center;gap:9px;padding:11px 14px;border:1px solid var(--line);border-radius:11px;background:var(--surface);text-decoration:none;color:var(--fg);font-weight:650}
|
|
935
959
|
.rooms a::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--accent)}
|
|
960
|
+
.archived{margin-top:26px}
|
|
961
|
+
.archived summary{color:var(--dim);cursor:pointer;margin-bottom:10px}
|
|
962
|
+
.archived .rooms a{font-weight:450;color:var(--dim)}
|
|
963
|
+
.archived .rooms a::before{background:var(--dim)}
|
|
936
964
|
</style></head><body><div class="wrap">
|
|
937
965
|
<div class="brand">${MARK}Peertable</div>
|
|
938
966
|
<p class="tag">A round table of peer agents. No orchestrator at the head.</p>
|
|
939
|
-
<ul class="rooms">${list.map(r => `<li><a href="/${encodeURIComponent(r)}">${esc(r)}</a></li>`).join('') || '<li class="empty"
|
|
967
|
+
<ul class="rooms">${list.map(r => `<li><a href="/${encodeURIComponent(r)}">${esc(r)}</a></li>`).join('') || '<li class="empty">(現役の卓は無い)</li>'}</ul>
|
|
968
|
+
${archived.length ? `<details class="archived"><summary>解散した卓(${archived.length})</summary><ul class="rooms">${archived.map(r => `<li><a href="/${encodeURIComponent(r)}">${esc(r)}</a></li>`).join('')}</ul></details>` : ''}
|
|
940
969
|
</div></body></html>`
|
|
@@ -1,103 +1,38 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Peertable seat placement snapshot
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
```yaml
|
|
4
|
+
schema: peertable.models-placement.v1
|
|
5
|
+
source_repository: https://github.com/kitepon/dotagents
|
|
6
|
+
source_path: docs/02_models.md
|
|
7
|
+
source_commit: dcf4d9064b5c4f21a7643cbd169b7047cf3607bf
|
|
8
|
+
captured_at: 2026-08-30
|
|
9
|
+
```
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
背骨: **役割が要求する能力で選び、順位は実測で昇降格する。ただし、現状維持に候補側だけの立証責任を負わせない。** 新しい有力候補は代表実務へ期限・範囲を切って投入し、成功率・手戻り・監査工数・総token・所要時間・quotaで現役と比較する。未検証の現役を「安全」、未検証の新顔を「危険」とは扱わない。
|
|
8
|
-
|
|
9
|
-
本書の根拠は次の4種を混ぜない。
|
|
10
|
-
|
|
11
|
-
1. **一次事実**: model提供元公式の価格、context、対応effort、live catalog。
|
|
12
|
-
2. **外部観測**: benchmark、Xの利用報告、独立評価。harness・標本数・再現条件を併記する。
|
|
13
|
-
3. **dotagents実測**: 当工場のtask、監査通過率、配線実測。一般性能へ拡張しない。
|
|
14
|
-
4. **運用判断**: 上記を踏まえた配置。客観的に確定した事実を「オーナー裁定」と呼ばない。
|
|
15
|
-
|
|
16
|
-
**親のモデル×effortはオーナーの領分**であり、規範・AIはピンを打ち替えない。親候補と根拠は提示し、子の配置はこの表で解決する。
|
|
17
|
-
|
|
18
|
-
## 役割の定義(9つ)
|
|
19
|
-
|
|
20
|
-
1. **統括** — campaign/会話の主体。裁定・契約クリティカルな受入・commit
|
|
21
|
-
2. **反証** — 実ファイルを読んで主張を殺しにかかる
|
|
22
|
-
3. **監査・発見** — 自成果物(repo・設計・文書)の欠陥出し。誤検知は後段が裁く(第三者reviewを含む)
|
|
23
|
-
4. **設計** — 実装前の構造・境界・停止判断の案出し
|
|
24
|
-
5. **相談** — 実読不要の純推論second opinion
|
|
25
|
-
6. **実装** — 仕様固定済みのまとまった実装・テスト・移設
|
|
26
|
-
7. **局所コーディング** — focused test付きの狭い修正・実装
|
|
27
|
-
8. **軽作業** — 分類・抽出・字面回収
|
|
28
|
-
9. **調査** — 外部事実の回収〜統合(浅↔深はeffortで刻む)
|
|
11
|
+
これはPeertableがrelease単位で所有する着席配置の既定値である。役割、順位、model slug、effortだけを同梱し、dotagentsの説明文や内部リンクは複製しない。外部表を使う場合は`PEERTABLE_MODELS_DOC`または`DOTAGENTS_ROOT`を明示する。
|
|
29
12
|
|
|
30
13
|
## 順位表(役割→1位〜3位)
|
|
31
14
|
|
|
32
|
-
「実測」= dotagents 2026-08-19 役割配置実験([実験記録](../rag/models/role-placement-experiment-20260819.md)、各セルn=1)。
|
|
33
|
-
|
|
34
15
|
| 役割 | 1位 | 2位 | 3位 |
|
|
35
16
|
|---|---|---|---|
|
|
36
|
-
| 統括 |
|
|
37
|
-
| 反証 | Grok 4.6×high
|
|
38
|
-
| 監査・発見 | Grok 4.6×medium
|
|
39
|
-
| 設計 | Opus 5×high
|
|
40
|
-
| 相談 | ChatGPT(gpt-connector・別quota
|
|
41
|
-
| 実装 | Terra×high
|
|
42
|
-
| 局所コーディング | Luna×medium
|
|
43
|
-
| 軽作業 | Luna×low
|
|
44
|
-
| 調査 | Grok 4.6×low
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
|
55
|
-
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
| Claude Sonnet 5 | alias `sonnet` | **$2/$10(恒久価格)** | 1M | low〜xhigh/max |
|
|
59
|
-
| Claude Haiku 4.5 | alias `haiku` | $1/$5 | 200K | **effortなし** |
|
|
60
|
-
| GPT-5.6 Sol | `gpt-5.6-sol` | $5/$30(長contextは$10/$45) | 1.05M | none〜max(Codex CLIはultraあり) |
|
|
61
|
-
| GPT-5.6 Terra | `gpt-5.6-terra` | $2/$12(長contextは$4/$18) | 1.05M | 同上 |
|
|
62
|
-
| GPT-5.6 Luna | `gpt-5.6-luna` | $0.20/$1.20(長contextは$0.40/$1.80) | 1.05M | none〜max(ultraなし) |
|
|
63
|
-
| Grok 4.6 | `grok-4.6` | $2/$6(200K超は$4/$12) | 500K | low/medium/high(既定)/xhigh |
|
|
64
|
-
|
|
65
|
-
価格は標準API定価であり、Claude Code・Codex・Grok Buildのsubscription quotaとは別物。OpenAIの長context課金は階層制(境界と現行値は公式pricingを実行時に確認)。Grok Buildのcatalogに`grok-build-0.1`(Composer 2.5系)の記載が公式docsにあるが、端末live catalogでの実在は未検証——Composer入口はcatalog確認まで`unsupported`のまま、Grokへfallbackしない。根拠は [GPT-5.6](../rag/models/gpt-5.6-family.md)、[Claude 5](../rag/models/claude-5-family.md)、[Grok 4.6](../rag/models/xai-grok46.md)。
|
|
66
|
-
|
|
67
|
-
### 消費枠(quotaの独立勘定。規則2のfallback判断に使う)
|
|
68
|
-
|
|
69
|
-
Anthropic(Claude Code本体・Agent/Workflow)/OpenAI Codex(Codex CLI・codex-sidecar・aiterm codex_agent)/OpenAI ChatGPT(gpt-connector・API fallback禁止)/xAI(Grok Build・aiterm grok_agent)の4枠は別勘定。同役割の次順位が別枠なら、quota逼迫時のfallbackはコスト増でなく枠の移動になる。
|
|
70
|
-
|
|
71
|
-
## 入口と使い分け
|
|
72
|
-
|
|
73
|
-
- **Codex親の三入口を分ける**: ① native subagent=repo密結合、② external execution=codex-sidecar/aiterm、③ consultation=gpt-connector。Grok/ComposerはAitermの別harness入口であり、Codex→Codexの入口判断とは別契約。
|
|
74
|
-
- Aitermの`codex_agent`/`grok_agent`/`claude_agent`はmodelとeffortを毎回明示する。live catalog不在・effort非対応は明示エラーにし、別modelへfallbackしない。
|
|
75
|
-
- **委譲の安全・回収・受入契約は[委譲契約](../shared/orchestrate/delegation-contract.md)が正本**。Aitermの運用型は[aiterm-dispatch](../shared/orchestrate/aiterm-dispatch.md)を正とする。external writerはinstalled→registered→verified→execution-verifiedの最終段だけに置く。
|
|
76
|
-
- codex-sidecarはmodel/effortを毎回明示するか`.codex-sidecar.yml` defaultsへ置く。現行schemaはlow〜xhighでmaxを渡せない。
|
|
77
|
-
- **役割と配置関係の機械可読な対応**は`lib/orchestrate/placement-policy.mjs` v1が固定する。自動ConsultationはAnthropic/OpenAIだけだが、これは現行配線のclosed enumであり、xAIの能力評価ではない。
|
|
78
|
-
|
|
79
|
-
## effortの規範
|
|
80
|
-
|
|
81
|
-
effortは単調に品質を上げない。まず役割に合うpresetへ置き、同じ代表taskで隣接levelを比べる。失敗後に変えるのはmodel tierかeffortの片方だけにする。
|
|
82
|
-
|
|
83
|
-
| model | 出発点 | 上げ下げの規律 |
|
|
84
|
-
|---|---|---|
|
|
85
|
-
| Opus 5 | **medium**(工程限定の実装・review)、**high**(長期agent・複雑設計) | xhigh/maxは長時間の知識仕事で測定差が出た時だけ。権限境界が緩い工程へ高effortで置かない |
|
|
86
|
-
| Fable 5 | high | 契約criticalのスポット。常用親と同義になる使い方はしない |
|
|
87
|
-
| Sonnet 5 | medium | finderの字面回収だけlow。高effortは代表taskで差が出た時 |
|
|
88
|
-
| Haiku 4.5 | effortなし | 存在しない`haiku×low`を指定しない |
|
|
89
|
-
| Sol/Terra | medium | high/xhighは測定可能な品質差がある時。maxは最難関quality-first。ultraはCodex harnessのmax+自動fan-outであり明示要求時だけ |
|
|
90
|
-
| Luna | **分解済み・仕様固定・focused test付きはmedium**(2026-08-19実測: medium=max同格・14/14) | 分解が甘い仕事はLunaのeffortを上げて救わず別モデルを選ぶ(旧「maxのみ」実測の教訓を適用範囲限定で継承) |
|
|
91
|
-
| Grok 4.6 | low=X/字面回収、medium=統合調査・監査finder、high=統括・反証・長時間agent | xhighは思考loopと遅延の外部報告がある。代表taskでhighを上回った時だけ使う |
|
|
92
|
-
|
|
93
|
-
## Grok 4.6をどう読むか
|
|
94
|
-
|
|
95
|
-
xAI公式値ではoffice/agentic系で最前線級、DeepSWE・TerminalBenchでは比較対象を下回る。Xには統括・review・長時間実装の成功例と、security誤判定・部分読み完了誤認・思考loopの失敗例が併存する。dotagents実測(2026-08-19)では監査finderのrecall首位(7/10・FP 0)と反証満点(14/14)で、監査・反証・調査の第一候補として実戦配置する。
|
|
96
|
-
|
|
97
|
-
## 指定と世代交代時の更新手順
|
|
98
|
-
|
|
99
|
-
- Claude Code内はfloating alias(`fable`/`opus`/`sonnet`/`haiku`)だけを使う。Agent/Workflowのmodelとeffortは対応する場合に毎回明示し、Haikuへeffortを付けない。
|
|
100
|
-
- Codexにfloating aliasがないため、`codex/agents/*.toml`と`.codex-sidecar.yml`は具体slugを持つ公認例外。世代交代時は本書と同一commitで更新する。
|
|
101
|
-
- 外部CLIはpinせず`agents-update`でlatest追従する。model live catalogは実行直前に見る。
|
|
102
|
-
- オーナーの世代交代宣言後、`grep -rn "前提:"`で影響面を列挙し、本書、公認例外、focused fixture、RAGを同時更新する。新規候補は小さな実戦比較を行い、失敗も次の配置判断へ残す。
|
|
103
|
-
- 順位の変更は実測(役割配置実験の再走または実戦の成功率・手戻り)を根拠にし、実験記録をRAGへ残してから表を書き換える。
|
|
17
|
+
| 統括 | オーナー指定 | — | — |
|
|
18
|
+
| 反証 | Grok 4.6×high | Sol×high | Opus 5×high |
|
|
19
|
+
| 監査・発見 | Grok 4.6×medium | Sonnet 5×medium | Terra×medium |
|
|
20
|
+
| 設計 | Opus 5×high | Grok 4.6×high | Sol×medium |
|
|
21
|
+
| 相談 | ChatGPT(gpt-connector・別quota) | Grok 4.6×medium | Fable 5×high |
|
|
22
|
+
| 実装 | Terra×high | Sonnet 5×medium | Grok 4.6×medium |
|
|
23
|
+
| 局所コーディング | Luna×medium | Sonnet 5×medium | Grok 4.6×medium |
|
|
24
|
+
| 軽作業 | Luna×low | Haiku | Grok 4.6×low |
|
|
25
|
+
| 調査 | Grok 4.6×low | Sonnet 5×medium | Sol×medium |
|
|
26
|
+
|
|
27
|
+
## モデル台帳
|
|
28
|
+
|
|
29
|
+
| モデル | slug |
|
|
30
|
+
|---|---|
|
|
31
|
+
| Claude Fable 5 | alias `fable` |
|
|
32
|
+
| Claude Opus 5 | alias `opus` |
|
|
33
|
+
| Claude Sonnet 5 | alias `sonnet` |
|
|
34
|
+
| Claude Haiku 4.5 | alias `haiku` |
|
|
35
|
+
| GPT-5.6 Sol | `gpt-5.6-sol` |
|
|
36
|
+
| GPT-5.6 Terra | `gpt-5.6-terra` |
|
|
37
|
+
| GPT-5.6 Luna | `gpt-5.6-luna` |
|
|
38
|
+
| Grok 4.6 | `grok-4.6` |
|
package/skill/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: 任意プロジェクトに Peertable チーム(対等メンバ
|
|
|
5
5
|
|
|
6
6
|
# Peertable — setup / teardown
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
製品境界と設計の正典は peertable リポジトリの docs/current-design.md。本スキルはsetup / resume / teardownの手順正本である。
|
|
9
9
|
|
|
10
10
|
## 前提
|
|
11
11
|
|
|
@@ -50,7 +50,7 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
|
|
|
50
50
|
|
|
51
51
|
手順は **聞き取り → script → 着任指示** の3段である。scripts が機械部分を全部持つので、AI が手で tmux を組み立てることはしない。
|
|
52
52
|
|
|
53
|
-
1. **聞き取り**: 対象プロジェクトのパス / **工程正本(`Lattice 併用`=既定 / `単独`)** /
|
|
53
|
+
1. **聞き取り**: 対象プロジェクトのパス / **工程正本(`Lattice 併用`=既定 / `単独`)** / メンバー数と**役割**(同梱 `02_models.snapshot.md` の役割名そのもの。未指定・未知は着席しない。model×effort は `launch-seat.sh` が同梱順位表1位から機械解決する。隣接dotagentsは暗黙に読まず、外部表は明示opt-inだけ。呼び出し側が model / harness / effort を渡して正本を迂回しない。決定49改・決定91)/ 初期タスク群(何を作るか)/ room 名(既定: プロジェクトのディレクトリ名)/ **公開URL基底**(Lattice 併用のみ。外部ペインに書く URL。クオ環境は `https://peertable.kitepon.dev`。未指定なら room サーバーの URL がそのまま入る=LAN URL は Lattice を外から見た時に開けない)
|
|
54
54
|
- **メンバー数の既定**: Lattice 併用なら plan compile 結果の幅(`max_frontier_width`)に合わせる(実測: 幅3→3人、第2 campaign で幅4→4人目追加)。frontier より多い席は最初から遊ぶ。単独モードには frontier が無いので既定の根拠も無く、聞き取りで決める
|
|
55
55
|
- **運用中のworker席数の標準は「ready+activeな実装ToDo数」(決定68)**: 監査専任席はworker数、reclaim、scale-down候補へ含めず、最終試験結果を待って監査を担う。claimできるToDoが無いworker席は仕事を発明せず、最終手段として親だけへ待機DMする
|
|
56
56
|
- **モードの選び分け**: タスク間に依存があり並列境界の機械保証が要るなら Lattice 併用。依存の無い小規模作業で、対象プロジェクトに Lattice を持ち込みたくないなら単独。単独で失うのは task 間スケジューリングの機械保証だけで、円卓の核(room・憲章・宣言による協力)は変わらない(決定47)
|
|
@@ -124,7 +124,7 @@ script は内部で Aiterm の公開 `claude_agent` / `codex_agent` / `grok_agen
|
|
|
124
124
|
- **席と spool は接触しない。** 席が触るのは room と worktree だけで、`.lattice/` の直読み・直書き禁止の契約はそのまま
|
|
125
125
|
- **席の作法の正本は `templates/member.md` の「Lattice の実行層へ自分の着手を載せる」節**(intake→intervention 判定→attach→作業→監査担当が `done.sh`→intake 席が accept の順・1席1 intake・禁止操作・検証の回し方・成果の正本)。ここに二重化しない
|
|
126
126
|
- **席は自分の pid を装置へ渡す(attach)。** その pid は room 台帳の member 行(`launch-seat.sh` が着席直後に登録する本人性欄)が持ち、席は `pull-attach-input.mjs` で読むだけで attach input になる(席file は 2026-08-22 廃止・member に帰属する情報の正本は台帳だけ)。**raw argv を保存しない**——token値をargvから除いた後も、将来の引数を無条件に複製しない。持つのはdigestだけ。Lattice の再観測は `/bin/ps -o command=`。pid/lstart が一致して digest だけ違うときは親が `skill/scripts/refresh-seat-identity.mjs <project> <name>` で揃える。席は台帳の本人性欄を書き換えない
|
|
127
|
-
- **run-bridge は退役した(2026-08-22・オーナー裁定)。** 介入(hold)は席が自分の Lattice コマンド応答(intake / attach / accept / `run intake intervention`)で受け取る——これが唯一の経路である。装置の介入を DM で先回り通知する中継は、凍った席には届かず、届いた席を退席させ、死んだ席の shell
|
|
127
|
+
- **run-bridge は退役した(2026-08-22・オーナー裁定)。** 介入(hold)は席が自分の Lattice コマンド応答(intake / attach / accept / `run intake intervention`)で受け取る——これが唯一の経路である。装置の介入を DM で先回り通知する中継は、凍った席には届かず、届いた席を退席させ、死んだ席の shell へ打鍵する事故だけを生んだので廃止した。現行の常駐bridgeは wakeup、seat-status、alarm の3本であり、Lattice介入の中継は持たない
|
|
128
128
|
|
|
129
129
|
運用側が踏みやすい所(実測で確認した挙動):
|
|
130
130
|
|
|
@@ -18,7 +18,9 @@ export function parseWindowsCreationDate(value) {
|
|
|
18
18
|
export function observeWindowsPidCommand(pid, hashArgv) {
|
|
19
19
|
const filter = `ProcessId=${pid}`
|
|
20
20
|
const script = `Get-CimInstance Win32_Process -Filter ${JSON.stringify(filter)} | Select-Object ProcessId,CreationDate,CommandLine | ConvertTo-Json -Compress`
|
|
21
|
-
const output = execFileSync('
|
|
21
|
+
const output = execFileSync('pwsh.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
}).trim()
|
|
22
24
|
if (!output) throw new Error('pid のWin32_Processを観測できない')
|
|
23
25
|
const row = JSON.parse(output)
|
|
24
26
|
const started = parseWindowsCreationDate(row.CreationDate)
|
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
export function resolveWindowsCommand(cli, argv, {
|
|
2
2
|
platform = process.platform,
|
|
3
3
|
exists,
|
|
4
|
-
|
|
4
|
+
pwsh = 'pwsh.exe',
|
|
5
5
|
} = {}) {
|
|
6
6
|
if (platform !== 'win32') return { command: cli, argv }
|
|
7
7
|
const lower = cli.toLowerCase()
|
|
8
|
-
|
|
8
|
+
if (lower.endsWith('.exe')) return { command: cli, argv }
|
|
9
|
+
|
|
10
|
+
const ps1 = lower.endsWith('.ps1')
|
|
9
11
|
? cli
|
|
10
|
-
:
|
|
11
|
-
? `${cli}.
|
|
12
|
-
: cli
|
|
13
|
-
if (
|
|
14
|
-
|
|
12
|
+
: /\.(?:cmd|bat)$/iu.test(cli)
|
|
13
|
+
? `${cli.slice(0, -4)}.ps1`
|
|
14
|
+
: `${cli}.ps1`
|
|
15
|
+
if (!exists?.(ps1)) {
|
|
16
|
+
const error = new Error(`PowerShell 7 shimが見つかりません: ${ps1}`)
|
|
17
|
+
error.code = 'PEERTABLE_WINDOWS_PWSH_SHIM_REQUIRED'
|
|
18
|
+
throw error
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
command: pwsh,
|
|
22
|
+
argv: ['-NoLogo', '-NoProfile', '-NonInteractive', '-File', ps1, ...argv],
|
|
23
|
+
}
|
|
15
24
|
}
|
|
16
25
|
|
|
17
|
-
export function resolveWindowsLatticeCommand(cli, exists) {
|
|
18
|
-
return resolveWindowsCommand(cli, ['todo', 'status', '--json'], {
|
|
26
|
+
export function resolveWindowsLatticeCommand(cli, exists, { pwsh = 'pwsh.exe' } = {}) {
|
|
27
|
+
return resolveWindowsCommand(cli, ['todo', 'status', '--json'], {
|
|
28
|
+
platform: 'win32',
|
|
29
|
+
exists,
|
|
30
|
+
pwsh,
|
|
31
|
+
})
|
|
19
32
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
2
|
+
// 同梱snapshotの役割→1位〜3位から、着席可能な harness / model / effort を解決する。
|
|
3
|
+
// 外部文書を読むのは呼出側が明示した時だけ。隣接repoの有無で製品挙動を変えない。
|
|
4
4
|
import { existsSync, readFileSync, realpathSync } from 'node:fs'
|
|
5
5
|
import { dirname, join, resolve } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
@@ -23,8 +23,6 @@ const fail = (code, message) => {
|
|
|
23
23
|
export function findModelsDoc({ env = process.env, exists = existsSync, scriptDir = here } = {}) {
|
|
24
24
|
if (env.PEERTABLE_MODELS_DOC) return resolve(env.PEERTABLE_MODELS_DOC)
|
|
25
25
|
if (env.DOTAGENTS_ROOT) return join(resolve(env.DOTAGENTS_ROOT), 'docs/02_models.md')
|
|
26
|
-
const sibling = resolve(scriptDir, '../../../dotagents/docs/02_models.md')
|
|
27
|
-
if (exists(sibling)) return sibling
|
|
28
26
|
const bundled = join(scriptDir, '../02_models.snapshot.md')
|
|
29
27
|
if (exists(bundled)) return bundled
|
|
30
28
|
return null
|
|
@@ -270,7 +268,7 @@ if (isMain) {
|
|
|
270
268
|
if (!roles) roles = positional[0] ?? ''
|
|
271
269
|
const doc = findModelsDoc()
|
|
272
270
|
if (!doc) {
|
|
273
|
-
fail('SEAT_MODELS_DOC_MISSING', '02_models.md
|
|
271
|
+
fail('SEAT_MODELS_DOC_MISSING', '同梱の02_models.snapshot.mdが見つからない')
|
|
274
272
|
}
|
|
275
273
|
if (!existsSync(doc)) {
|
|
276
274
|
fail('SEAT_MODELS_DOC_MISSING', `02_models.md が無い: ${doc}`)
|
|
@@ -59,7 +59,9 @@ function posixIdentity(pid) {
|
|
|
59
59
|
|
|
60
60
|
function winQuery(filter) {
|
|
61
61
|
const script = `Get-CimInstance Win32_Process -Filter ${JSON.stringify(filter)} | Select-Object ProcessId,ParentProcessId,CreationDate,CommandLine | ConvertTo-Json -Compress`
|
|
62
|
-
const out = execFileSync('
|
|
62
|
+
const out = execFileSync('pwsh.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script], {
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
}).trim()
|
|
63
65
|
if (!out) return []
|
|
64
66
|
const parsed = JSON.parse(out)
|
|
65
67
|
return Array.isArray(parsed) ? parsed : [parsed]
|
|
@@ -102,7 +102,7 @@ export function tmuxArgv(extraArgs = [], { socket, env = process.env, platform =
|
|
|
102
102
|
|
|
103
103
|
/**
|
|
104
104
|
* npm の extensionless shim は Windows の execFile で ENOENT。
|
|
105
|
-
*
|
|
105
|
+
* 同じnpm binにある .ps1 shimをPowerShell 7で起動し、cmd.exeへfallbackしない。
|
|
106
106
|
*/
|
|
107
107
|
export function resolveLatticeExecutable(cli, { platform = process.platform, exists = existsSync } = {}) {
|
|
108
108
|
if (typeof cli !== 'string' || !cli) return { command: cli, argv: ['todo', 'status', '--json'] }
|
|
@@ -113,9 +113,9 @@ export function resolveLatticeExecutable(cli, { platform = process.platform, exi
|
|
|
113
113
|
export function resolveLatticeInvocation(cli, argv, {
|
|
114
114
|
platform = process.platform,
|
|
115
115
|
exists = existsSync,
|
|
116
|
-
|
|
116
|
+
pwsh = 'pwsh.exe',
|
|
117
117
|
} = {}) {
|
|
118
|
-
return resolveWindowsCommand(cli, argv, { platform, exists,
|
|
118
|
+
return resolveWindowsCommand(cli, argv, { platform, exists, pwsh })
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
/** member の自己申告を優先し、無い既存 member だけ旧 session 名へ互換フォールバックする。 */
|
|
@@ -311,6 +311,17 @@ urllib.request.urlopen(req, timeout=10).read()
|
|
|
311
311
|
else
|
|
312
312
|
miss "停止済みbridge台帳の解除に失敗(HTTP ${bridge_code:-000})"
|
|
313
313
|
fi
|
|
314
|
+
# 解散した room を公開一覧(/api/rooms・トップページ主一覧)から外す。外さないと公開面が
|
|
315
|
+
# 過去卓で埋まり、公開トップの live 窓が死卓を選び続ける(2026-08-30 実測)。
|
|
316
|
+
# 次の setup の member 登録が自動で解除するので、復活に手作業は要らない
|
|
317
|
+
arch_code=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$url/api/$room/archive" -H "X-Peertable-Token: $PEERTABLE_POST_TOKEN" || true)
|
|
318
|
+
if [ "$arch_code" = 200 ]; then
|
|
319
|
+
did "room を公開一覧から archive(個別ページとログは残る)"
|
|
320
|
+
elif [ "$arch_code" = 404 ]; then
|
|
321
|
+
skip "room の archive(server が archive を持たない版・公開一覧に残る)"
|
|
322
|
+
else
|
|
323
|
+
miss "room の archive に失敗(HTTP ${arch_code:-000})"
|
|
324
|
+
fi
|
|
314
325
|
fi
|
|
315
326
|
# room 削除は --purge だけ。トークンを要する唯一の段で、ここだけが外部サービスへの依存境界
|
|
316
327
|
elif [ "$log_saved" = no ]; then
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// 背景: main plan の migrate 時に narrative_ref へ #L 行番号を持たせなかったため、
|
|
5
5
|
// `lattice todo split` が `predecessor_source_inventory_unavailable` で機構的に失敗した
|
|
6
|
-
// (実測 2026-08-11。docs/plan_peertable-autonomy-runtime-fx-20260811.md f4)。`todo revise` は
|
|
6
|
+
// (実測 2026-08-11。docs/archive/plan_peertable-autonomy-runtime-fx-20260811.md f4)。`todo revise` は
|
|
7
7
|
// 使えるが desired_plan 全体・source_cutover_batch を要求する重量級 API で、発見者が親裁定なしに
|
|
8
8
|
// 選べる手段ではない。このツールは「計画 Markdown に `### <task_id> <title>` 見出しで task を書く
|
|
9
9
|
// →本ツールで extraction.json を自動生成→`lattice todo migrate` で新規 companion plan として起票」
|
|
@@ -179,7 +179,7 @@ async function notifyParentOfFailure(seq, recipient, result, reason) {
|
|
|
179
179
|
if (!parentName) return
|
|
180
180
|
const key = `${seq}:${recipient}`
|
|
181
181
|
if (result === 'delivered') {
|
|
182
|
-
notifiedFailures.delete(key)
|
|
182
|
+
if (notifiedFailures.delete(key)) saveDeliveryState()
|
|
183
183
|
failureStreaks.delete(key)
|
|
184
184
|
return
|
|
185
185
|
}
|
|
@@ -202,6 +202,7 @@ async function notifyParentOfFailure(seq, recipient, result, reason) {
|
|
|
202
202
|
})
|
|
203
203
|
if (!res.ok) throw new Error(`HTTP ${res.status}`)
|
|
204
204
|
notifiedFailures.add(key)
|
|
205
|
+
saveDeliveryState()
|
|
205
206
|
} catch (error) {
|
|
206
207
|
log(`DELIVERY_FAILURE_NOTIFY_FAILED ${JSON.stringify({ seq, recipient, detail: error.message.split('\n')[0] })}`)
|
|
207
208
|
}
|
|
@@ -234,7 +235,10 @@ setInterval(beatBridge, 5_000) // 送信自体は BEAT_MS で間引く。失敗
|
|
|
234
235
|
// 席ごとに未配達を溜めて、2秒ごとにまとめて1回起こす(連投で席を何度も起こさない)。
|
|
235
236
|
// pending は room message の宛先名から作る。起動引数や harness は候補集合を決めない。
|
|
236
237
|
const pending = new Map() // name -> Map<seq, message>
|
|
237
|
-
const stuckStreaks = new Map() // name -> { key: 'seq,seq', count }
|
|
238
|
+
const stuckStreaks = new Map() // name -> { key: 'seq,seq', code, count }(連続失敗数。STUCK=5・席不在=150周期で打ち切り)
|
|
239
|
+
// 席不在系の打ち切り周期(150周期≈5分)。上書きは experiments の再現 harness が待ち時間短縮に使うだけ
|
|
240
|
+
const goneAbandonCycles = Number(process.env.PEERTABLE_GONE_ABANDON_CYCLES) > 0
|
|
241
|
+
? Number(process.env.PEERTABLE_GONE_ABANDON_CYCLES) : 150
|
|
238
242
|
const deliveryStates = new Map() // seq -> { message, targets, delivered }
|
|
239
243
|
let seats = []
|
|
240
244
|
let members = new Map()
|
|
@@ -300,14 +304,15 @@ function markReady() {
|
|
|
300
304
|
function loadDeliveryState() {
|
|
301
305
|
try {
|
|
302
306
|
const saved = JSON.parse(readFileSync(deliveryStatePath, 'utf8'))
|
|
303
|
-
if (saved.room !== room || saved.server_url !== url) return { primed: false, lastSeq: 0, delivered: new Set() }
|
|
307
|
+
if (saved.room !== room || saved.server_url !== url) return { primed: false, lastSeq: 0, delivered: new Set(), notified: new Set() }
|
|
304
308
|
return {
|
|
305
309
|
primed: saved.primed === true,
|
|
306
310
|
lastSeq: Number.isSafeInteger(saved.last_seq) && saved.last_seq >= 0 ? saved.last_seq : 0,
|
|
307
311
|
delivered: new Set(Array.isArray(saved.delivered) ? saved.delivered.filter(key => typeof key === 'string') : []),
|
|
312
|
+
notified: new Set(Array.isArray(saved.notified) ? saved.notified.filter(key => typeof key === 'string') : []),
|
|
308
313
|
}
|
|
309
314
|
} catch {
|
|
310
|
-
return { primed: false, lastSeq: 0, delivered: new Set() }
|
|
315
|
+
return { primed: false, lastSeq: 0, delivered: new Set(), notified: new Set() }
|
|
311
316
|
}
|
|
312
317
|
}
|
|
313
318
|
|
|
@@ -315,6 +320,10 @@ const deliveryState = loadDeliveryState()
|
|
|
315
320
|
let lastSeq = deliveryState.lastSeq
|
|
316
321
|
const delivered = deliveryState.delivered
|
|
317
322
|
let primed = deliveryState.primed
|
|
323
|
+
// 通知済み集合を耐再起動にする。メモリだけだと bridge が再起動するたび同じ [配達失敗] DM を
|
|
324
|
+
// room へ再投稿する(2026-08-30 実測: 13回の再起動で同一通知が13重複し、公開feedの末尾78件が
|
|
325
|
+
// 全部配達失敗になった)
|
|
326
|
+
for (const key of deliveryState.notified) notifiedFailures.add(key)
|
|
318
327
|
function saveDeliveryState() {
|
|
319
328
|
const temp = `${deliveryStatePath}.${process.pid}.tmp`
|
|
320
329
|
writeFileSync(temp, JSON.stringify({
|
|
@@ -323,6 +332,7 @@ function saveDeliveryState() {
|
|
|
323
332
|
primed,
|
|
324
333
|
last_seq: lastSeq,
|
|
325
334
|
delivered: [...delivered].slice(-10_000),
|
|
335
|
+
notified: [...notifiedFailures].slice(-10_000),
|
|
326
336
|
}) + '\n')
|
|
327
337
|
renameSync(temp, deliveryStatePath)
|
|
328
338
|
}
|
|
@@ -723,18 +733,31 @@ async function flushSeat(seat) {
|
|
|
723
733
|
// 再試行が成立すれば同じ (seq, recipient) を delivered で上書きする
|
|
724
734
|
const result = ['SEAT_TUI_GONE', 'MEMBER_MISSING', 'DESCRIPTOR_MISSING'].includes(code) ? 'seat_unavailable' : 'failed'
|
|
725
735
|
for (const msg of msgs) await postReceipt(msg.seq, seat, result, code)
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
|
|
736
|
+
// 同一集合の失敗が続いたら再試行を打ち切る。STUCK は5周期(composer汚染を止める)、
|
|
737
|
+
// 席不在系は150周期≈5分(席の再起動・立て直しは待ち、卓ごと死んだ席だけを見切る)。
|
|
738
|
+
// 打ち切りは delivered 台帳へ耐再起動で記録する——記録しないと lastSeq が前進せず、
|
|
739
|
+
// bridge の再起動ごとに同じ seq の再試行が蘇る(2026-08-30 実測: 席全滅の卓で
|
|
740
|
+
// seq 2件が18時間・3.7万行の SEAT_TUI_GONE を刻み続けた)。failed receipt と
|
|
741
|
+
// 親宛[配達失敗]DMは既に出ており、無限再試行は誰も救わない。
|
|
742
|
+
const abandonAfter = code === 'DELIVERY_STUCK' ? 5
|
|
743
|
+
: ['SEAT_TUI_GONE', 'MEMBER_MISSING', 'DESCRIPTOR_MISSING'].includes(code) ? goneAbandonCycles
|
|
744
|
+
: null
|
|
745
|
+
if (abandonAfter !== null) {
|
|
729
746
|
const key = msgs.map((msg) => msg.seq).join(',')
|
|
730
747
|
const prev = stuckStreaks.get(seat)
|
|
731
|
-
const count = prev?.key === key ? prev.count + 1 : 1
|
|
732
|
-
stuckStreaks.set(seat, { key, count })
|
|
733
|
-
if (count >=
|
|
748
|
+
const count = prev?.key === key && prev.code === code ? prev.count + 1 : 1
|
|
749
|
+
stuckStreaks.set(seat, { key, code, count })
|
|
750
|
+
if (count >= abandonAfter) {
|
|
734
751
|
const queue = pending.get(seat)
|
|
735
|
-
|
|
752
|
+
for (const msg of msgs) {
|
|
753
|
+
if (queue) queue.delete(msg.seq)
|
|
754
|
+
deliveryStates.get(msg.seq)?.delivered.add(seat)
|
|
755
|
+
delivered.add(deliveryKey(msg.seq, seat))
|
|
756
|
+
}
|
|
736
757
|
stuckStreaks.delete(seat)
|
|
737
|
-
|
|
758
|
+
saveDeliveryState()
|
|
759
|
+
advanceLastSeq()
|
|
760
|
+
log(`DELIVERY_ABANDONED: ${seat} への seq ${key} は${count}周期連続${code}のため再試行を打ち切る(receipt=${result}・親へ通知済み・再起動後も再試行しない)`)
|
|
738
761
|
}
|
|
739
762
|
} else {
|
|
740
763
|
stuckStreaks.delete(seat)
|