peertable 0.8.56 → 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/package.json +1 -1
- package/room/client.mjs +1 -1
- package/skill/scripts/launch-seat.mjs +8 -1
- package/skill/scripts/launch-seat.test.mjs +15 -0
- package/skill/scripts/leave-seat.mjs +2 -0
- package/skill/scripts/leave-seat.test.mjs +11 -0
- package/skill/scripts/seat-launch-phase.mjs +11 -0
- package/skill/scripts/wakeup-bridge.mjs +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peertable",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.57",
|
|
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.57'
|
|
18
18
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
19
19
|
|
|
20
20
|
const USAGE = `usage:
|
|
@@ -11,6 +11,7 @@ import { ensureProjectRuntime } from './ensure-project-runtime.mjs'
|
|
|
11
11
|
import { projectPath, readSetup, runScript, fail } from './project-scaffold.mjs'
|
|
12
12
|
import { packageRoot } from './install-skill.mjs'
|
|
13
13
|
import { resolveWindowsCommand } from './platform/windows/resolve-lattice-command.mjs'
|
|
14
|
+
import { beginSeatLaunch, endSeatLaunch } from './seat-launch-phase.mjs'
|
|
14
15
|
|
|
15
16
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
16
17
|
const harnessIds = { claude: 'claude-code', codex: 'codex-cli', grok: 'grok-cli' }
|
|
@@ -87,10 +88,13 @@ export async function launchSeat(options, dependencies = {}) {
|
|
|
87
88
|
const aiterm = dependencies.aiterm ?? new AitermClient({ env })
|
|
88
89
|
const sessionId = `peer-${name}`
|
|
89
90
|
let launched = false
|
|
91
|
+
let launchPhase = false
|
|
90
92
|
try {
|
|
91
93
|
prepareHarness(project, name, harness, env, script, home)
|
|
92
94
|
const before = await aiterm.sessions()
|
|
93
95
|
if (before.some(session => session.session_id === sessionId)) fail('SEAT_SESSION_CONFLICT', `${sessionId} が既に存在します`)
|
|
96
|
+
beginSeatLaunch(project, name)
|
|
97
|
+
launchPhase = true
|
|
94
98
|
launched = true
|
|
95
99
|
const launch = await aiterm.structured('agent_launch', {
|
|
96
100
|
session_name: sessionId, harness: harnessIds[harness], model, reasoning_effort: effort,
|
|
@@ -152,5 +156,8 @@ export async function launchSeat(options, dependencies = {}) {
|
|
|
152
156
|
catch (rollback) { throw new AggregateError([error, rollback], `着座失敗後の撤去も失敗しました: ${error.message}; ${rollback.message}`) }
|
|
153
157
|
} else script('seat-credential.mjs', ['remove', project, credential], { env })
|
|
154
158
|
throw error
|
|
155
|
-
} finally {
|
|
159
|
+
} finally {
|
|
160
|
+
if (launchPhase) endSeatLaunch(project, name)
|
|
161
|
+
if (!dependencies.aiterm) await aiterm.close()
|
|
162
|
+
}
|
|
156
163
|
}
|
|
@@ -6,6 +6,7 @@ import { tmpdir } from 'node:os'
|
|
|
6
6
|
import { join } from 'node:path'
|
|
7
7
|
import { launchSeat } from './launch-seat.mjs'
|
|
8
8
|
import { runScript } from './project-scaffold.mjs'
|
|
9
|
+
import { isSeatLaunching } from './seat-launch-phase.mjs'
|
|
9
10
|
|
|
10
11
|
function fixture(t, harness = 'claude') {
|
|
11
12
|
const project = mkdtempSync(join(tmpdir(), 'peertable-launch-'))
|
|
@@ -65,6 +66,20 @@ test('起動準備・登録・runtimeの成立後に長い日本語briefを一
|
|
|
65
66
|
assert.deepEqual(f.events.find(event => event.wait).wait, { executable: 'opaque-waiter', args: ['opaque-cursor'] })
|
|
66
67
|
})
|
|
67
68
|
|
|
69
|
+
test('参加通知が着任指示に先行しないよう、公開起動から初回送信まで配達を保留する', async t => {
|
|
70
|
+
const f = fixture(t)
|
|
71
|
+
const structured = f.dependencies.aiterm.structured
|
|
72
|
+
f.dependencies.aiterm.structured = async (tool, args) => {
|
|
73
|
+
if (tool === 'agent_launch' || tool === 'pty_send') assert.equal(isSeatLaunching(f.options.project, 'alice'), true)
|
|
74
|
+
return structured(tool, args)
|
|
75
|
+
}
|
|
76
|
+
await launchSeat(f.options, f.dependencies)
|
|
77
|
+
assert.equal(isSeatLaunching(f.options.project, 'alice'), false)
|
|
78
|
+
f.dependencies.aiterm.structured = async () => { throw new Error('外部起動失敗') }
|
|
79
|
+
await assert.rejects(launchSeat(f.options, f.dependencies), /外部起動失敗/)
|
|
80
|
+
assert.equal(isSeatLaunching(f.options.project, 'alice'), false)
|
|
81
|
+
})
|
|
82
|
+
|
|
68
83
|
test('モデル実測失敗とサイズ超過は既存席へ触らない', async t => {
|
|
69
84
|
const f = fixture(t)
|
|
70
85
|
f.dependencies.execFileSync = () => { throw new Error('モデル利用不可') }
|
|
@@ -5,6 +5,7 @@ import { AitermClient } from './aiterm-client.mjs'
|
|
|
5
5
|
import { RoomApi } from './room-api.mjs'
|
|
6
6
|
import { findSeatSession } from './seat-session.mjs'
|
|
7
7
|
import { projectPath, readSetup, runScript, fail } from './project-scaffold.mjs'
|
|
8
|
+
import { endSeatLaunch } from './seat-launch-phase.mjs'
|
|
8
9
|
|
|
9
10
|
export async function leaveSeat(project, name, { aiterm, api, credentialCommand = runScript } = {}) {
|
|
10
11
|
if (!name || !/^[A-Za-z0-9._:-]+$/.test(name)) fail('SEAT_LEAVE_ARGS_INVALID', 'member名に使えない文字があります')
|
|
@@ -34,6 +35,7 @@ export async function leaveSeat(project, name, { aiterm, api, credentialCommand
|
|
|
34
35
|
await api.request(`members/${encodeURIComponent(name)}`, { method: 'DELETE' })
|
|
35
36
|
if ((await api.members()).some(item => item.name === name)) fail('SEAT_LEAVE_MEMBER_FAILED', `${name} の登録が残っています`)
|
|
36
37
|
for (const suffix of ['.grok-home', '.codex']) rmSync(join(project, '.team', 'seats', name + suffix), { recursive: true, force: true })
|
|
38
|
+
endSeatLaunch(project, name)
|
|
37
39
|
credential(['remove', project, file])
|
|
38
40
|
return { schema: 'peertable.seat-leave.v1', status: 'left', member: name, session_id: session?.session_id ?? null }
|
|
39
41
|
} finally {
|
|
@@ -4,6 +4,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from 'node:
|
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
import { leaveSeat } from './leave-seat.mjs'
|
|
7
|
+
import { beginSeatLaunch, endSeatLaunch, isSeatLaunching } from './seat-launch-phase.mjs'
|
|
7
8
|
|
|
8
9
|
function fixture(t, failure) {
|
|
9
10
|
const project = mkdtempSync(join(tmpdir(), 'peertable-leave-'))
|
|
@@ -37,6 +38,16 @@ test('公開APIで停止を確認してから登録と秘密を消し、他のro
|
|
|
37
38
|
assert.equal(existsSync(join(f.project, '.team', 'seats', 'alice.grok-home')), false)
|
|
38
39
|
})
|
|
39
40
|
|
|
41
|
+
test('中断した起動の配達保留を退席で解除し、同名の再着席を可能にする', async t => {
|
|
42
|
+
const f = fixture(t)
|
|
43
|
+
beginSeatLaunch(f.project, 'alice')
|
|
44
|
+
await leaveSeat(f.project, 'alice', f)
|
|
45
|
+
assert.equal(isSeatLaunching(f.project, 'alice'), false)
|
|
46
|
+
beginSeatLaunch(f.project, 'alice')
|
|
47
|
+
endSeatLaunch(f.project, 'alice')
|
|
48
|
+
endSeatLaunch(f.project, 'alice')
|
|
49
|
+
})
|
|
50
|
+
|
|
40
51
|
for (const failure of ['list', 'close']) test(`Aitermの${failure}失敗では登録とcredentialを保つ`, async t => {
|
|
41
52
|
const f = fixture(t, failure)
|
|
42
53
|
await assert.rejects(leaveSeat(f.project, 'alice', f))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// 着任指示を渡すまで、その席へのroom配達を保留する。状態はPeertableが所有する。
|
|
2
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
const phasePath = (project, name) => join(project, '.team', 'seats', `${name}.launching`)
|
|
6
|
+
export const isSeatLaunching = (project, name) => existsSync(phasePath(project, name))
|
|
7
|
+
export function beginSeatLaunch(project, name) {
|
|
8
|
+
mkdirSync(join(project, '.team', 'seats'), { recursive: true })
|
|
9
|
+
writeFileSync(phasePath(project, name), `${process.pid}\n`, { flag: 'wx' })
|
|
10
|
+
}
|
|
11
|
+
export function endSeatLaunch(project, name) { rmSync(phasePath(project, name), { force: true }) }
|
|
@@ -25,6 +25,7 @@ import { AitermClient } from './aiterm-client.mjs'
|
|
|
25
25
|
import { seatSessionId } from './seat-session.mjs'
|
|
26
26
|
import { passSeatApproval } from './seat-approval.mjs'
|
|
27
27
|
import { BROADCAST_RECIPIENT, formatWakeNotice, isIdleSelfWake, isWakeupBridgeTarget, memberHarness, shouldDeferGrokWake } from './wakeup-delivery.mjs'
|
|
28
|
+
import { isSeatLaunching } from './seat-launch-phase.mjs'
|
|
28
29
|
import { bridgeRecordLive } from './bridge-record-live.mjs'
|
|
29
30
|
|
|
30
31
|
const run = promisify(execFile)
|
|
@@ -376,6 +377,7 @@ async function passKnownSeatApproval(member, observation) {
|
|
|
376
377
|
}
|
|
377
378
|
|
|
378
379
|
async function wake(seat, msgs) {
|
|
380
|
+
if (isSeatLaunching(proj, seat)) return 'deferred'
|
|
379
381
|
const last = msgs[msgs.length - 1]
|
|
380
382
|
const text = msgs.map(formatWakeNotice).join(' || ')
|
|
381
383
|
// 配送直前に member ledger を取り直し、current name -> descriptor の一経路だけを使う。
|