aiterm-mcp 0.15.1 → 0.17.0

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/README.ja.md CHANGED
@@ -24,10 +24,13 @@
24
24
 
25
25
  12 ツール: 6 つの **PTY ツール**(`pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list`)で 1 本の永続端末を開き・操作し・読む。加えて 4 つの **エージェント起動ツール**(`claude_agent` / `codex_agent` / `grok_agent` / `composer_agent`)が別のコーディングエージェントの TUI を新しい端末の中に起動し、`claude_turn`がdurable caller向けの構造化issue/recoveryを、`diagnostics`が安全なfactory readinessを返す。バックエンドは **tmux** なので、MCP サーバや AI クライアントが再起動してもセッションは生き残る。
26
26
 
27
- **v0.12.2 は release candidate(公開待ち)です。** factory diagnostics と local
27
+ **v0.17.0 を 2026-07-18 に公開。** 親エージェントは aiterm 上で一切ブロックしない:
28
+ agent session への send は常に非ブロック dispatch になり、完了待ちは `aiterm-wait` 一本
29
+ (exit code が receipt の outcome を映す: 0=done / 3=timeout=未完了 / 4=closed)、初回 prompt 付き
30
+ launch は structured receipt にコピペ可能な `wait_command` を含む。factory diagnostics と local
28
31
  runtime-error store は canonical dotagents config の `collection.enabled: true` が明示された
29
- 場合だけ収集し、既定OFF、network送信は行いません。npm公開、tag、CI、registry登録、registry由来install
30
- の確認は未実施です。
32
+ 場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
33
+ Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
31
34
 
32
35
  **状態:** 開発継続中 · この分野では新参で、別の形に賭けている([既存手段との比較](#既存手段との比較)参照)· 動作対象は Linux · WSL2 · macOS · Windows ネイティブ(core PTY ツール。`agent_done` は現時点では POSIX/WSL/macOS のみ)· MIT · [変更履歴](CHANGELOG.md)。
33
36
 
@@ -52,14 +55,15 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
52
55
 
53
56
  ### 2. その端末の中に他のコーディングエージェントを起動する — オーケストレーションの旗艦
54
57
 
55
- 同じ primitive が別エージェントの TUI を宿す。4 つの起動ツールが、Claude/Codex/Grok/Composer の対話 TUI を新しい永続端末の中に起動し、`session_id` を返す。既存の人間向けtextに加えて`aiterm.agent-launch-result.v1` structured receiptも返すため、durable callerは表示文字列を解析せずsession handleを取得できる。以後は同じ `pty_read` / `pty_send` で継続操作する。`agent_done:true` なら Stop hook でターン完了を待てる。managed Claudeでは全turnに`wait:"agent_done"`を必須とする。durable machine callerは`claude_turn({ action:"issue"|"recover", session_id, operation_id, ... })`を使い、人間向けerror文字列を解析せず`accepted`/`pending`/`completed`/`unknown`を判定できる。recoveryは再送せず、検証済み完了だけがexact `raw_output`を持つ。通常の`pty_send`/`pty_read`は対話callerと人間向けに維持する。`C-c`後もmarkerを保持し、Stopが来なければsessionをcloseする。`claude_agent` と `codex_agent` は、TUI ready 後に初回 `prompt` を送り `wait:"agent_done"` で待つ入口も公開する。Codex/Grok/Composer の live smoke は成功済み。Claude実モデルの初回/follow-up smokeは明示承認待ちであり、まだ成功扱いしない。
58
+ 同じ primitive が別エージェントの TUI を宿す。4 つの起動ツールが、Claude/Codex/Grok/Composer の対話 TUI を新しい永続端末の中に起動し、`session_id` を返す。既存の人間向けtextに加えて`aiterm.agent-launch-result.v1` structured receiptも返すため、durable callerは表示文字列を解析せずsession handleを取得できる。以後は同じ `pty_read` / `pty_send` で継続操作する。**起動は常に managed**(aiterm 所有の Stop hook 付き)で、agent session への `pty_send` は非ブロックの **dispatch** になり `event_cursor` 入り receipt を即返す。完了通知は `aiterm-wait --session <id> --cursor <event_cursor>` をホストのバックグラウンドタスクとして実行し、exit 時に receipt の `outcome` で判定する(exit 0=done / 3=timeout=未完了・既定600秒 / 4=closed。親はブロックもポーリングもしない)。起動時 `prompt` を渡した launch は structured receipt にコピペ可能な `wait_command` と `event_cursor` を含む。durable machine callerは`claude_turn({ action:"issue"|"recover", session_id, operation_id, ... })`を使い、人間向けerror文字列を解析せず`accepted`/`pending`/`completed`/`unknown`を判定できる。recoveryは再送せず、検証済み完了だけがexact `raw_output`を持つ。通常の`pty_send`/`pty_read`は対話callerと人間向けに維持する。`C-c`後もmarkerを保持し、Stopが来なければsessionをcloseする。`claude_agent` と `codex_agent` の初回 `prompt` は ready gate 経由で送信して待たずに返る(Grok/Composer は argv 渡し)。手動でキー操作したい場合は `pty_open` で素の端末を開き vendor CLI を自分で起動する。
56
59
 
57
60
  ```text
58
- codex_agent({ session_name: "codex1", cwd: "/repo", agent_done: true,
59
- wait: "agent_done", prompt: "port test/legacy.py to vitest" })
61
+ codex_agent({ session_name: "codex1", cwd: "/repo",
62
+ prompt: "port test/legacy.py to vitest" })
60
63
  → { session_id: "codex1", … } # Codex が永続端末で稼働開始
61
64
  pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
62
- pty_send("codex1", "also fix the imports it broke", { wait: "agent_done" })
65
+ pty_send("codex1", "also fix the imports it broke") # 非ブロックdispatch=event_cursor入りreceipt
66
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeout(未完了) / 4=closed。回収は pty_read(agent_transcript:true)
63
67
  → 操舵し、Codex の次の入力境界で返る
64
68
  ```
65
69
 
@@ -67,12 +71,12 @@ pty_send("codex1", "also fix the imports it broke", { wait: "agent_done" })
67
71
 
68
72
  | ツール | 起動するもの | 主な引数 |
69
73
  | --- | --- | --- |
70
- | `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `agent_done?`, `wait?`, `timeout?`, `screen?`, `lines?` |
71
- | `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?`, `agent_done?`, `wait?`, `timeout?`, `screen?`, `lines?` |
72
- | `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?`, `agent_done?` |
73
- | `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?`, `agent_done?` |
74
+ | `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
75
+ | `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?` |
76
+ | `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
77
+ | `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
74
78
 
75
- 各ベンダーの CLI が導入・認証済みであること(`claude_agent` は `claude`、`codex_agent` は `codex`、Grok 系は `grok`)。バイナリは `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、各既定path、`PATH` の順で解決する。CLI不在・不正なmodel/effort・実在しない`cwd`はsession作成前に失敗し、残骸を残さない。Claudeの`agent_done:true`は通常settingsを継承しないlaunch専用settingsとStop hookを使い、本文なしeventとowner-only bounded resultを分離する。`pty_read({ agent_transcript:true })`はdigestとbyte数を検証したresultだけを返し、Claude private transcriptを読まない。timeoutは成功扱いせず同じsessionを残し、後着resultをprompt再送なしで回収できる。managed Claudeでは通常`pty_send(wait:"none")`とC-c以外の`pty_key`を拒否する。自由なkey操作が必要なら`agent_done:false`で起動し、managed routeでは中断に`C-c`、解除に`pty_close`を使う。
79
+ 各ベンダーの CLI が導入・認証済みであること(`claude_agent` は `claude`、`codex_agent` は `codex`、Grok 系は `grok`)。バイナリは `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、各既定path、`PATH` の順で解決する。CLI不在・不正なmodel/effort・実在しない`cwd`はsession作成前に失敗し、残骸を残さない。Claudeは通常settingsを継承しないlaunch専用settingsとStop hookを使い、本文なしeventとowner-only bounded resultを分離する。`pty_read({ agent_transcript:true })`はdigestとbyte数を検証したresultだけを返し、Claude private transcriptを読まない。後着resultは同じsessionからprompt再送なしで回収できる。managed ClaudeではC-c以外の`pty_key`を拒否する(手動介入は`pty_send`の`force:true`)。中断は`C-c`、解除は`pty_close`。
76
80
 
77
81
  エージェント間の隠れたプロトコルは無い。起動したClaude/Codex/Grok/Composerは利用者がattachできるもう1本の永続sessionであり、MCPクライアントが通常のPTY操作で駆動する。
78
82
 
@@ -257,7 +261,7 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
257
261
  | `pty_key` | 制御キーを送る | `session_id`, `key`(`C-c`/`Enter`/`Up`…) |
258
262
  | `pty_close` | 冪等に閉じ、`closed` / `already_closed`を返す | `session_id` |
259
263
  | `pty_list` | セッション一覧 | (なし) |
260
- | `claude_turn` | 相関済みmanaged Claude operationを送信または回収 | `action`, `session_id`, `operation_id`, `text?`, `timeout?` |
264
+ | `claude_turn` | 相関済みmanaged Claude operationをdispatch(issue)または回収(recover) | `action`, `session_id`, `operation_id`, `text?` |
261
265
  | `diagnostics` | 機械可読 JSON による read-only factory readiness | (なし) |
262
266
 
263
267
  `diagnostics` は PTY やエージェントを起動しない。パッケージ版、MCP 呼出 readiness、read-only な PTY 一覧要約、bounded runtime-error-store status、任意 vendor launcher の可用性だけを返す。path・環境値・認証情報・コマンド本文・PTY 出力・raw log は意図的に返さない。通常未設定の任意依存は `not_applicable`、安全に確定できない状態は `unverified` と表す。
@@ -274,18 +278,18 @@ consumer は `aiterm-runtime-errors snapshot` を読み、durable ingestion 後
274
278
 
275
279
  | ツール | 起動するもの | 主な引数 |
276
280
  | --- | --- | --- |
277
- | `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `agent_done?`, `wait?`, `timeout?`, `screen?`, `lines?` |
278
- | `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?`, `agent_done?`, `wait?`, `timeout?`, `screen?`, `lines?` |
279
- | `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?`, `agent_done?` |
280
- | `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?`, `agent_done?` |
281
+ | `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
282
+ | `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?` |
283
+ | `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
284
+ | `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
281
285
 
282
- 対応するCLI(`claude` / `codex` / `grok`)の導入・認証が必要。解決順は`CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、既定path、`PATH`。前提違反はsession作成前に明示失敗する。Claude/Codexは初回prompt完了待ちを公開し、4 launcherすべてが対応環境で同じfollow-up `pty_send(wait:"agent_done")`契約を使う。Claudeはisolated managed settingsとhook-captured resultを使い、private transcriptへ依存しない。既存3 vendorのlive smokeはgreen、Claude実モデルsmokeは承認待ちでありfixture成功と混同しない。
286
+ 対応するCLI(`claude` / `codex` / `grok`)の導入・認証が必要。解決順は`CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、既定path、`PATH`。前提違反はsession作成前に明示失敗する。4 launcherすべてが同じ非ブロックdispatch契約を使い、Claude/Codexの初回promptはready gate経由で送信される。Claudeはisolated managed settingsとhook-captured resultを使い、private transcriptへ依存しない。既存3 vendorのlive smokeはgreen、Claude実モデルsmokeは承認待ちでありfixture成功と混同しない。
283
287
 
284
- エージェントの回答が `wait:"agent_done"` の画面 tailより長ければ、対話callerは`pty_read({ agent_transcript:true })`で再promptなしに全文回収する。Claudeはmanaged Stop hookがowner-only resultへ保存した本文をdigest/byte数で検証して返し、private transcriptを読まない。durable machine callerは`claude_turn`を使う。`issue`は一度だけ送信し、`recover`は決して再送せず、`pending`を破損やidentity不一致と区別する。検証済みの`completed`だけがexact `raw_output`を持ち、`unknown`は未dispatchと帰属不能を区別する。不一致・破損は成功statusへ丸めずtool errorのままにする。IDなしの対話turnも匿名markerで直列化するため、現在Stop待ちの間に古い回答を返さない。CodexはStop hookの`turn_id`で構造化transcriptへjoinし、Grok/Composerは最後の実user行より後ろのassistant行を採る。不在・非agent・抽出不能は明示エラー。
288
+ エージェントの回答が画面 tailより長ければ、対話callerは`pty_read({ agent_transcript:true })`で再promptなしに全文回収する。Claudeはmanaged Stop hookがowner-only resultへ保存した本文をdigest/byte数で検証して返し、private transcriptを読まない。durable machine callerは`claude_turn`を使う。`issue`は一度だけ送信し、`recover`は決して再送せず、`pending`を破損やidentity不一致と区別する。検証済みの`completed`だけがexact `raw_output`を持ち、`unknown`は未dispatchと帰属不能を区別する。不一致・破損は成功statusへ丸めずtool errorのままにする。IDなしの対話turnも匿名markerで直列化するため、現在Stop待ちの間に古い回答を返さない。CodexはStop hookの`turn_id`で構造化transcriptへjoinし、Grok/Composerは最後の実user行より後ろのassistant行を採る。不在・非agent・抽出不能は明示エラー。
285
289
 
286
290
  ### 完了検出(5 層)
287
291
 
288
- `pty_read({ wait: true })` は、プロセス終了 / `mark:true` sentinel の自動検出(後述)/ `until` 一致(**既定はリテラル部分一致**、`until_regex: true` で正規表現)/ 出力静止 ∧ シェル復帰(quiescence)/ timeout の 5 層で「コマンドが終わったか」を判定する。ネスト中(SSH・コンテナ・REPL・起動したエージェントの TUI の中)はシェル復帰判定が効かないので、`until` で内側プロンプトを指定するか、`mark: true` で送れば `pty_read({ wait: true })` が sentinel を自動検出する(until 不要・ネストでも効く)——全画面のエージェント TUI なら、出力が落ち着いた時点で `{ screen: true }` を読む。`agent_done:true` で起動したセッションは代わりに `pty_send({ wait:"agent_done" })` を使える。必要な場合はまず agent TUI の入力欄を待ち、その後 vendor Stop hook を待ってターン境界後の画面を返す。`codex_agent` launcher の `wait:"agent_done"` は起動時 `prompt` に同じ待機を行う。`pty_send` の送信前 ready 失敗は MCP エラー、launcher の初回 prompt ready 失敗は `initial_prompt=not_sent` を返す。送信後 timeout は `is_complete=False via agent_timeout` で返り、成功扱いしない。agent session の通常 `pty_read` には `agent_event_seen=true completion_attribution=none` のような補助 metadata が付くことがあるが、古い hook event を `is_complete=True` に昇格しない。完結した hook JSONL 行が壊れていた場合は、切り分け用に timeout suffix へ `malformed_events=N` が付く。ターンは完了したが端末 screen/log が flush 窓内で安定しなかった場合は、`agent_done_but_screen_unstable` が付く。
292
+ `pty_read({ wait: true })` は、プロセス終了 / `mark:true` sentinel の自動検出(後述)/ `until` 一致(**既定はリテラル部分一致**、`until_regex: true` で正規表現)/ 出力静止 ∧ シェル復帰(quiescence)/ timeout の 5 層で「コマンドが終わったか」を判定する。ネスト中(SSH・コンテナ・REPL・起動したエージェントの TUI の中)はシェル復帰判定が効かないので、`until` で内側プロンプトを指定するか、`mark: true` で送れば `pty_read({ wait: true })` が sentinel を自動検出する(until 不要・ネストでも効く)——全画面のエージェント TUI なら、出力が落ち着いた時点で `{ screen: true }` を読む。agent session は第6の正確な層を使う: vendor Stop hook が完了 event を書き、`pty_send` dispatch が返した `event_cursor` 境界から `aiterm-wait --cursor` が完了を観測する(親はブロックもポーリングもしない)。`pty_send` の送信前 ready 失敗は MCP エラー、launcher の初回 prompt ready 失敗は `initial_prompt=not_sent` を返す。agent session の通常 `pty_read` には `agent_event_seen=true completion_attribution=none` のような補助 metadata が付くことがあるが、古い hook event を `is_complete=True` に昇格しない。完結した hook JSONL 行が壊れていた場合は、`aiterm-wait` receipt の `malformed_events` に数えられる。ターンは完了したが端末 screen/log が flush 窓内で安定しなかった場合は、`agent_done_but_screen_unstable` が付く。
289
293
 
290
294
  ### トークン削減
291
295
 
package/README.md CHANGED
@@ -24,16 +24,18 @@
24
24
 
25
25
  Twelve tools: six **PTY tools** — `pty_open` / `pty_send` / `pty_read` / `pty_key` / `pty_close` / `pty_list` — to open, drive, and read one persistent terminal, four **agent launchers** — `claude_agent` / `codex_agent` / `grok_agent` / `composer_agent` — that each start another coding agent's TUI inside a fresh one, `claude_turn` for durable structured issue/recovery, and `diagnostics` for safe factory readiness. The backend is **tmux**, so sessions survive even if the MCP server or the AI client restarts.
26
26
 
27
- **v0.15.0 was published on 2026-07-18.** It brings the interactive agent
28
- launchers, durable `claude_turn` issue/recovery, machine-readable launch and
29
- idempotent close receipts, the hardened TUI readiness gate, and the new
30
- `aiterm-wait` completion-push binary to npm. Factory diagnostics and the local
31
- runtime-error store collect only when canonical dotagents config explicitly sets
32
- `collection.enabled: true`; collection is off by default and performs no network
33
- I/O. It ships via tag-triggered CI with npm provenance (OIDC Trusted Publishing);
34
- the GitHub Release re-registers the Official MCP Registry entry.
35
-
36
- **Status:** actively maintained · the newcomer here, betting on a different shape (see [vs. the alternatives](#vs-the-alternatives)) · runs on Linux · WSL2 · macOS · native Windows for the core PTY tools (`agent_done` is POSIX/WSL/macOS only for now) · MIT · see the [CHANGELOG](CHANGELOG.md).
27
+ **v0.17.0 was published on 2026-07-18.** A parent agent never blocks on aiterm:
28
+ every send to an agent session is a non-blocking dispatch, completion is one
29
+ universal `aiterm-wait` waiter whose exit codes mirror the receipt outcome
30
+ (`0`=done / `3`=timeout, not finished / `4`=closed), and a launch with an
31
+ initial prompt returns a ready-made `wait_command` in its structured receipt.
32
+ Factory diagnostics and the local runtime-error store collect only when
33
+ canonical dotagents config explicitly sets `collection.enabled: true`;
34
+ collection is off by default and performs no network I/O. It ships via
35
+ tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
36
+ Release re-registers the Official MCP Registry entry.
37
+
38
+ **Status:** actively maintained · the newcomer here, betting on a different shape (see [vs. the alternatives](#vs-the-alternatives)) · runs on Linux · WSL2 · macOS · native Windows for the core PTY tools (managed completion is POSIX/WSL/macOS only for now) · MIT · see the [CHANGELOG](CHANGELOG.md).
37
39
 
38
40
  ## Why now
39
41
 
@@ -62,27 +64,29 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
62
64
 
63
65
  ### 2. Launch other coding agents into that terminal — the orchestration flagship
64
66
 
65
- The same primitive hosts another agent's TUI. Four launchers each start one vendor's interactive coding-agent TUI inside a fresh persistent terminal and return a `session_id`. Their existing human-readable text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt, so durable callers never parse display text for the session handle. From there you drive it with the same `pty_read` / `pty_send` you'd use on any shell: read its output token-reduced, send it the next step. (The TUIs are full-screen apps, so `pty_read({ screen: true })` gives you the rendered view.) Agent launchers can also opt into hook-backed turn completion with `agent_done: true`, letting `pty_send({ wait: "agent_done" })` return after the agent turn ends. A managed Claude session requires that wait mode for every turn. Durable machine callers use `claude_turn({ action: "issue" | "recover", session_id, operation_id, ... })`: it returns fixed `accepted` / `pending` / `completed` / `unknown` states without parsing human-facing errors, never resends during recovery, and includes exact `raw_output` only for a verified completion. The same operation ID is carried through the dispatch receipt, active marker, Stop event, and result. The ordinary `pty_send` / `pty_read` surface remains available for interactive callers and humans. `C-c` keeps the marker for a delayed Stop; if no Stop arrives, close the session. `claude_agent` and `codex_agent` can additionally wait for their initial `prompt`: pass `prompt`, `agent_done: true`, and `wait: "agent_done"`; aiterm starts the TUI first, waits until its input area is ready, submits the prompt, then returns after that first turn's Stop hook. Codex/Grok/Composer have passing live smokes for follow-up completion, and Codex initial-prompt wait is also live-smoked. Claude's real-model initial/follow-up smoke remains an explicit approval gate. Grok/Composer initial-prompt wait is intentionally not exposed until the post-OAuth smoke passes. This needs the vendor's own CLI installed and authenticated — see [Requirements](#requirements).
67
+ The same primitive hosts another agent's TUI. Four launchers each start one vendor's interactive coding-agent TUI inside a fresh persistent terminal and return a `session_id`. Their existing human-readable text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt, so durable callers never parse display text for the session handle; when the launch carries an initial `prompt`, the receipt also includes the `event_cursor` and a ready-made `wait_command` for the completion waiter. From there you drive it with the same `pty_read` / `pty_send` you'd use on any shell: read its output token-reduced, send it the next step. (The TUIs are full-screen apps, so `pty_read({ screen: true })` gives you the rendered view.) Every launch is **managed**: aiterm installs its own Stop hook, so turn completion is a first-class event. Sending to an agent session is a non-blocking **dispatch** — the call returns immediately with an `event_cursor`, and completion arrives via [`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait). Durable machine callers use `claude_turn({ action: "issue" | "recover", session_id, operation_id, ... })`: it returns fixed `accepted` / `pending` / `completed` / `unknown` states without parsing human-facing errors, never resends during recovery, and includes exact `raw_output` only for a verified completion. The same operation ID is carried through the dispatch receipt, active marker, Stop event, and result. The ordinary `pty_send` / `pty_read` surface remains available for interactive callers and humans. `C-c` keeps the marker for a delayed Stop; if no Stop arrives, close the session. An initial `prompt` on `claude_agent`/`codex_agent` is submitted through the same ready gate and the launcher returns without waiting; on Grok/Composer it is passed on the CLI's argv. This needs the vendor's own CLI installed and authenticated — see [Requirements](#requirements).
66
68
 
67
69
  ```text
68
- codex_agent({ session_name: "codex1", cwd: "/repo", agent_done: true,
69
- wait: "agent_done", prompt: "port test/legacy.py to vitest" })
70
+ codex_agent({ session_name: "codex1", cwd: "/repo",
71
+ prompt: "port test/legacy.py to vitest" })
70
72
  → { session_id: "codex1", … } # Codex now live in a persistent terminal
71
73
  pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
72
- pty_send("codex1", "also fix the imports it broke", { wait: "agent_done" })
73
- → steer it, then return once Codex reaches its next turn boundary
74
+ pty_send("codex1", "also fix the imports it broke")
75
+ → non-blocking dispatch; receipt carries event_cursor
76
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # host background task; exit code 0=done, 3=timeout (not done), 4=closed
77
+ pty_read("codex1", { agent_transcript: true }) → collect the full answer
74
78
  ```
75
79
 
76
80
  One call per model, so the tool name itself tells you which model you get:
77
81
 
78
82
  | Tool | Launches | Key args |
79
83
  | --- | --- | --- |
80
- | `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `agent_done?`, `launch_operation_id?`, `wait?`, `timeout?`, `screen?`, `lines?` |
81
- | `codex_agent` | Codex CLI (OpenAI; terminal config/CLI default unless overridden) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`/`ultra`; ultra enables proactive automatic delegation), `cwd?`, `session_name?`, `agent_done?`, `wait?`, `timeout?`, `screen?`, `lines?` |
82
- | `grok_agent` | Grok Build, model `grok-4.5` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error; Grok CLI `--effort` is headless-only), `cwd?`, `session_name?`, `agent_done?` |
83
- | `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error), `cwd?`, `session_name?`, `agent_done?` |
84
+ | `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `launch_operation_id?` |
85
+ | `codex_agent` | Codex CLI (OpenAI; terminal config/CLI default unless overridden) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`/`ultra`; ultra enables proactive automatic delegation), `cwd?`, `session_name?` |
86
+ | `grok_agent` | Grok Build, model `grok-4.5` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error; Grok CLI `--effort` is headless-only), `cwd?`, `session_name?` |
87
+ | `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error), `cwd?`, `session_name?` |
84
88
 
85
- The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/claude` / `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are checked **before** a session exists: empty `model` values and unsupported effort values are rejected up front; a missing CLI binary or a nonexistent `cwd` fails for all four. A rejected launch leaves **zero leftover session** behind. Claude and Codex launchers forward `model` and `reasoning_effort` through their vendor CLI's public flags; Grok/Composer reject `reasoning_effort` because it is headless-only. Pass an absolute path for `cwd` — `~` is not expanded. Durable callers can make a promptless managed Claude launch exactly replayable by passing an explicit `session_name`, `agent_done:true`, and a `launch_operation_id` formatted as `sha256:<64 lowercase hex>`. Repeating the identical launch returns the same structured session receipt without starting the CLI twice; a different correlation ID or launch argument for that session fails explicitly. With `agent_done:true`, Claude uses launch-local managed settings containing only aiterm's Stop hook: normal user/project/local hooks are not inherited, the hook event contains no answer body, and the bounded owner-only result is returned by `pty_read({ agent_transcript:true })` without reading Claude's private transcript. A timeout remains `is_complete=False`; the same session and a late result remain recoverable without re-sending the prompt. Ordinary `pty_send(wait:"none")` and non-interrupt keys are rejected on this managed Claude route; use `wait:"agent_done"`, `pty_key("C-c")`, and `pty_close`. Start with `agent_done:false` when unconstrained manual key-by-key driving is desired. Codex uses a managed `CODEX_HOME`; Grok/Composer isolate their managed homes and pass validated OAuth state through `GROK_AUTH_PATH`. Before the first unbound completion-waiting send, aiterm waits for the vendor TUI's input prompt and fails before sending if it is not ready. `agent_done` requires POSIX filesystem semantics, so it is supported on Linux, WSL2, and macOS; native Windows can still use launchers without `agent_done`.
89
+ The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). aiterm resolves the binary via `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then `~/.local/bin/claude` / `~/.local/bin/codex` / `~/.grok/bin/grok`, then `PATH`. Prerequisites are checked **before** a session exists: empty `model` values and unsupported effort values are rejected up front; a missing CLI binary or a nonexistent `cwd` fails for all four. A rejected launch leaves **zero leftover session** behind. Claude and Codex launchers forward `model` and `reasoning_effort` through their vendor CLI's public flags; Grok/Composer reject `reasoning_effort` because it is headless-only. Pass an absolute path for `cwd` — `~` is not expanded. Durable callers can make a promptless Claude launch exactly replayable by passing an explicit `session_name` and a `launch_operation_id` formatted as `sha256:<64 lowercase hex>`. Repeating the identical launch returns the same structured session receipt without starting the CLI twice; a different correlation ID or launch argument for that session fails explicitly. Claude uses launch-local managed settings containing only aiterm's Stop hook: normal user/project/local hooks are not inherited, the hook event contains no answer body, and the bounded owner-only result is returned by `pty_read({ agent_transcript:true })` without reading Claude's private transcript. A late result remains recoverable from the same session without re-sending the prompt. Non-interrupt keys are rejected on the managed Claude route; use dispatch, `pty_key("C-c")`, and `pty_close` (`force:true` on `pty_send` is the manual-intervention escape). For unconstrained manual key-by-key driving, open a plain `pty_open` session and start the vendor CLI yourself. Codex uses a managed `CODEX_HOME`; Grok/Composer isolate their managed homes and pass validated OAuth state through `GROK_AUTH_PATH`. Before the first unbound dispatch, aiterm waits for the vendor TUI's input prompt and fails before sending if it is not ready. Managed completion requires POSIX filesystem semantics (Linux, WSL2, macOS).
86
90
 
87
91
  There is no hidden protocol between agents: a launched Claude, Codex, Grok, or Composer is another user-visible persistent terminal session. The MCP client drives that TUI with ordinary PTY operations, and a human can attach to watch or take over.
88
92
 
@@ -264,12 +268,12 @@ On top of that sits a productized layer a raw tmux bridge doesn't have: **token-
264
268
  | Tool | Role | Key args |
265
269
  | --- | --- | --- |
266
270
  | `pty_open` | Grab one terminal, return a `session_id` | `name?`, `shell="bash"` |
267
- | `pty_send` | Send text (a command) | `session_id`, `text`, `enter=true`, `wait`, `timeout`, `screen`, `lines`, `operation_id`, `mark`, `force`, `rtk`, `raw` |
271
+ | `pty_send` | Send text; on an agent session this is a non-blocking **dispatch** returning an `event_cursor` | `session_id`, `text`, `enter=true`, `mark`, `force`, `rtk`, `raw` |
268
272
  | `pty_read` | Read output, token-reduced (incremental by default) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk`, `agent_transcript`, `operation_id` |
269
273
  | `pty_key` | Send a control key | `session_id`, `key` (`C-c`/`Enter`/`Up`…) |
270
274
  | `pty_close` | Close idempotently; return `closed` / `already_closed` | `session_id` |
271
275
  | `pty_list` | List sessions (agent rows carry `agent=<kind>` metadata) | (none) |
272
- | `claude_turn` | Issue or recover one correlated managed-Claude operation | `action`, `session_id`, `operation_id`, `text?`, `timeout?` |
276
+ | `claude_turn` | Issue (dispatch-only) or recover one correlated managed-Claude operation | `action`, `session_id`, `operation_id`, `text?` |
273
277
  | `diagnostics` | Read-only factory readiness as machine-readable JSON | (none) |
274
278
 
275
279
  `diagnostics` never starts a PTY or agent. It reports package version, MCP call readiness, a read-only PTY-list summary, bounded runtime-error-store status, and optional vendor-launcher availability. It deliberately excludes paths, environment values, credentials, command text, PTY output, and raw logs; normal unset optional dependencies are `not_applicable`, while an indeterminate probe is `unverified`.
@@ -286,31 +290,29 @@ Each launcher starts a specific vendor's interactive coding-agent TUI inside a f
286
290
 
287
291
  | Tool | Launches | Key args |
288
292
  | --- | --- | --- |
289
- | `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `agent_done?`, `launch_operation_id?`, `wait?`, `timeout?`, `screen?`, `lines?` |
290
- | `codex_agent` | Codex CLI (OpenAI; terminal config/CLI default unless overridden) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`/`ultra`; ultra enables proactive automatic delegation), `cwd?`, `session_name?`, `agent_done?`, `wait?`, `timeout?`, `screen?`, `lines?` |
291
- | `grok_agent` | Grok Build, model `grok-4.5` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error; Grok CLI `--effort` is headless-only), `cwd?`, `session_name?`, `agent_done?` |
292
- | `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error), `cwd?`, `session_name?`, `agent_done?` |
293
+ | `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `launch_operation_id?` |
294
+ | `codex_agent` | Codex CLI (OpenAI; terminal config/CLI default unless overridden) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`/`ultra`; ultra enables proactive automatic delegation), `cwd?`, `session_name?` |
295
+ | `grok_agent` | Grok Build, model `grok-4.5` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error; Grok CLI `--effort` is headless-only), `cwd?`, `session_name?` |
296
+ | `composer_agent` | Grok Build, model `grok-composer-2.5-fast` by default (`model?` overrides) (xAI) | `prompt?`, `model?`, `reasoning_effort?` unsupported (an explicit value is an error), `cwd?`, `session_name?` |
293
297
 
294
- The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). Binary resolution uses `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then each documented default location, then `PATH`. Missing binaries, invalid model/effort values, and nonexistent `cwd` fail before a session is created. Claude and Codex expose initial-prompt completion waiting; all four use the same follow-up `pty_send(wait:"agent_done")` contract where supported. Claude uses isolated managed settings and a hook-captured bounded result rather than private transcript access. Codex/Grok/Composer live smokes are green; Claude real-model smoke remains approval-gated and is not claimed from fixtures. Native Windows can launch agents but `agent_done` is not supported yet.
298
+ The vendor CLI must be installed and authenticated (`claude` for `claude_agent`; `codex` for `codex_agent`; `grok` for both Grok tools). Binary resolution uses `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`, then each documented default location, then `PATH`. Missing binaries, invalid model/effort values, and nonexistent `cwd` fail before a session is created. All four share the same non-blocking dispatch contract for follow-up turns; `claude_agent`/`codex_agent` submit an initial `prompt` through the ready gate. Claude uses isolated managed settings and a hook-captured bounded result rather than private transcript access. Codex/Grok/Composer live smokes are green; Claude real-model smoke remains approval-gated and is not claimed from fixtures. Native Windows can launch agents but managed completion is not supported yet.
295
299
 
296
- When an agent's answer is longer than the `wait:"agent_done"` screen tail (pane height ≈ 24 lines), interactive callers can recover it in full with `pty_read({ agent_transcript: true })`. It returns the most recently completed turn's final assistant message in plain text with no re-prompting. Claude reads the bounded owner-only result captured by the managed Stop hook and verifies its digest/byte count; it never reads Claude's private transcript. Durable machine callers should use `claude_turn`: `issue` sends once, `recover` never sends, `pending` is distinct from unsafe or malformed state, and only `completed` carries the exact verified `raw_output`. `unknown` distinguishes `operation_not_found` from a receipt whose result can no longer be attributed. Mismatch and corruption remain tool errors rather than being folded into a successful status. ID-less interactive Claude turns are still serialized by an anonymous marker, so an older answer is not returned while the current Stop is pending. Codex joins its structured transcript on the Stop hook `turn_id`; Grok/Composer take the assistant rows after the last real user row. A missing result/transcript, a non-agent session, or an unextractable message is an explicit error, never a silent empty.
300
+ When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lines), callers recover it in full with `pty_read({ agent_transcript: true })`. It returns the most recently completed turn's final assistant message in plain text with no re-prompting. Claude reads the bounded owner-only result captured by the managed Stop hook and verifies its digest/byte count; it never reads Claude's private transcript. Durable machine callers should use `claude_turn`: `issue` sends once, `recover` never sends, `pending` is distinct from unsafe or malformed state, and only `completed` carries the exact verified `raw_output`. `unknown` distinguishes `operation_not_found` from a receipt whose result can no longer be attributed. Mismatch and corruption remain tool errors rather than being folded into a successful status. ID-less interactive Claude turns are still serialized by an anonymous marker, so an older answer is not returned while the current Stop is pending. Codex joins its structured transcript on the Stop hook `turn_id`; Grok/Composer take the assistant rows after the last real user row. A missing result/transcript, a non-agent session, or an unextractable message is an explicit error, never a silent empty.
297
301
 
298
302
  ### Completion detection (5 layers)
299
303
 
300
- `pty_read({ wait: true })` decides "is the command done?" via five layers: process exit / a `mark:true` sentinel (auto-detected — see below) / an `until` match (a literal substring by default; pass `until_regex: true` for a regex) / output is quiescent ∧ the shell is back (quiescence) / timeout. While nested (inside SSH, a container, a REPL, or a launched agent's TUI), the "shell is back" check cannot fire, so pass `until` with the inner prompt — or send with `mark: true` and `pty_read({ wait: true })` auto-detects the completion sentinel (no `until` needed, works nested too) — or, for a full-screen agent TUI, read `{ screen: true }` once its output settles. Sessions launched with `agent_done:true` can instead use `pty_send({ wait:"agent_done" })`, which first waits for the agent TUI input prompt when needed, then waits for the vendor Stop hook and returns the screen after the turn boundary; `claude_agent` and `codex_agent` launcher `wait:"agent_done"` do the same for the initial `prompt`. Pre-send readiness failures are MCP errors for `pty_send`, while launch-time initial prompt readiness failures return the session with `initial_prompt=not_sent`; timeouts after sending are reported as `is_complete=False via agent_timeout`, not as success. A late Claude completion remains recoverable from the same session with `pty_read({ agent_transcript:true })`, without resending. Normal `pty_read` on an agent session can append auxiliary metadata such as `agent_event_seen=true completion_attribution=none`, but a stale hook event is not promoted to `is_complete=True`. If a complete hook JSONL line is malformed, the timeout suffix includes `malformed_events=N` for diagnosis. If the turn is done but the terminal screen/log does not settle within the flush window, aiterm appends `agent_done_but_screen_unstable`.
304
+ `pty_read({ wait: true })` decides "is the command done?" via five layers: process exit / a `mark:true` sentinel (auto-detected — see below) / an `until` match (a literal substring by default; pass `until_regex: true` for a regex) / output is quiescent ∧ the shell is back (quiescence) / timeout. While nested (inside SSH, a container, a REPL, or a launched agent's TUI), the "shell is back" check cannot fire, so pass `until` with the inner prompt — or send with `mark: true` and `pty_read({ wait: true })` auto-detects the completion sentinel (no `until` needed, works nested too) — or, for a full-screen agent TUI, read `{ screen: true }` once its output settles. Agent sessions use the sixth, exact layer instead: the vendor Stop hook writes a completion event, `pty_send` dispatch returns the `event_cursor` boundary, and `aiterm-wait --cursor` observes the completion without the parent blocking or polling. Pre-send readiness failures are MCP errors for `pty_send`, while launch-time initial prompt readiness failures return the session with `initial_prompt=not_sent`. A late Claude completion remains recoverable from the same session with `pty_read({ agent_transcript:true })`, without resending. Normal `pty_read` on an agent session can append auxiliary metadata such as `agent_event_seen=true completion_attribution=none`, but a stale hook event is not promoted to `is_complete=True`. If a complete hook JSONL line is malformed, the `aiterm-wait` receipt counts it in `malformed_events` for diagnosis. If the turn is done but the terminal screen/log does not settle within the flush window, aiterm appends `agent_done_but_screen_unstable`.
301
305
 
302
306
  ### Completion push for parent agents (`aiterm-wait`)
303
307
 
304
- The `wait:"agent_done"` route blocks the caller's tool call. For fire-and-forget orchestration ("B-style": dispatch now, collect later, never poll), the `aiterm-wait` binary turns the Stop-hook completion event into a process exit that a host harness can treat as a push notification:
305
-
306
- 1. Launch the child agent (`agent_done: true`) and start `aiterm-wait --session <id> [--operation sha256:<64hex>] [--timeout <sec>]` as a **background task of the parent's harness**.
307
- 2. Dispatch the prompt with `claude_turn issue` (`timeout: 0`) or `pty_send(wait:"agent_done", timeout: 0)` — the TUI ready gate and submit sequencing still run; the call returns immediately.
308
- 3. The parent is free. When the child's turn ends, the vendor Stop hook appends the event, `aiterm-wait` exits with a one-line receipt (`aiterm.agent-wait-result.v1`, outcome `done` / `timeout` / `closed`), and a harness that re-invokes its agent on background-task exit (Claude Code does) wakes the parent with zero polling.
309
- 4. The parent collects the result exactly as before: `claude_turn recover` (Claude) or `pty_read` (other vendors). The waiter carries the signal, never the payload.
308
+ As of v0.16 a parent agent **never blocks** on aiterm — there is no wait parameter anywhere (v0.17 makes the waiter's exit codes mirror its outcome). The whole flow is dispatch + one universal waiter:
310
309
 
311
- `aiterm-wait` is a **pure reader**: it takes no locks, never writes session state, and never dispatches — so any number of waiters can run beside the MCP server and beside each other (one per launch), and `pty_close`/concurrent sends are unaffected. Start the waiter *before* dispatching (or pass `--operation`, which is start-order-independent) so no completion can slip past the observation boundary.
310
+ 1. Launch the child (`claude_agent` / `codex_agent` / ...; every launch is managed). Send a turn with plain `pty_send` (or `claude_turn issue` for durable Claude operations). The call passes the TUI ready gate, submits, and returns immediately with an `event_cursor` in its structured receipt.
311
+ 2. Run `aiterm-wait --session <id> --cursor <event_cursor> [--operation sha256:<64hex>] [--timeout <sec>]` (a launch with an initial `prompt` returns this command ready-made as `wait_command` in its structured receipt). It observes the vendor Stop-hook completion event as a **pure reader** and exits with a one-line `aiterm.agent-wait-result.v1` receipt. **Exit ≠ done**: the receipt's `outcome` is authoritative, and the exit code mirrors it — `0` = `done`, `3` = `timeout` (the turn is **not** finished; default `--timeout` is 600 s), `4` = `closed`, `1` = error. On `timeout` just re-run the waiter with the same cursor. The `--cursor` boundary makes it start-order independent: no completion can slip past even if the waiter starts late.
312
+ 3. Host integration picks the invocation style: a harness that re-invokes its agent when a background task exits (Claude Code) runs the waiter **in the background** and gets woken with zero polling; a host without that mechanism runs it as a foreground shell command. Either way the aiterm MCP call itself never blocks.
313
+ 4. Collect the result exactly as before: `pty_read(agent_transcript: true)`, or `claude_turn recover` for durable Claude operations. The waiter carries the signal, never the payload.
312
314
 
313
- Codex CLI as the *parent* currently has no equivalent "wake on background completion" hook (its `notify` config is human-facing and MCP notifications are not surfaced to the model — see openai/codex#17543 / #18056), so a Codex parent still uses the blocking wait or manual recovery until upstream support lands.
315
+ `aiterm-wait` takes no locks, never writes session state, and never dispatches — any number can run beside the MCP server and each other, and `pty_close`/concurrent sends are unaffected.
314
316
 
315
317
  ### Token reduction
316
318
 
@@ -380,7 +382,7 @@ MIT
380
382
 
381
383
  ## Grok OAuth isolation
382
384
 
383
- For `agent_done`, Grok/Composer keep launch-local `GROK_HOME` and fake `HOME`.
385
+ For managed completion, Grok/Composer keep launch-local `GROK_HOME` and fake `HOME`.
384
386
  The child receives `GROK_AUTH_PATH` pointing at the validated normal auth
385
387
  canonical file; managed homes never contain auth or lock symlinks/copies.
386
388
  aiterm does not create locks or copy credentials back. An inherited
@@ -1,20 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
  // aiterm-wait — agent turn 完了eventの純リーダー観測CLI。
3
3
  // 完了/timeout/close を1行のJSON receiptで返してexitする。lock・PTY・dispatch状態には一切触れない。
4
- // 親AIホストのバックグラウンドタスクとして起動し、exitを「完了通知」として使う。
4
+ // 親AIホストのバックグラウンドタスクとして起動し、exitを「観測終了の通知」として使う。
5
+ // exit≠完了: exit code は outcome を映す(0=done / 3=timeout=未完了 / 4=closed / 1=エラー)。
6
+ // receipt の outcome が正で、done 以外は未完了。timeout の既定は core の DEFAULT_AGENT_DONE_TIMEOUT(600秒)。
5
7
  import { fileURLToPath } from "node:url";
6
8
  import * as fs from "node:fs";
7
9
  import { AitermError, observeAgentDone } from "./core.js";
8
10
  const SESSION_RE = /^[A-Za-z0-9_-]{1,64}$/;
9
11
  const OPERATION_RE = /^sha256:[0-9a-f]{64}$/;
10
- const USAGE = "usage: aiterm-wait --session <name> [--operation sha256:<64hex>] [--timeout <sec>]";
12
+ const USAGE = "usage: aiterm-wait --session <name> [--cursor <event_cursor>] [--operation sha256:<64hex>] [--timeout <sec>]";
11
13
  export function parseArgs(argv) {
12
14
  let session = null;
13
15
  let operationId = null;
14
16
  let timeout = null;
17
+ let cursor = null;
15
18
  for (let i = 0; i < argv.length; i++) {
16
19
  const a = argv[i];
17
- if (a === "--session" || a === "--operation" || a === "--timeout") {
20
+ if (a === "--session" || a === "--operation" || a === "--timeout" || a === "--cursor") {
18
21
  const v = argv[i + 1];
19
22
  if (v === undefined)
20
23
  throw new Error(`${a} に値がありません。${USAGE}`);
@@ -33,6 +36,13 @@ export function parseArgs(argv) {
33
36
  throw new Error(`--operation が不正です。${USAGE}`);
34
37
  operationId = v;
35
38
  }
39
+ else if (a === "--cursor") {
40
+ if (cursor !== null)
41
+ throw new Error(`--cursor が重複しています。${USAGE}`);
42
+ if (!/^\d+$/.test(v))
43
+ throw new Error(`--cursor は0以上の整数byte offsetだけを受理します。${USAGE}`);
44
+ cursor = Number(v);
45
+ }
36
46
  else {
37
47
  if (timeout !== null)
38
48
  throw new Error(`--timeout が重複しています。${USAGE}`);
@@ -49,18 +59,22 @@ export function parseArgs(argv) {
49
59
  }
50
60
  if (session === null)
51
61
  throw new Error(`--session は必須です。${USAGE}`);
52
- return { session, operationId, timeout: timeout ?? 600 };
62
+ return { session, operationId, timeout: timeout ?? 600, cursor };
53
63
  }
54
64
  function emit(value) {
55
65
  process.stdout.write(JSON.stringify(value) + "\n");
56
66
  }
67
+ // outcome → exit code。exit status しか見えないホストでも done とそれ以外を誤読できないようにする。
68
+ const OUTCOME_EXIT_CODES = { done: 0, timeout: 3, closed: 4 };
57
69
  export async function main(argv) {
58
70
  const cmd = parseArgs(argv);
59
71
  const result = await observeAgentDone(cmd.session, {
60
72
  operation_id: cmd.operationId,
61
73
  timeout: cmd.timeout,
74
+ cursor: cmd.cursor,
62
75
  });
63
76
  emit(result);
77
+ process.exitCode = OUTCOME_EXIT_CODES[result.outcome];
64
78
  }
65
79
  function isDirectExecution() {
66
80
  const entry = process.argv[1];
package/dist/core.js CHANGED
@@ -1215,9 +1215,6 @@ export function readOnlyPtyListDiagnostic() {
1215
1215
  }
1216
1216
  function closeSessionInternal(name, observeDependency = true) {
1217
1217
  assertSessionName(name);
1218
- if (agentWaitLocks.has(name)) {
1219
- throw new AitermError(`agent session '${name}' は agent_done 待機中のため close できません`, 2);
1220
- }
1221
1218
  {
1222
1219
  // 別プロセスの待機は in-memory Set に映らない。生きた file lock があれば close で state を消さない
1223
1220
  const foreign = liveWaitLocks(name);
@@ -1266,9 +1263,6 @@ export function closeSessionResult(name) {
1266
1263
  };
1267
1264
  }
1268
1265
  export function killAll() {
1269
- if (agentWaitLocks.size > 0) {
1270
- throw new AitermError(`agent_done 待機中の session があるため killAll できません: ${Array.from(agentWaitLocks).join(",")}`, 2);
1271
- }
1272
1266
  {
1273
1267
  // 別プロセスの待機(file lock が生きているもの)も巻き添えにしない
1274
1268
  const foreign = liveWaitLocks(null);
@@ -1332,16 +1326,8 @@ export function killAll() {
1332
1326
  return "killed all sessions on this socket";
1333
1327
  }
1334
1328
  const DEFAULT_AGENT_DONE_TIMEOUT = 600;
1335
- const agentWaitLocks = new Set();
1336
1329
  const agentMetadataNegativeCache = new Map();
1337
1330
  let agentTuiReadyStableSamplesTestOverride = null;
1338
- function normalizeAgentLauncherWait(v) {
1339
- if (v == null || v === "none")
1340
- return "none";
1341
- if (v === "agent_done")
1342
- return "agent_done";
1343
- throw new AitermError('wait は "none" または "agent_done" を指定してください', 2);
1344
- }
1345
1331
  function codexHookScriptPath() {
1346
1332
  return path.join(path.dirname(fileURLToPath(import.meta.url)), "codex-stop-hook.js");
1347
1333
  }
@@ -2031,61 +2017,12 @@ function liveWaitLocks(name) {
2031
2017
  const session = f.slice(0, f.indexOf("."));
2032
2018
  if (name != null && session !== name)
2033
2019
  continue;
2034
- if (agentWaitLocks.has(session))
2035
- continue; // in-process 待機は呼び出し側の既存ガードが担当
2036
2020
  const probe = probeWaitLock(path.join(dir, f));
2037
2021
  if (probe.live)
2038
2022
  out.push({ session, pid: probe.pid, at: probe.at });
2039
2023
  }
2040
2024
  return out;
2041
2025
  }
2042
- function waitLockBusyError(session, probe) {
2043
- const detail = probe.pid != null ? `(pid ${probe.pid}${probe.at ? ` / ${probe.at} 開始` : ""})` : "";
2044
- return new AitermError(`agent session '${session}' は別プロセスの agent_done 待機中です${detail}`, 2);
2045
- }
2046
- function acquireAgentWaitFileLock(meta) {
2047
- const p = agentWaitLockPath(meta.aiterm_session, meta.launch_id);
2048
- const nofollow = fs.constants.O_NOFOLLOW ?? 0;
2049
- let fd = null;
2050
- for (let attempt = 0; attempt < 2 && fd == null; attempt++) {
2051
- try {
2052
- fd = fs.openSync(p, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | nofollow, 0o600);
2053
- }
2054
- catch (e) {
2055
- if (e.code !== "EEXIST")
2056
- throw e;
2057
- const probe = probeWaitLock(p);
2058
- // 生きた待機、または回収後の再取得でも EEXIST(=直後に別プロセスが取得した race)は拒否
2059
- if (probe.live || attempt > 0)
2060
- throw waitLockBusyError(meta.aiterm_session, probe);
2061
- unlinkStaleWaitLock(p);
2062
- }
2063
- }
2064
- if (fd == null)
2065
- throw new AitermError(`agent session '${meta.aiterm_session}' は別プロセスの agent_done 待機中です`, 2);
2066
- try {
2067
- fs.writeSync(fd, JSON.stringify({ pid: process.pid, at: new Date().toISOString() }) + "\n", undefined, "utf8");
2068
- }
2069
- finally {
2070
- fs.closeSync(fd);
2071
- }
2072
- try {
2073
- fs.chmodSync(p, 0o600);
2074
- }
2075
- catch {
2076
- /* noop */
2077
- }
2078
- return () => {
2079
- try {
2080
- const st = fs.lstatSync(p);
2081
- if (st.isFile() && !st.isSymbolicLink() && st.uid === currentUid())
2082
- fs.unlinkSync(p);
2083
- }
2084
- catch {
2085
- /* noop */
2086
- }
2087
- };
2088
- }
2089
2026
  function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId, launchRequestDigest) {
2090
2027
  const launchId = randomBytes(16).toString("hex");
2091
2028
  const eventFile = agentEventPath(name, launchId);
@@ -2489,8 +2426,11 @@ function findLatestCodexTranscript(codexHome, vendorSessionId) {
2489
2426
  visit(sessionsDir);
2490
2427
  return latestFile;
2491
2428
  }
2429
+ // 未完了系エラーの共通出口案内。pollingへ誘導せず、正規の完了待ち手段を必ず指す。
2430
+ const AGENT_WAIT_GUIDE = "完了待ちは aiterm-wait --session <session_id> のバックグラウンド実行で受ける(polling不要。" +
2431
+ "receiptのoutcome=doneを確認してから再取得)。";
2492
2432
  function transcriptUnavailable() {
2493
- throw new AitermError("transcript がまだありません。ターン完了後に再取得してください。", 2);
2433
+ throw new AitermError(`transcript がまだありません。ターン完了後に再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2494
2434
  }
2495
2435
  function transcriptNotFound(vendor) {
2496
2436
  throw new AitermError(`最終 assistant メッセージを特定できませんでした(vendor=${vendor})。screen で確認してください。`, 2);
@@ -2557,18 +2497,18 @@ export async function readAgentTranscript(name, o = {}) {
2557
2497
  const active = readClaudeOperationMarker(meta);
2558
2498
  if (active) {
2559
2499
  const label = active.operationId ? `operation ${active.operationId}` : "operation_idなしのClaude turn";
2560
- throw new AitermError(`${label} はまだ完了していません。Stop完了後に同じsessionから再取得してください。`, 2);
2500
+ throw new AitermError(`${label} はまだ完了していません。Stop完了後に同じsessionから再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2561
2501
  }
2562
2502
  }
2563
2503
  // wait timeout は「失敗」ではなく状態不明。後着した同一launchの完了eventから
2564
2504
  // vendor session をbindし、promptを再送せず結果だけ回収できるようにする。
2565
2505
  recoverAgentVendorSession(meta);
2566
2506
  if (!meta.vendor_session_id) {
2567
- throw new AitermError(`agent session '${name}' はまだターンが完了していません。agent_done 完了後に再取得してください。`, 2);
2507
+ throw new AitermError(`agent session '${name}' はまだターンが完了していません。agent_done 完了後に再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2568
2508
  }
2569
2509
  const done = latestAgentDoneEvent(meta, operationId);
2570
2510
  if (operationId && !done) {
2571
- throw new AitermError(`operation ${operationId} はまだ完了していません。同じoperation_idで後から再取得してください。`, 2);
2511
+ throw new AitermError(`operation ${operationId} はまだ完了していません。同じoperation_idで後から再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2572
2512
  }
2573
2513
  const turnId = done?.turn_id ?? null;
2574
2514
  let text = "";
@@ -2711,42 +2651,8 @@ function assertInitialPromptNotPendingForSend(name, force) {
2711
2651
  throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。通常 pty_send は混入防止のため送信しません。` +
2712
2652
  `完了後に pty_send(wait:"agent_done") するか、手動介入が必要な場合だけ force:true を明示してください。`, 2);
2713
2653
  }
2714
- async function waitAgentDoneEvent(meta, startOffset, timeout, expectedOperationId = null) {
2715
- const deadline = performance.now() + timeout * 1000;
2716
- let cursor = startOffset;
2717
- let carry = "";
2718
- let malformedEvents = 0;
2719
- for (;;) {
2720
- const size = safeStatSize(meta.event_file);
2721
- if (size < cursor) {
2722
- cursor = 0;
2723
- carry = "";
2724
- }
2725
- if (size > cursor) {
2726
- if (size - cursor > AGENT_EVENT_MAX_BYTES) {
2727
- throw new AitermError("agent event file の増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
2728
- }
2729
- carry += readFileRange(meta.event_file, cursor, size).toString("utf8");
2730
- cursor = size;
2731
- const parts = carry.split("\n");
2732
- carry = parts.pop() ?? "";
2733
- const scanned = scanAgentDoneLines(parts, meta, expectedOperationId);
2734
- malformedEvents += scanned.malformedEvents;
2735
- if (scanned.ambiguousVendorSession) {
2736
- throw new AitermError("agent event file に複数の vendor_session_id が混在しています。該当セッションを閉じて起動し直してください。", 2);
2737
- }
2738
- if (scanned.event) {
2739
- bindAgentVendorSession(meta, scanned.event);
2740
- if (meta.vendor_session_id)
2741
- writeAgentMetadata(meta);
2742
- return { event: scanned.event, malformedEvents };
2743
- }
2744
- }
2745
- if (performance.now() >= deadline)
2746
- return { event: null, malformedEvents };
2747
- await sleep(AGENT_DONE_POLL_MS);
2748
- }
2749
- }
2654
+ // aiterm-wait の exit 契約(CLI と各所の案内文で共有する正)。exit≠完了: outcome が done の時だけ完了。
2655
+ export const AITERM_WAIT_OUTCOME_NOTE = `exit 0=done / 3=timeout(既定${DEFAULT_AGENT_DONE_TIMEOUT}秒・未完了) / 4=closed。receiptのoutcomeが正で、done以外は未完了`;
2750
2656
  // 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
2751
2657
  // event fileのtail規律(未終端行保持・増分上限)はwaitAgentDoneEventと同一だが、
2752
2658
  // vendor_session_idのbind永続化を行わない点だけ意図的に異なる(waiterは観測者であって所有者でない)。
@@ -2756,11 +2662,14 @@ export async function observeAgentDone(name, o = {}) {
2756
2662
  if (operationId && meta.kind !== "claude") {
2757
2663
  throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
2758
2664
  }
2665
+ if (o.cursor != null && (!Number.isInteger(o.cursor) || o.cursor < 0)) {
2666
+ throw new AitermError("cursor は0以上の整数byte offsetで指定してください", 2);
2667
+ }
2759
2668
  const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
2760
2669
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
2761
- // operation相関があるならoperation_idの一意性で誤帰属を防げるため先頭から全走査できる
2762
- // (waiter起動がdispatchより遅れても取りこぼさない)。相関なしはwaiter起動時EOFを境界にする。
2763
- const startOffset = operationId ? 0 : safeStatSize(meta.event_file);
2670
+ // 境界の優先順: dispatch receipt の event_cursor(起動順序に依存しない)→ operation相関
2671
+ // (operation_idの一意性で先頭から全走査できる)→ waiter起動時EOF(waiter先行起動が前提)。
2672
+ const startOffset = o.cursor ?? (operationId ? 0 : safeStatSize(meta.event_file));
2764
2673
  const deadline = performance.now() + timeout * 1000;
2765
2674
  let cursor = startOffset;
2766
2675
  let carry = "";
@@ -2806,24 +2715,6 @@ export async function observeAgentDone(name, o = {}) {
2806
2715
  await sleep(AGENT_DONE_POLL_MS);
2807
2716
  }
2808
2717
  }
2809
- function agentDoneSuffix(wait, vendor, operationId = null) {
2810
- const ev = wait.event;
2811
- if (!ev) {
2812
- const malformed = wait.malformedEvents ? ` malformed_events=${wait.malformedEvents}` : "";
2813
- const operation = operationId ? ` operation_id=${operationId}` : "";
2814
- return ` [is_complete=False via agent_timeout vendor=${vendor}${operation}${malformed}]`;
2815
- }
2816
- const bits = [
2817
- "is_complete=True",
2818
- "via agent_done",
2819
- `vendor=${ev.vendor}`,
2820
- ev.turn_id ? `turn_id=${ev.turn_id}` : null,
2821
- ev.vendor_session_id ? `vendor_session_id=${ev.vendor_session_id}` : null,
2822
- ev.operation_id ? `operation_id=${ev.operation_id}` : null,
2823
- `done_status=${ev.done_status}`,
2824
- ].filter(Boolean);
2825
- return ` [${bits.join(" ")}]`;
2826
- }
2827
2718
  function isAgentTuiReady(kind, screen) {
2828
2719
  if (kind === "claude") {
2829
2720
  return screen.includes("Claude Code") && /(^|\n)\s*❯/.test(screen);
@@ -2890,12 +2781,6 @@ async function settleAgentDoneScreenImpl(sample, sleepFn, opts = {}) {
2890
2781
  }
2891
2782
  return { unstable: true, samples };
2892
2783
  }
2893
- async function settleAgentDoneScreen(name, lines) {
2894
- return settleAgentDoneScreenImpl(() => ({
2895
- screen: captureScreen(name, lines),
2896
- logSize: safeStatSize(logpath(name)),
2897
- }), sleep);
2898
- }
2899
2784
  export async function __testSettleAgentDoneScreen(samples, opts = {}) {
2900
2785
  if (samples.length === 0)
2901
2786
  throw new AitermError("screen settle test samples が空です", 2);
@@ -2939,135 +2824,96 @@ async function sendAgentPromptText(name, text) {
2939
2824
  }
2940
2825
  export async function sendInitialAgentPrompt(name, text, o = {}) {
2941
2826
  assertSessionName(name);
2942
- const waitMode = normalizeAgentLauncherWait(o.wait);
2943
2827
  const meta = loadAgentMetadata(name);
2944
- if (agentWaitLocks.has(name))
2945
- throw new AitermError(`agent session '${name}' は別の agent_done 待機中です`, 2);
2946
2828
  if (meta.initial_prompt === "done") {
2947
2829
  throw new AitermError(`agent session '${name}' の起動時 prompt は既に完了しています`, 2);
2948
2830
  }
2949
2831
  if (meta.initial_prompt === "pending" || meta.initial_prompt === "sent") {
2950
2832
  throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。初回応答完了後に再度操作してください。`, 2);
2951
2833
  }
2952
- const releaseFileLock = acquireAgentWaitFileLock(meta);
2953
- const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
2954
- const screen = o.screen ?? true;
2955
- agentWaitLocks.add(name);
2834
+ setInitialPromptState(meta, "not_sent");
2835
+ const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2836
+ if (!ready.ready) {
2837
+ return {
2838
+ text: `initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples}\n` +
2839
+ `agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。prompt は送信していません。`,
2840
+ event_cursor: null,
2841
+ };
2842
+ }
2843
+ const startOffset = safeStatSize(meta.event_file);
2956
2844
  try {
2957
- setInitialPromptState(meta, "not_sent");
2958
- const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2959
- if (!ready.ready) {
2960
- return (`initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples}\n` +
2961
- `agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。prompt は送信していません。`);
2962
- }
2963
- const startOffset = safeStatSize(meta.event_file);
2964
- try {
2965
- if (meta.kind === "claude") {
2966
- prepareSendText(text, { raw: false, force: true });
2967
- reserveAnonymousClaudeTurn(meta);
2968
- }
2969
- await sendAgentPromptText(name, text);
2970
- setInitialPromptState(meta, "pending");
2971
- }
2972
- catch (e) {
2973
- setInitialPromptState(meta, "failed");
2974
- throw e;
2975
- }
2976
- if (waitMode === "none") {
2977
- return (`initial_prompt=pending vendor=${meta.kind}\n` +
2978
- `起動時 prompt を送信しました。完了後の follow-up は pty_send(wait:"agent_done") を使ってください。`);
2979
- }
2980
- const wait = await waitAgentDoneEvent(meta, startOffset, timeout);
2981
- if (wait.event) {
2982
- setInitialPromptState(meta, "done");
2845
+ if (meta.kind === "claude") {
2846
+ prepareSendText(text, { raw: false, force: true });
2847
+ reserveAnonymousClaudeTurn(meta);
2983
2848
  }
2984
- else {
2985
- setInitialPromptState(meta, "pending");
2986
- }
2987
- const settled = wait.event
2988
- ? await settleAgentDoneScreen(name, o.lines ?? 0)
2989
- : { unstable: false, samples: 0 };
2990
- const out = await readOutput(name, {
2991
- screen,
2992
- lines: o.lines ?? null,
2993
- timeout: 0,
2994
- });
2995
- writeOffset(name, safeStatSize(logpath(name)));
2996
- return out + agentDoneSuffix(wait, meta.kind) + (settled.unstable ? " [agent_done_but_screen_unstable]" : "");
2849
+ await sendAgentPromptText(name, text);
2850
+ setInitialPromptState(meta, "pending");
2997
2851
  }
2998
- finally {
2999
- agentWaitLocks.delete(name);
3000
- releaseFileLock();
2852
+ catch (e) {
2853
+ setInitialPromptState(meta, "failed");
2854
+ throw e;
3001
2855
  }
2856
+ return {
2857
+ text: `initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset}\n` +
2858
+ `起動時 prompt を送信した。完了通知は aiterm-wait --session ${name} --cursor ${startOffset} をホストのバックグラウンドタスクとして実行し、` +
2859
+ `exit 時に receipt の outcome で判定する(${AITERM_WAIT_OUTCOME_NOTE})。回収は pty_read(agent_transcript:true) を使う。`,
2860
+ event_cursor: startOffset,
2861
+ };
2862
+ }
2863
+ export function isAgentSession(name) {
2864
+ assertSessionName(name);
2865
+ return tryLoadAgentMetadata(name) !== null;
3002
2866
  }
3003
- export async function sendAndWaitAgentDone(name, text, o = {}) {
2867
+ // v0.16.0: 親をブロックする wait 経路は廃止した。send は ready gate と submit 分離を内蔵した
2868
+ // dispatch として即返り、event_cursor(送信直前の event file 境界)を receipt で返す。
2869
+ // 完了通知は aiterm-wait(--cursor で境界を渡す)、回収は pty_read / claude_turn recover が担う。
2870
+ export async function dispatchAgentTurn(name, text, o = {}) {
3004
2871
  assertSessionName(name);
3005
- if (o.enter === false)
3006
- throw new AitermError('wait:"agent_done" は enter:false と併用できません', 2);
3007
- if (o.mark)
3008
- throw new AitermError('wait:"agent_done" と mark:true は併用できません', 2);
3009
- if (o.rtk)
3010
- throw new AitermError('wait:"agent_done" と rtk:true は併用できません', 2);
3011
2872
  const meta = loadAgentMetadata(name);
3012
2873
  const operationId = o.operation_id == null ? null : validateOperationId(o.operation_id);
3013
2874
  if (operationId && meta.kind !== "claude") {
3014
2875
  throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
3015
2876
  }
3016
- if (agentWaitLocks.has(name))
3017
- throw new AitermError(`agent session '${name}' は別の agent_done 待機中です`, 2);
3018
- const releaseFileLock = acquireAgentWaitFileLock(meta);
3019
- const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
3020
- const screen = o.screen ?? true;
3021
- agentWaitLocks.add(name);
3022
- try {
3023
- bindCompletedInitialPrompt(meta);
3024
- if (!meta.vendor_session_id) {
3025
- const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
3026
- if (!ready.ready) {
3027
- throw new AitermError(`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。文字列は送信していません。` +
3028
- "少し後で pty_read(screen:true) を確認し、TUI が起動済みなら再度 pty_send(wait:\"agent_done\") してください。", 2);
3029
- }
3030
- }
3031
- const startOffset = safeStatSize(meta.event_file);
3032
- if (meta.kind === "claude") {
3033
- // durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
3034
- // receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
3035
- prepareSendText(text, { raw: o.raw, force: o.force });
3036
- if (operationId) {
3037
- reserveClaudeOperation(meta, operationId);
3038
- }
3039
- else
3040
- reserveAnonymousClaudeTurn(meta);
2877
+ bindCompletedInitialPrompt(meta);
2878
+ if (!meta.vendor_session_id) {
2879
+ const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2880
+ if (!ready.ready) {
2881
+ throw new AitermError(`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。文字列は送信していません。` +
2882
+ "少し後で pty_read(screen:true) を確認し、TUI が起動済みなら再度 pty_send してください。", 2);
3041
2883
  }
3042
- send(name, text, {
3043
- enter: false,
3044
- force: o.force,
3045
- raw: o.raw,
3046
- mark: false,
3047
- rtk: false,
3048
- preserveAgentOperation: meta.kind === "claude",
3049
- });
3050
- // Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
3051
- await sleep(AGENT_SUBMIT_DELAY_MS);
3052
- sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" });
3053
- const wait = await waitAgentDoneEvent(meta, startOffset, timeout, operationId);
3054
- const settled = wait.event
3055
- ? await settleAgentDoneScreen(name, o.lines ?? 0)
3056
- : { unstable: false, samples: 0 };
3057
- const out = await readOutput(name, {
3058
- screen,
3059
- lines: o.lines ?? null,
3060
- timeout: 0,
3061
- });
3062
- writeOffset(name, safeStatSize(logpath(name)));
3063
- return out + agentDoneSuffix(wait, meta.kind, operationId) + (settled.unstable ? " [agent_done_but_screen_unstable]" : "");
3064
2884
  }
3065
- finally {
3066
- agentWaitLocks.delete(name);
3067
- releaseFileLock();
2885
+ const startOffset = safeStatSize(meta.event_file);
2886
+ if (meta.kind === "claude") {
2887
+ // durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
2888
+ // receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
2889
+ prepareSendText(text, { raw: o.raw, force: o.force });
2890
+ if (operationId) {
2891
+ reserveClaudeOperation(meta, operationId);
2892
+ }
2893
+ else
2894
+ reserveAnonymousClaudeTurn(meta);
3068
2895
  }
2896
+ send(name, text, {
2897
+ enter: false,
2898
+ force: o.force,
2899
+ raw: o.raw,
2900
+ mark: false,
2901
+ rtk: false,
2902
+ preserveAgentOperation: meta.kind === "claude",
2903
+ });
2904
+ // Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
2905
+ await sleep(AGENT_SUBMIT_DELAY_MS);
2906
+ sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" });
2907
+ return {
2908
+ schema: "aiterm.agent-dispatch.v1",
2909
+ session_id: meta.aiterm_session,
2910
+ launch_id: meta.launch_id,
2911
+ vendor: meta.kind,
2912
+ event_cursor: startOffset,
2913
+ operation_id: operationId,
2914
+ };
3069
2915
  }
3070
- export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, timeout, }) {
2916
+ export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, }) {
3071
2917
  assertSessionName(name);
3072
2918
  if (action !== "issue" && action !== "recover") {
3073
2919
  throw new AitermError('action は "issue" または "recover" を指定してください', 2);
@@ -3080,22 +2926,12 @@ export async function runClaudeOperation({ session_id: name, action, operation_i
3080
2926
  if (typeof text !== "string" || text.length === 0) {
3081
2927
  throw new AitermError("claude_turn issueには空でないtextが必要です", 2);
3082
2928
  }
3083
- const waitTimeout = timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
3084
- if (!Number.isFinite(waitTimeout) || waitTimeout < 0 || waitTimeout > 3600) {
3085
- throw new AitermError("claude_turn timeoutは0〜3600秒で指定してください", 2);
3086
- }
3087
- await sendAndWaitAgentDone(name, text, {
3088
- operation_id: operationId,
3089
- timeout: waitTimeout,
3090
- screen: false,
3091
- lines: 0,
3092
- });
2929
+ // v0.16.0: issue は dispatch-only。完了通知は aiterm-wait --operation、回収は recover が担う。
2930
+ await dispatchAgentTurn(name, text, { operation_id: operationId });
3093
2931
  }
3094
2932
  else {
3095
2933
  if (text != null)
3096
2934
  throw new AitermError("claude_turn recoverにtextは指定できません", 2);
3097
- if (timeout != null)
3098
- throw new AitermError("claude_turn recoverにtimeoutは指定できません", 2);
3099
2935
  }
3100
2936
  const inspected = inspectClaudeOperation(meta, operationId, action);
3101
2937
  if (action === "issue" && inspected.status === "pending") {
@@ -3497,37 +3333,26 @@ export function openAgent(kind, opts = {}) {
3497
3333
  `起動直後に増分 pty_read すると空/半描画になり得るので screen:true を使う。`,
3498
3334
  ];
3499
3335
  }
3500
- async function sendInitialPromptWithoutAgentDone(name, kind, text, opts = {}) {
3501
- const ready = await waitAgentTuiReadyByKind(name, kind, opts.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
3502
- if (!ready.ready) {
3503
- return (`initial_prompt=not_sent vendor=${kind} ready=false samples=${ready.samples}\n` +
3504
- `agent session '${name}' の ${agentLabel(kind)} TUI が入力受付状態になりません。prompt は送信していません。`);
3505
- }
3506
- await sendAgentPromptText(name, text);
3507
- return `initial_prompt=sent vendor=${kind}\n起動時 prompt を送信しました。完了待ちは通常の pty_read で行ってください。`;
3508
- }
3509
3336
  export async function openAgentWithInitialPrompt(kind, opts = {}) {
3510
- const waitMode = normalizeAgentLauncherWait(opts.wait);
3511
3337
  const prompt = opts.prompt ?? null;
3512
- const agentDone = !!opts.agent_done;
3513
- if (opts.launch_operation_id != null && (prompt !== null || waitMode !== "none")) {
3514
- throw new AitermError('launch_operation_idはpromptなし・wait:"none"のmanaged Claude launchだけで指定できます', 2);
3515
- }
3516
- if (waitMode === "agent_done" && !prompt) {
3517
- throw new AitermError('wait:"agent_done" は prompt 指定時だけ使えます', 2);
3518
- }
3519
- if (waitMode === "agent_done" && !agentDone) {
3520
- throw new AitermError('wait:"agent_done" には agent_done:true が必要です', 2);
3521
- }
3522
- if (!prompt) {
3523
- return openAgent(kind, opts);
3524
- }
3525
- if (kind !== "codex" && kind !== "claude") {
3526
- if (waitMode === "agent_done") {
3527
- throw new AitermError(`${agentLabel(kind)} の起動時 prompt wait は未対応です。` +
3528
- `agent_done:true で prompt なし起動後、TUI のログイン/ready を確認してから pty_send(wait:"agent_done") を使ってください。`, 2);
3529
- }
3530
- return openAgent(kind, opts);
3338
+ if (opts.launch_operation_id != null && prompt !== null) {
3339
+ throw new AitermError("launch_operation_idはpromptなしのmanaged Claude launchだけで指定できます", 2);
3340
+ }
3341
+ // v0.16.0: launcher は常に managed(Stop hook つき)で立つ。手動運転したい場合は
3342
+ // pty_open で素の PTY を開き、vendor CLI を自分で send する。
3343
+ // 第3要素は「起動時点でturnが走っているか」の event_cursor: Grok/Composer の argv prompt は
3344
+ // event file 新規作成直後の起動=境界0、prompt なしの起動は turn なし=null。
3345
+ if (!prompt || (kind !== "codex" && kind !== "claude")) {
3346
+ const [sid, hint] = openAgent(kind, {
3347
+ session_name: opts.session_name ?? null,
3348
+ model: opts.model ?? null,
3349
+ reasoning_effort: opts.reasoning_effort ?? null,
3350
+ cwd: opts.cwd ?? null,
3351
+ prompt,
3352
+ agent_done: true,
3353
+ launch_operation_id: opts.launch_operation_id ?? null,
3354
+ });
3355
+ return [sid, hint, prompt ? 0 : null];
3531
3356
  }
3532
3357
  const [sid, hint] = openAgent(kind, {
3533
3358
  session_name: opts.session_name ?? null,
@@ -3535,22 +3360,14 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3535
3360
  reasoning_effort: opts.reasoning_effort ?? null,
3536
3361
  cwd: opts.cwd ?? null,
3537
3362
  prompt: null,
3538
- agent_done: agentDone,
3363
+ agent_done: true,
3539
3364
  launch_operation_id: opts.launch_operation_id ?? null,
3540
3365
  });
3541
3366
  try {
3542
- const initial = agentDone
3543
- ? await sendInitialAgentPrompt(sid, prompt, {
3544
- wait: waitMode,
3545
- timeout: opts.timeout ?? undefined,
3546
- ready_timeout: opts.ready_timeout ?? undefined,
3547
- screen: opts.screen ?? undefined,
3548
- lines: opts.lines ?? null,
3549
- })
3550
- : await sendInitialPromptWithoutAgentDone(sid, kind, prompt, {
3551
- ready_timeout: opts.ready_timeout ?? undefined,
3552
- });
3553
- return [sid, `${hint}\n${initial}`];
3367
+ const initial = await sendInitialAgentPrompt(sid, prompt, {
3368
+ ready_timeout: opts.ready_timeout ?? undefined,
3369
+ });
3370
+ return [sid, `${hint}\n${initial.text}`, initial.event_cursor];
3554
3371
  }
3555
3372
  catch (e) {
3556
3373
  const code = e instanceof AitermError ? e.code : 1;
package/dist/index.js CHANGED
@@ -86,27 +86,19 @@ server.registerTool("pty_open", {
86
86
  }
87
87
  });
88
88
  server.registerTool("pty_send", {
89
- description: "セッションへテキスト(コマンド)を送る。通常は送信だけ行い、出力は pty_read で取得する。" +
90
- "agent_done:true で起動した Claude/Codex/Grok/Composer セッションは wait:'agent_done' で Stop hook まで待てる。" +
91
- "managed Claude sessionはturn相関のためwait:'agent_done'を必須とし、通常送信を拒否する。",
89
+ description: "セッションへテキストを送る。通常PTYへは送信のみ(出力は pty_read で取得)。" +
90
+ "agent session(launcher起動)への send は自動で dispatch になる: TUI の ready gate と submit 分離を通して即返り、" +
91
+ "receipt の event_cursor を返す=親はブロックしない。完了通知は `aiterm-wait --session <id> --cursor <event_cursor>` を" +
92
+ "ホストのバックグラウンドタスクとして実行し、exit時にreceiptのoutcomeで判定する" +
93
+ `(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
94
+ "結果回収は pty_read(agent_transcript:true)、Claude の durable turn は claude_turn を使う。" +
95
+ "force:true は agent session への手動介入用の素送信。",
92
96
  inputSchema: {
93
97
  session_id: z.string(),
94
98
  text: z
95
99
  .string()
96
- .describe("送る文字列(コマンド)。UTF-8で最大64KiB"),
97
- enter: z.boolean().default(true).describe("末尾で Enter を送る"),
98
- wait: z
99
- .enum(["none", "agent_done"])
100
- .default("none")
101
- .describe("none=従来通り送信のみ(managed Claudeでは拒否)。agent_done=agent Stop hook まで待って最終画面を返す"),
102
- timeout: z.number().default(600).describe("wait:'agent_done' の最大待ち秒数"),
103
- screen: z.boolean().default(true).describe("wait:'agent_done' の返り値を描画済みスクリーンにする"),
104
- lines: z.number().int().nullish().describe("wait:'agent_done' で返す末尾 N 行"),
105
- operation_id: z
106
- .string()
107
- .regex(/^sha256:[0-9a-f]{64}$/)
108
- .nullish()
109
- .describe("Claudeのdurable caller operation ID。wait:'agent_done'時だけ指定し、timeout後の同一結果回収へ使う"),
100
+ .describe("送る文字列(コマンド/prompt)。UTF-8で最大64KiB"),
101
+ enter: z.boolean().default(true).describe("末尾で Enter を送る(agent dispatch では常に submit)"),
110
102
  mark: z
111
103
  .boolean()
112
104
  .default(false)
@@ -116,28 +108,59 @@ server.registerTool("pty_send", {
116
108
  force: z
117
109
  .boolean()
118
110
  .default(false)
119
- .describe("破壊的コマンドゲートを越える。agent 起動時 prompt の完了待ち中の混入防止ガードも同時に解除する"),
111
+ .describe("破壊的コマンドゲートを越える。agent session では dispatch せず素送信する(手動介入用)"),
120
112
  rtk: z.boolean().default(false).describe("既知コマンドを rtk 形へ委譲して送る(rtk 不在なら素通し)"),
121
113
  raw: z.boolean().default(false).describe("送信前サニタイズを無効化"),
122
114
  },
123
- }, async ({ session_id, text, enter, wait, timeout, screen, lines, operation_id, mark, force, rtk, raw }) => {
115
+ outputSchema: {
116
+ schema: z.literal("aiterm.pty-send-result.v1"),
117
+ mode: z.enum(["sent", "agent_dispatch"]),
118
+ session_id: z.string(),
119
+ event_cursor: z.number().int().nullable(),
120
+ launch_id: z.string().nullable(),
121
+ vendor: z.enum(["claude", "codex", "grok", "composer"]).nullable(),
122
+ },
123
+ }, async ({ session_id, text, enter, mark, force, rtk, raw }) => {
124
124
  try {
125
- if (wait === "agent_done") {
126
- return ok(await core.sendAndWaitAgentDone(session_id, text, {
127
- enter,
128
- mark,
129
- force,
130
- rtk,
131
- raw,
132
- timeout,
133
- screen,
134
- lines: lines ?? null,
135
- operation_id: operation_id ?? null,
136
- }));
125
+ if (!force && core.isAgentSession(session_id)) {
126
+ if (enter === false)
127
+ throw new Error("agent session への dispatch は enter:false と併用できません(手動介入は force:true)");
128
+ if (mark)
129
+ throw new Error("agent session への dispatch は mark:true と併用できません");
130
+ if (rtk)
131
+ throw new Error("agent session への dispatch は rtk:true と併用できません");
132
+ const receipt = await core.dispatchAgentTurn(session_id, text, { raw });
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: `dispatchした(vendor=${receipt.vendor})。完了通知: aiterm-wait --session ${receipt.session_id} --cursor ${receipt.event_cursor} を` +
138
+ `ホストのバックグラウンドタスクとして実行し、exit時にreceiptのoutcomeで判定(${core.AITERM_WAIT_OUTCOME_NOTE})。` +
139
+ "回収: pty_read(agent_transcript:true)",
140
+ },
141
+ ],
142
+ structuredContent: {
143
+ schema: "aiterm.pty-send-result.v1",
144
+ mode: "agent_dispatch",
145
+ session_id: receipt.session_id,
146
+ event_cursor: receipt.event_cursor,
147
+ launch_id: receipt.launch_id,
148
+ vendor: receipt.vendor,
149
+ },
150
+ };
137
151
  }
138
- if (operation_id != null)
139
- throw new Error("operation_idはwait:'agent_done'時だけ指定できます");
140
- return ok(core.send(session_id, text, { enter, mark, force, rtk, raw }));
152
+ const out = core.send(session_id, text, { enter, mark, force, rtk, raw });
153
+ return {
154
+ content: [{ type: "text", text: out }],
155
+ structuredContent: {
156
+ schema: "aiterm.pty-send-result.v1",
157
+ mode: "sent",
158
+ session_id,
159
+ event_cursor: null,
160
+ launch_id: null,
161
+ vendor: null,
162
+ },
163
+ };
141
164
  }
142
165
  catch (e) {
143
166
  return fail(e);
@@ -279,7 +302,6 @@ server.registerTool("claude_turn", {
279
302
  session_id: z.string(),
280
303
  operation_id: z.string().regex(/^sha256:[0-9a-f]{64}$/),
281
304
  text: z.string().optional().describe("issueだけに指定するbounded turn本文"),
282
- timeout: z.number().min(0).max(3600).optional().describe("issueだけに指定するStop待ち秒数"),
283
305
  },
284
306
  outputSchema: {
285
307
  schema: z.literal("aiterm.claude-operation-result.v1"),
@@ -290,14 +312,13 @@ server.registerTool("claude_turn", {
290
312
  raw_output: z.string().nullable(),
291
313
  reason: z.enum(["operation_not_found", "result_unknown"]).nullable(),
292
314
  },
293
- }, async ({ action, session_id, operation_id, text, timeout }) => {
315
+ }, async ({ action, session_id, operation_id, text }) => {
294
316
  try {
295
317
  const result = await core.runClaudeOperation({
296
318
  action,
297
319
  session_id,
298
320
  operation_id,
299
321
  text: text ?? undefined,
300
- timeout,
301
322
  });
302
323
  return {
303
324
  content: [{ type: "text", text: JSON.stringify(result) }],
@@ -323,68 +344,61 @@ const agentEffortDesc = (kind) => kind === "claude"
323
344
  "composer は effort 自体非対応)。指定すると起動前にエラーを返す"
324
345
  : "reasoning effort(思考レベル)。low/medium/high/xhigh/max/ultra(CLI 版依存)。" +
325
346
  "ultra は max 推論+proactive 自動委譲 ON=使用量急増注意(明示要求時のみ)。省略時は端末 config/CLI 既定。";
347
+ // 全launcher共通の完了受信ガイド。launch応答のwait_command(起動時promptあり時)/pty_send dispatchの
348
+ // event_cursorから組んだaiterm-waitをホストのバックグラウンドタスクとして1本実行し、exit時にreceiptの
349
+ // outcomeで判定する。
350
+ const agentCompletionDesc = `完了通知は起動応答の wait_command(初回prompt時)または pty_send dispatch 後の ` +
351
+ `aiterm-wait --session <id> --cursor <event_cursor> をホストのバックグラウンドタスクとして実行し、` +
352
+ `exit時にreceiptのoutcomeで判定する(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
353
+ `結果回収は pty_read(agent_transcript:true)。`;
326
354
  function registerAgentTool(toolName, kind, desc) {
327
- const initialPromptWaitSchema = {};
328
- if (kind === "codex" || kind === "claude") {
329
- initialPromptWaitSchema.wait = z
330
- .enum(["none", "agent_done"])
331
- .default("none")
332
- .describe("none=従来通り起動/初回prompt送信のみ。agent_done=起動時promptのStop hookまで待つ(agent_done:true必須)");
333
- initialPromptWaitSchema.timeout = z.number().default(600).describe("wait:'agent_done' の最大待ち秒数");
334
- initialPromptWaitSchema.screen = z.boolean().default(true).describe("wait:'agent_done' の返り値を描画済みスクリーンにする");
335
- initialPromptWaitSchema.lines = z.number().int().nullish().describe("wait:'agent_done' で返す末尾 N 行");
336
- }
337
355
  const correlatedLaunchSchema = {};
338
356
  if (kind === "claude") {
339
357
  correlatedLaunchSchema.launch_operation_id = z
340
358
  .string()
341
359
  .regex(/^sha256:[0-9a-f]{64}$/)
342
360
  .optional()
343
- .describe("promptless managed launchのexact replay相関ID。session_nameとagent_done:trueが必須");
361
+ .describe("promptless managed launchのexact replay相関ID。session_name必須");
344
362
  }
345
363
  server.registerTool(toolName, {
346
364
  description: desc,
347
365
  inputSchema: {
348
- prompt: z.string().nullish().describe("起動時に渡す初手プロンプト(任意)。省略で素のTUI起動"),
366
+ prompt: z.string().nullish().describe("起動時に渡す初手プロンプト(任意)。送信後は待たずに即返る"),
349
367
  model: z.string().nullish().describe(agentModelDesc(kind)),
350
368
  // grok/composer の effort は対話 TUI で無効(headless 専用)=core 側が起動前に明示エラーで拒否。
351
369
  // codex は CLI 側の値集合が版で変わるため縛らない(core 側も同方針)。
352
370
  reasoning_effort: z.string().nullish().describe(agentEffortDesc(kind)),
353
371
  cwd: z.string().nullish().describe("作業ディレクトリ(対象リポのルート等・任意)"),
354
372
  session_name: z.string().nullish().describe("セッション名(省略で自動採番)"),
355
- agent_done: z
356
- .boolean()
357
- .default(false)
358
- .describe("managed Stop hook を有効化し、pty_send(wait:'agent_done') を使えるようにする"),
359
373
  ...correlatedLaunchSchema,
360
- ...initialPromptWaitSchema,
361
374
  },
362
375
  outputSchema: {
363
376
  schema: z.literal("aiterm.agent-launch-result.v1"),
364
377
  provider: z.literal(kind),
365
378
  session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
366
379
  managed_completion: z.boolean(),
380
+ // 起動時 prompt でturnが走っている時だけ非null(additive拡張)。wait_command はそのままホストの
381
+ // バックグラウンドタスクとして実行できる完了待ちコマンド。
382
+ event_cursor: z.number().int().nullable(),
383
+ wait_command: z.string().nullable(),
367
384
  },
368
- }, async ({ prompt, model, reasoning_effort, cwd, session_name, agent_done, launch_operation_id, wait, timeout, screen, lines }) => {
385
+ }, async ({ prompt, model, reasoning_effort, cwd, session_name, launch_operation_id }) => {
369
386
  try {
370
- const [sid, hint] = await core.openAgentWithInitialPrompt(kind, {
387
+ const [sid, hint, eventCursor] = await core.openAgentWithInitialPrompt(kind, {
371
388
  prompt: prompt ?? undefined,
372
389
  model: model ?? undefined,
373
390
  reasoning_effort: reasoning_effort ?? undefined,
374
391
  cwd: cwd ?? undefined,
375
392
  session_name: session_name ?? undefined,
376
- agent_done: agent_done ?? false,
377
393
  launch_operation_id: launch_operation_id ?? undefined,
378
- wait: wait ?? "none",
379
- timeout,
380
- screen,
381
- lines,
382
394
  });
383
395
  const structured = {
384
396
  schema: "aiterm.agent-launch-result.v1",
385
397
  provider: kind,
386
398
  session_id: sid,
387
- managed_completion: agent_done ?? false,
399
+ managed_completion: true,
400
+ event_cursor: eventCursor,
401
+ wait_command: eventCursor === null ? null : `aiterm-wait --session ${sid} --cursor ${eventCursor}`,
388
402
  };
389
403
  return {
390
404
  content: [{ type: "text", text: `session_id: ${sid}\n${hint}` }],
@@ -397,14 +411,22 @@ function registerAgentTool(toolName, kind, desc) {
397
411
  });
398
412
  }
399
413
  registerAgentTool("claude_agent", "claude", "【Claude Code (Anthropic)】の対話エージェントTUIを永続端末に起動する。`claude -p`ではなく、" +
400
- "同じ利用者可視sessionへpty_sendで継続入力する。agent_done:trueではisolated settingsのmanaged Stop hookで完了と結果を回収する。");
414
+ "同じ利用者可視sessionへpty_sendで継続入力する。常にmanaged(isolated settingsのStop hook)で起動する。" +
415
+ agentCompletionDesc +
416
+ "Claude の durable turn は claude_turn でも回収できる。");
401
417
  registerAgentTool("codex_agent", "codex", "【Codex (OpenAI)】の対話エージェント TUI を永続端末に起動する。実装・レビュー・調査を対話で回す。" +
402
- "起動後は pty_read で画面を読み pty_send で操作する。model / reasoning_effort を引数で指定可" +
418
+ "turn は pty_send で送る(自動で非ブロック dispatch になる)。" +
419
+ agentCompletionDesc +
420
+ "model / reasoning_effort を引数で指定可" +
403
421
  "(省略時は端末 config/CLI 既定を継承。実効値は起動応答に明示)。");
404
422
  registerAgentTool("grok_agent", "grok", "【Grok Build の Grok モデル (既定 grok-4.5)】の対話エージェント TUI を永続端末に起動する。" +
405
- "起動後は pty_read/pty_send で対話操作。model を引数で指定可。reasoning_effort は対話 TUI 非対応(指定はエラー)。");
423
+ "turn は pty_send で送る(自動で非ブロック dispatch になる)。" +
424
+ agentCompletionDesc +
425
+ "model を引数で指定可。reasoning_effort は対話 TUI 非対応(指定はエラー)。");
406
426
  registerAgentTool("composer_agent", "composer", "【Grok Build の Composer モデル (既定 grok-composer-2.5-fast)】の対話エージェント TUI を永続端末に起動する。" +
407
- "起動後は pty_read/pty_send で対話操作。model を引数で指定可。reasoning_effort は非対応(指定はエラー)。");
427
+ "turn は pty_send で送る(自動で非ブロック dispatch になる)。" +
428
+ agentCompletionDesc +
429
+ "model を引数で指定可。reasoning_effort は非対応(指定はエラー)。");
408
430
  async function main() {
409
431
  const transport = new StdioServerTransport();
410
432
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.15.1",
3
+ "version": "0.17.0",
4
4
  "mcpName": "io.github.kitepon-rgb/aiterm-mcp",
5
5
  "description": "AI-driven persistent terminal as a local stdio MCP server (tmux-backed). Holds one local PTY; SSH and containers are just commands you send into it. Also launches interactive Claude/Codex/Grok/Composer agent TUIs in a persistent terminal. Token-reducing reads.",
6
6
  "keywords": [