aiterm-mcp 0.57.0 → 0.57.1

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/CHANGELOG.md CHANGED
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.57.1] - 2026-10-07
11
+
12
+ ### 修正
13
+
14
+ - Windowsで、`pty_close`されたClaude Codeが会話終了のhook(SessionEnd)を走らせない件を直す(ADR 0100)。psmuxは、sessionを止める時に
15
+ 画面のprocessへ切断の合図を送らない(POSIXのtmuxは送る)。
16
+ - WindowsのClaude Codeの席を閉じる時、止める前にC-cを間を空けずに2回送り、Claude Codeが自分で終わるのを最大4秒待つ。
17
+ 動いている番の席へは、先にC-cを1回送って入力待ちに戻るのを最大1.5秒待つ。終わらなくても、その後は今までどおり止める。
18
+ - `pty_close`は、WindowsのClaude Codeの席でだけ遅くなる(実物で、入力待ちの席は約2.3秒、動いている番の席は約3.2秒)。
19
+ - Linux・macOS、Codex・Grok・Cursorの席、返りの形は変わらない。
20
+ - Codexが回答を流している間に届いた文を、新しいturnでなく差し込み(`mode: agent_steer`)として返す(ADR 0101)。Codex(0.160.1)は
21
+ 回答を流している間、画面に動作中の行を出さない。今までは「入力待ち」と読んで新しいturnとして返し、Codexはその文を動いている
22
+ turnへ取り込むので、同じ1つの回答が2回の完了として届いていた(親への配送があれば2通)。
23
+ - 画面に入力欄が見え、この起動の記録(rollout)の最後の境界がturnの開始で、Codexのprocessが生きていて、0.4秒おいて見直しても
24
+ 記録が同じturnの開始のままで画面が動いている時だけ、動いていると数える。1つでも欠けたら、今までどおり新しいturnで返す。
25
+ - その間に届いた文の受け付けは、5.6秒から約0.8秒になる(入力受付の11回の確かめを通らない。Windowsは約2秒)。
26
+ - 引数・返りの形・断りの文面、Claude Code・Grok・Cursorの席、画面に動作中の行が出ている時の動きは変わらない。
27
+ - 配送パッケージ`aiterm-steer-delivery`を0.3.1へ上げる。
28
+ - Codexの親配送の登録(`aiterm-setup --codex-steer enable`、`ensureCodexParentSteer`)が、登録の間に終わったCodexを、再起動待ちの記録
29
+ (`stale_processes`)へ残さない。ほかの導入が設定のために一時的に起こして止めたCodexが、止まり切る前の一覧に載って残っていた。
30
+ 居ないprocessは照合で当たらないので、配送は止まっていなかった。
31
+ - Codexを起こす時、PATHにnodeの場所が無ければ足す。0.56.1で`aiterm-parent-delivery`が自分でしていた事を、MCPの本体からCodexを
32
+ 起こす所(認証の確かめ、model一覧、親配送)にも広げる。
33
+
10
34
  ## [0.57.0] - 2026-10-07
11
35
 
12
36
  ### 追加
@@ -2356,7 +2380,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2356
2380
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2357
2381
  provenance.
2358
2382
 
2359
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.0...HEAD
2383
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.1...HEAD
2384
+ [0.57.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.0...v0.57.1
2360
2385
  [0.57.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.56.1...v0.57.0
2361
2386
  [0.56.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.56.0...v0.56.1
2362
2387
  [0.56.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.4...v0.56.0
package/README.ja.md CHANGED
@@ -209,7 +209,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
209
209
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
210
210
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
211
211
 
212
- **状態:** 開発継続中 · 現行公開版 **v0.57.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.57.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
@@ -401,6 +401,9 @@ pty_close("t1") → 端末を解放
401
401
 
402
402
  `pty_close` は冪等で、`closed` / `already_closed` のstructured receiptを返す。
403
403
  MCP応答を失ったdurable callerも同じ`session_id`への再試行だけでclose結果を確定できる。
404
+ Windowsでは、Claude Codeの席を閉じる前に、Claude Codeへ自分で終わる機会を与える(C-cを2回送り、最大4秒待つ)。
405
+ psmuxは、POSIXのtmuxが送る切断の合図なしでsessionを止めるので、そのままでは会話終了のhook(SessionEnd)が走らない。
406
+ 終わっても終わらなくても、その後にsessionを止める。
404
407
 
405
408
  これだけ。`t1` の端末は本物で永続——`ssh`・`docker exec`・REPL・起動したエージェントのTUIは、そこに住む「もの」に過ぎない。ワーカー起動も1コールで、`agent_launch({ harness: "codex-cli" })`が返す`session_id`を同じ`pty_read`/`pty_send`で操作する。
406
409
 
package/README.md CHANGED
@@ -223,7 +223,7 @@ collection is off by default and performs no network I/O. It ships via
223
223
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
224
224
  Release re-registers the Official MCP Registry entry.
225
225
 
226
- **Status:** actively maintained · current public release **v0.57.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
226
+ **Status:** actively maintained · current public release **v0.57.1** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
227
227
 
228
228
  ### Update and rollback
229
229
 
@@ -431,6 +431,9 @@ pty_close("t1") → terminal released
431
431
 
432
432
  `pty_close` is idempotent and returns a structured `closed` / `already_closed`
433
433
  receipt, so durable callers can retry the same `session_id` after losing the MCP response.
434
+ On Windows, closing a Claude Code session first asks Claude Code to exit on its own (Ctrl-C twice, then a wait of up to 4 seconds)
435
+ so that its `SessionEnd` hooks run; psmux stops a session without the hangup signal that tmux sends on POSIX. The session is stopped
436
+ afterwards either way.
434
437
 
435
438
  That's it. The terminal in `t1` is real and persistent — `ssh`, `docker exec`, a REPL, or a launched agent's TUI are just things that live inside it. To launch a worker agent instead, one call does it: `agent_launch({ harness: "codex-cli" })` returns a `session_id` you drive with the same `pty_read` / `pty_send`.
436
439
 
package/dist/core.js CHANGED
@@ -22,7 +22,7 @@ import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, past
22
22
  import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_RATE_LIMIT_POLL_MS, pollGate, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
23
23
  import { catalogUnavailable } from "./model-catalog.js";
24
24
  import { grokAuthPlan, grokAuthStatus, grokAuthPane, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokModelChoices, grokEventsTranscript, latestGrokCompletion, grokTurnSettled, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
25
- import { codexAuthPlan, codexAuthStatus, codexAuthPane, bindCodexTranscriptSession, latestCodexCompletion, codexTurnSettled, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexTuiBusy, codexHelperProcess, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, } from "./harnesses/codex.js";
25
+ import { codexAuthPlan, codexAuthStatus, codexAuthPane, bindCodexTranscriptSession, latestCodexCompletion, codexTurnSettled, codexRunningTurn, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexTuiBusy, codexHelperProcess, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, } from "./harnesses/codex.js";
26
26
  import { claudeAuthPlan, claudeAuthStatus, claudeAuthPane, OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, claudeModelChoices, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeUsageLimit, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, claudeApiErrorAfter, } from "./harnesses/claude.js";
27
27
  import { cursorAuthPlan, cursorAuthStatus, cursorAuthPane, bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, cursorModelChoices, buildCursorAgentCmd, cursorAgentArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, cursorPromptHooksRunning, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
28
28
  import { readInterimWords, recordInterimBoundary } from "./interim-words.js";
@@ -1386,8 +1386,8 @@ export function readOnlyPtyListDiagnostic(runTmux = tmux) {
1386
1386
  }
1387
1387
  return { status: "unverified", session_count: null };
1388
1388
  }
1389
- function closeSessionInternal(name, observeDependency = true) {
1390
- assertSessionName(name);
1389
+ /** 別processが待機中・送信中のsessionは閉じない。閉じる前の操作(下のClaude Codeへの終了の鍵)より先に確かめる。 */
1390
+ function assertSessionClosable(name) {
1391
1391
  {
1392
1392
  // 別プロセスの待機は in-memory Set に映らない。生きた file lock があれば close で state を消さない
1393
1393
  const foreign = liveWaitLocks(name);
@@ -1403,6 +1403,80 @@ function closeSessionInternal(name, observeDependency = true) {
1403
1403
  throw new AitermError(`session '${name}' は別プロセス${d.pid != null ? `(pid ${d.pid})` : ""}の送信中のため close できません`, 2);
1404
1404
  }
1405
1405
  }
1406
+ }
1407
+ // Windowsのpsmuxは、sessionを止める時に画面のprocessへ切断の合図を送らない(POSIXのtmuxはSIGHUPを送り、Claude Codeは
1408
+ // 片付けて終わる)。合図なしで止められたClaude Codeは、会話終了のhook(SessionEnd)を走らせない。
1409
+ // 止める前に、Claude Codeへ自分で終わる機会を与える(ADR 0100)。
1410
+ // 動いている番を止めた後、入力待ちに戻るのを待つ上限。
1411
+ const CLAUDE_EXIT_INTERRUPT_WAIT_MS = 1500;
1412
+ // 終了の受付(「Press Ctrl-C again to exit」)は短い。実物では、間が0.3〜0.7秒の2回は通り、2.2秒の2回は切れた。
1413
+ const CLAUDE_EXIT_KEY_GAP_MS = 200;
1414
+ // 2回目の後、会話終了のhookを走らせて終わるのを待つ上限。実物(fox)では、hookが走ったのは2回目の約1秒後で、processはその
1415
+ // 0.2〜0.3秒後に終わった。hookが遅い端末でも走り切れるように、余りを取る。
1416
+ const CLAUDE_EXIT_WAIT_MS = 4000;
1417
+ const CLAUDE_EXIT_POLL_MS = 100;
1418
+ /**
1419
+ * Claude Codeへ終了を頼む鍵と待ちの順番。
1420
+ * 入力待ちでない席(動いている番など)へは、先にC-cを1回送って入力待ちに戻るのを待つ(そのC-cは番を止めるのに使われ、
1421
+ * 終了の受付にならない)。その後、C-cを間を空けずに2回送り、processが終わるのを待つ。
1422
+ * 終わらなかった時も、呼ぶ側は今までどおりsessionを止める。
1423
+ */
1424
+ export function requestClaudeExit(steps) {
1425
+ if (!steps.idle()) {
1426
+ steps.interrupt();
1427
+ const until = steps.now() + CLAUDE_EXIT_INTERRUPT_WAIT_MS;
1428
+ while (!steps.idle() && steps.now() < until)
1429
+ steps.pause(CLAUDE_EXIT_POLL_MS);
1430
+ }
1431
+ steps.interrupt();
1432
+ steps.pause(CLAUDE_EXIT_KEY_GAP_MS);
1433
+ steps.interrupt();
1434
+ const until = steps.now() + CLAUDE_EXIT_WAIT_MS;
1435
+ for (;;) {
1436
+ if (!steps.alive())
1437
+ return "exited";
1438
+ if (steps.now() >= until)
1439
+ return "still_running";
1440
+ steps.pause(CLAUDE_EXIT_POLL_MS);
1441
+ }
1442
+ }
1443
+ let claudeExitBeforeCloseTestOverride = null;
1444
+ /** 試験用: POSIXでも、閉じる前にClaude Codeへ終了を頼む(nullで既定へ戻す)。 */
1445
+ export function __testSetClaudeExitBeforeClose(value) {
1446
+ claudeExitBeforeCloseTestOverride = value;
1447
+ }
1448
+ /**
1449
+ * 閉じる席がWindowsのClaude Codeで、processが生きている時だけ、止める前に終了を頼む。
1450
+ * 待ちは同期のまま(最大で数秒)。待つ間にほかの呼び出しを通すと、閉じかけの席へ送信が入る。
1451
+ * 席の観測や鍵の送信に失敗した時は、何もせずに今までどおり止める(閉じる事を妨げない)。
1452
+ */
1453
+ function letClaudeExitBeforeClose(name) {
1454
+ if (!(claudeExitBeforeCloseTestOverride ?? isWin))
1455
+ return;
1456
+ try {
1457
+ if (tryLoadAgentMetadata(name)?.kind !== "claude" || !sessionExists(name))
1458
+ return;
1459
+ const seen = observeSession(name);
1460
+ if (seen.harness_alive !== true)
1461
+ return;
1462
+ const harness = seen.harness_process;
1463
+ const pause = new Int32Array(new SharedArrayBuffer(4));
1464
+ requestClaudeExit({
1465
+ idle: () => claudePaneObservation(captureScreen(name, AGENT_TUI_READY_LINES)).state === "idle",
1466
+ interrupt: () => { tmux("send-keys", "-t", name, "C-c"); },
1467
+ // processが1つに決まらない席は、席の観測で見る(Windowsではprocessの一覧を引くので遅い)。
1468
+ alive: () => harness ? isPidAlive(harness.pid) : observeSession(name).harness_alive === true,
1469
+ pause: ms => { Atomics.wait(pause, 0, 0, ms); },
1470
+ now: () => performance.now(),
1471
+ });
1472
+ }
1473
+ catch { /* 終了を頼めなかった席は、今までどおり止める */ }
1474
+ }
1475
+ function closeSessionInternal(name, observeDependency = true, o = {}) {
1476
+ assertSessionName(name);
1477
+ assertSessionClosable(name);
1478
+ if (o.letHarnessExit)
1479
+ letClaudeExitBeforeClose(name);
1406
1480
  (observeDependency ? tmux : tmuxCleanup)("kill-session", "-t", name);
1407
1481
  for (const p of [logpath(name), offsetpath(name), lastcmdpath(name), markpath(name), sendLockPath(name)]) {
1408
1482
  try {
@@ -1430,7 +1504,7 @@ export function closeSession(name) {
1430
1504
  export function closeSessionResult(name) {
1431
1505
  assertSessionName(name);
1432
1506
  const existed = sessionExists(name);
1433
- closeSessionInternal(name, true);
1507
+ closeSessionInternal(name, true, { letHarnessExit: true });
1434
1508
  return {
1435
1509
  schema: "aiterm.pty-close-result.v1",
1436
1510
  session_id: name,
@@ -4396,6 +4470,39 @@ async function waitSteerScreen(name, predicate, stillRunning) {
4396
4470
  export function __testSteerQueued(kind, screen) {
4397
4471
  return kind === "cursor" ? cursorSteerQueued(screen) : grokSteerQueued(screen);
4398
4472
  }
4473
+ // 記録の上でturnが動いている席を、間を置いて見直すまでの待ち。回答を流している画面は、この間に必ず動く(実測で0.15秒ごと)。
4474
+ const CODEX_RECORD_RUNNING_CONFIRM_MS = 400;
4475
+ /**
4476
+ * Codexの席で、画面は入力待ちに見えるが、記録の上ではturnが動いているか(回答を流している間。Codex 0.160.1は、その間
4477
+ * 動作中の行を出さない)。
4478
+ * 止まっている席を動いていると読むと、文は新しいturnになるのに差し込みとして返り、その完了を誰も待たない。
4479
+ * 読み違えない方へ倒し、次を全部満たす時だけtrueにする。1つでも欠けたら、今までどおり新しいturnの道へ回す。
4480
+ * - 画面に入力欄が見えている(承認などの画面は、新しいturnの道が今までどおり打鍵前に断る)。
4481
+ * - この起動の記録の最後の境界がturnの開始で、harnessのprocessが生きている(Codexは、生きている間はturnの終わりを必ず書く)。
4482
+ * - 間を置いて見直しても、記録は同じturnの開始のままで、画面が動いている(止まっている席の画面は動かない)。
4483
+ */
4484
+ async function codexTurnRunningBehindIdleScreen(name, meta, screen) {
4485
+ try {
4486
+ if (!isAgentTuiIdleReady("codex", screen))
4487
+ return false;
4488
+ const turn = codexRunningTurn(meta);
4489
+ if (turn === null)
4490
+ return false;
4491
+ if (observeSession(name).harness_alive !== true)
4492
+ return false;
4493
+ await sleep(CODEX_RECORD_RUNNING_CONFIRM_MS);
4494
+ const later = captureScreen(name, AGENT_TUI_READY_LINES);
4495
+ if (codexRunningTurn(meta) !== turn)
4496
+ return false;
4497
+ // 見直す間に動作中の行が出た時は、画面の読みでも動いている。
4498
+ if (isAgentTuiBusy("codex", later))
4499
+ return true;
4500
+ return later !== screen && isAgentTuiIdleReady("codex", later);
4501
+ }
4502
+ catch {
4503
+ return false; /* 読めない時は、動いていると数えない */
4504
+ }
4505
+ }
4399
4506
  /**
4400
4507
  * agent sessionへの唯一の送信口。呼び出し側は子の状態を知らないまま呼び、Aitermがこの時点の画面で振り分ける。
4401
4508
  * 実行中なら現在のturnへ差し込み(steer)、そうでなければ新しいturnとしてdispatchする。
@@ -4414,7 +4521,8 @@ async function sendAgentMessageInLock(name, text, o) {
4414
4521
  const meta = loadAgentMetadata(name);
4415
4522
  // 前面回復は busy 判定より先(bash 前面のままだと画面の実行中マーカーを読んでも打鍵が届かない)。
4416
4523
  const paneInputRecovery = await ensureAgentOwnsPaneInput(name, meta.kind);
4417
- let running = isAgentTuiBusy(meta.kind, captureScreen(name, AGENT_TUI_READY_LINES));
4524
+ const screen = captureScreen(name, AGENT_TUI_READY_LINES);
4525
+ let running = isAgentTuiBusy(meta.kind, screen);
4418
4526
  // Claudeのtool処理中は画面のbusy表示が消えることがある。Stopまで保持するturnの印を正とし、
4419
4527
  // APIエラーでStopが来なかった印は、会話記録の終了時刻を確認して消す。
4420
4528
  // 有効な印がある間は差し込み、終了後は画面にbusy表示が残っても新しいturnとして送る。
@@ -4427,6 +4535,9 @@ async function sendAgentMessageInLock(name, text, o) {
4427
4535
  // turnは終わっているので新しいturnとして送る。dispatchは入力を受け付けるまで待ってから送る。
4428
4536
  if (running && meta.kind === "cursor" && latestCursorCompletion(meta, readTranscriptLines) !== null)
4429
4537
  running = false;
4538
+ // Codexは回答を流している間、画面に動作中の行を出さない。その間に届いた文を、Codexは動いているturnへ取り込む(ADR 0101)。
4539
+ if (!running && meta.kind === "codex" && await codexTurnRunningBehindIdleScreen(name, meta, screen))
4540
+ running = true;
4430
4541
  if (running) {
4431
4542
  return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery, preface: o.preface });
4432
4543
  }
@@ -262,24 +262,23 @@ export function codexUsageLimit(record) {
262
262
  return error?.info === "usage_limit_exceeded" ? error.message : null;
263
263
  }
264
264
  /**
265
- * この起動のroot rolloutで、最後のturnが終わっていて、次のturnが始まっていないか。
266
- * Codexはturnの開始でtask_started、終わりでtask_complete、Escで止めた時はturn_aborted(reasonはinterrupted)を書く
267
- * (0.160.1の実記録)。末尾から見て最初の境界が、task_completeか、止めた時のturn_abortedの時だけtrue。
268
- * rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はfalse(分からない時は「終わった」と数えない)。
265
+ * この起動のroot rolloutの、末尾から見て最初のturnの境界。
266
+ * Codexはturnの開始でtask_started、終わりでtask_complete、止めた時はturn_aborted(reasonはinterrupted)を書く(0.160.1の実記録)。
267
+ * rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はnull。
269
268
  */
270
- export function codexTurnSettled(meta) {
269
+ function codexLatestTurnBoundary(meta) {
271
270
  let transcript;
272
271
  try {
273
272
  transcript = codexRootTranscript(meta);
274
273
  }
275
274
  catch {
276
- return false;
275
+ return null;
277
276
  }
278
277
  if (!transcript)
279
- return false;
278
+ return null;
280
279
  const lines = readTailLinesNewestFirst(transcript, AGENT_TURN_BOUNDARY_TAIL_BYTES);
281
280
  if (!lines)
282
- return false;
281
+ return null;
283
282
  for (const line of lines) {
284
283
  if (!line.includes("task_started") && !line.includes("task_complete") && !line.includes("turn_aborted"))
285
284
  continue;
@@ -288,19 +287,36 @@ export function codexTurnSettled(meta) {
288
287
  record = JSON.parse(line);
289
288
  }
290
289
  catch {
291
- return false;
290
+ return null;
292
291
  }
293
292
  if (record?.type !== "event_msg")
294
293
  continue;
295
- if (record?.payload?.type === "task_started")
296
- return false;
297
- if (record?.payload?.type === "task_complete")
298
- return typeof record.payload.turn_id === "string" && record.payload.turn_id !== "";
299
- // 見たのはEscで止めた時の形だけ。他の理由(別のturnへの置き換え等)は、次のturnが続くかも知れないので数えない。
300
- if (record?.payload?.type === "turn_aborted")
301
- return record.payload.reason === "interrupted";
294
+ const type = record?.payload?.type;
295
+ if (type === "task_started" || type === "task_complete" || type === "turn_aborted")
296
+ return { type, turn_id: record.payload.turn_id, reason: record.payload.reason };
302
297
  }
303
- return false;
298
+ return null;
299
+ }
300
+ /**
301
+ * この起動のroot rolloutで、最後のturnが終わっていて、次のturnが始まっていないか。
302
+ * 末尾から見て最初の境界が、task_completeか、止めた時のturn_abortedの時だけtrue。
303
+ * rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はfalse(分からない時は「終わった」と数えない)。
304
+ */
305
+ export function codexTurnSettled(meta) {
306
+ const boundary = codexLatestTurnBoundary(meta);
307
+ if (boundary?.type === "task_complete")
308
+ return typeof boundary.turn_id === "string" && boundary.turn_id !== "";
309
+ // 見たのはEscで止めた時の形だけ。他の理由(別のturnへの置き換え等)は、次のturnが続くかも知れないので数えない。
310
+ return boundary?.type === "turn_aborted" && boundary.reason === "interrupted";
311
+ }
312
+ /**
313
+ * この起動のroot rolloutの上で動いているturnのID。末尾から見て最初の境界がtask_startedの時だけ返す。
314
+ * rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はnull(分からない時は「動いている」と数えない)。
315
+ * Codexは、processが生きている間、turnの終わりを必ず書く。書かずに終わるのは落ちた時なので、呼ぶ側はprocessの生死も見る。
316
+ */
317
+ export function codexRunningTurn(meta) {
318
+ const boundary = codexLatestTurnBoundary(meta);
319
+ return boundary?.type === "task_started" && typeof boundary.turn_id === "string" && boundary.turn_id !== "" ? boundary.turn_id : null;
304
320
  }
305
321
  export function latestCodexCompletion(meta, readTranscriptLines) {
306
322
  const transcript = codexRootTranscript(meta);
package/docs/DESIGN.md CHANGED
@@ -62,6 +62,14 @@ agentの席への`pty_send`は、任意の`preface`(本文の前に置く1行
62
62
  ほかのharnessの席へは1つにつないで貼る。`preface`は入力欄がそのまま読むので、1行・200字以内・制御文字なし・行頭が記号や空白でない・
63
63
  `@`を含まない文だけを通し、外れた時は打鍵前に断る。
64
64
 
65
+ Codexは回答を流している間、画面に動作中の行を出さない。`pty_send`の振り分けは、Codexの席に限り、画面が入力待ちでも、この起動の記録の
66
+ 最後の境界がturnの開始で、processが生きていて、0.4秒おいて見直しても記録が同じturnのままで画面が動いている時を、動いていると数える
67
+ (ADR 0101)。止まっている席を動いていると読まないよう、1つでも欠けたら新しいturnの道へ回す。
68
+
69
+ Windowsの`pty_close`は、Claude Codeの席を止める前に、C-cを間を空けずに2回送って、Claude Codeが自分で終わるのを待つ(ADR 0100)。
70
+ psmuxは切断の合図なしでsessionを止めるので、そのままでは会話終了のhook(SessionEnd)が走らない。動いている番の席へは、先にC-cを
71
+ 1回送って入力待ちに戻るのを待つ。待ちは同期で、上限つき。終わらなくても、その後にsessionを止める。POSIXとほかのharnessの席は変えない。
72
+
65
73
  `pty_observe`は存在、pane/harnessの生存、画面状態と理由、native process identityを分ける。
66
74
  PIDは開始識別子・argv digestと組にし、paneとharnessを同一視しない。特定できないidentityはnull。
67
75
  同じlaunchに属するnpm shimとnative本体は、中間の非候補processも含めた祖先関係から一つの起動として扱う。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.57.0",
3
+ "version": "0.57.1",
4
4
  "mcpName": "io.github.kitepon/aiterm-mcp",
5
5
  "description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
6
6
  "keywords": [
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "@modelcontextprotocol/sdk": "^1.29.0",
75
- "aiterm-steer-delivery": "^0.3.0",
75
+ "aiterm-steer-delivery": "^0.3.1",
76
76
  "ws": "8.21.3",
77
77
  "zod": "^4.4.3"
78
78
  },