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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peertable",
3
- "version": "0.8.56",
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.56'
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 { if (!dependencies.aiterm) await aiterm.close() }
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 の一経路だけを使う。