aiterm-mcp 0.15.0 → 0.16.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
@@ -52,14 +52,15 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
52
52
 
53
53
  ### 2. その端末の中に他のコーディングエージェントを起動する — オーケストレーションの旗艦
54
54
 
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は明示承認待ちであり、まだ成功扱いしない。
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` で継続操作する。**起動は常に managed**(aiterm 所有の Stop hook 付き)で、agent session への `pty_send` は非ブロックの **dispatch** になり `event_cursor` 入り receipt を即返す。完了通知は `aiterm-wait --session <id> --cursor <event_cursor>` をホストのバックグラウンドタスクとして実行し、その exit で受ける(親はブロックもポーリングもしない)。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
56
 
57
57
  ```text
58
- codex_agent({ session_name: "codex1", cwd: "/repo", agent_done: true,
59
- wait: "agent_done", prompt: "port test/legacy.py to vitest" })
58
+ codex_agent({ session_name: "codex1", cwd: "/repo",
59
+ prompt: "port test/legacy.py to vitest" })
60
60
  → { session_id: "codex1", … } # Codex が永続端末で稼働開始
61
61
  pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
62
- pty_send("codex1", "also fix the imports it broke", { wait: "agent_done" })
62
+ pty_send("codex1", "also fix the imports it broke") # 非ブロックdispatch=event_cursor入りreceipt
63
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # exitが完了push。回収は pty_read(agent_transcript:true)
63
64
  → 操舵し、Codex の次の入力境界で返る
64
65
  ```
65
66
 
@@ -67,12 +68,12 @@ pty_send("codex1", "also fix the imports it broke", { wait: "agent_done" })
67
68
 
68
69
  | ツール | 起動するもの | 主な引数 |
69
70
  | --- | --- | --- |
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?` |
71
+ | `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
72
+ | `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?` |
73
+ | `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
74
+ | `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
74
75
 
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`を使う。
76
+ 各ベンダーの 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
77
 
77
78
  エージェント間の隠れたプロトコルは無い。起動したClaude/Codex/Grok/Composerは利用者がattachできるもう1本の永続sessionであり、MCPクライアントが通常のPTY操作で駆動する。
78
79
 
@@ -257,7 +258,7 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
257
258
  | `pty_key` | 制御キーを送る | `session_id`, `key`(`C-c`/`Enter`/`Up`…) |
258
259
  | `pty_close` | 冪等に閉じ、`closed` / `already_closed`を返す | `session_id` |
259
260
  | `pty_list` | セッション一覧 | (なし) |
260
- | `claude_turn` | 相関済みmanaged Claude operationを送信または回収 | `action`, `session_id`, `operation_id`, `text?`, `timeout?` |
261
+ | `claude_turn` | 相関済みmanaged Claude operationをdispatch(issue)または回収(recover) | `action`, `session_id`, `operation_id`, `text?` |
261
262
  | `diagnostics` | 機械可読 JSON による read-only factory readiness | (なし) |
262
263
 
263
264
  `diagnostics` は PTY やエージェントを起動しない。パッケージ版、MCP 呼出 readiness、read-only な PTY 一覧要約、bounded runtime-error-store status、任意 vendor launcher の可用性だけを返す。path・環境値・認証情報・コマンド本文・PTY 出力・raw log は意図的に返さない。通常未設定の任意依存は `not_applicable`、安全に確定できない状態は `unverified` と表す。
@@ -274,18 +275,18 @@ consumer は `aiterm-runtime-errors snapshot` を読み、durable ingestion 後
274
275
 
275
276
  | ツール | 起動するもの | 主な引数 |
276
277
  | --- | --- | --- |
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?` |
278
+ | `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
279
+ | `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?` |
280
+ | `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
281
+ | `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
281
282
 
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成功と混同しない。
283
+ 対応する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
284
 
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・抽出不能は明示エラー。
285
+ エージェントの回答が画面 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
286
 
286
287
  ### 完了検出(5 層)
287
288
 
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` が付く。
289
+ `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
290
 
290
291
  ### トークン削減
291
292
 
package/README.md CHANGED
@@ -33,7 +33,7 @@ runtime-error store collect only when canonical dotagents config explicitly sets
33
33
  I/O. It ships via tag-triggered CI with npm provenance (OIDC Trusted Publishing);
34
34
  the GitHub Release re-registers the Official MCP Registry entry.
35
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).
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 (managed completion is POSIX/WSL/macOS only for now) · MIT · see the [CHANGELOG](CHANGELOG.md).
37
37
 
38
38
  ## Why now
39
39
 
@@ -62,27 +62,29 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
62
62
 
63
63
  ### 2. Launch other coding agents into that terminal — the orchestration flagship
64
64
 
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).
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.) 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
66
 
67
67
  ```text
68
- codex_agent({ session_name: "codex1", cwd: "/repo", agent_done: true,
69
- wait: "agent_done", prompt: "port test/legacy.py to vitest" })
68
+ codex_agent({ session_name: "codex1", cwd: "/repo",
69
+ prompt: "port test/legacy.py to vitest" })
70
70
  → { session_id: "codex1", … } # Codex now live in a persistent terminal
71
71
  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
72
+ pty_send("codex1", "also fix the imports it broke")
73
+ → non-blocking dispatch; receipt carries event_cursor
74
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # host background task; its exit = completion push
75
+ pty_read("codex1", { agent_transcript: true }) → collect the full answer
74
76
  ```
75
77
 
76
78
  One call per model, so the tool name itself tells you which model you get:
77
79
 
78
80
  | Tool | Launches | Key args |
79
81
  | --- | --- | --- |
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?` |
82
+ | `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `launch_operation_id?` |
83
+ | `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?` |
84
+ | `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?` |
85
+ | `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
86
 
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`.
87
+ 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
88
 
87
89
  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
90
 
@@ -264,12 +266,12 @@ On top of that sits a productized layer a raw tmux bridge doesn't have: **token-
264
266
  | Tool | Role | Key args |
265
267
  | --- | --- | --- |
266
268
  | `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` |
269
+ | `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
270
  | `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
271
  | `pty_key` | Send a control key | `session_id`, `key` (`C-c`/`Enter`/`Up`…) |
270
272
  | `pty_close` | Close idempotently; return `closed` / `already_closed` | `session_id` |
271
273
  | `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?` |
274
+ | `claude_turn` | Issue (dispatch-only) or recover one correlated managed-Claude operation | `action`, `session_id`, `operation_id`, `text?` |
273
275
  | `diagnostics` | Read-only factory readiness as machine-readable JSON | (none) |
274
276
 
275
277
  `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 +288,29 @@ Each launcher starts a specific vendor's interactive coding-agent TUI inside a f
286
288
 
287
289
  | Tool | Launches | Key args |
288
290
  | --- | --- | --- |
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?` |
291
+ | `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `launch_operation_id?` |
292
+ | `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?` |
293
+ | `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?` |
294
+ | `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
295
 
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.
296
+ 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
297
 
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.
298
+ 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
299
 
298
300
  ### Completion detection (5 layers)
299
301
 
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`.
302
+ `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
303
 
302
304
  ### Completion push for parent agents (`aiterm-wait`)
303
305
 
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:
306
+ As of v0.16.0 a parent agent **never blocks** on aiterm — there is no wait parameter anywhere. The whole flow is dispatch + one universal waiter:
305
307
 
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
+ 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.
309
+ 2. Run `aiterm-wait --session <id> --cursor <event_cursor> [--operation sha256:<64hex>] [--timeout <sec>]`. It observes the vendor Stop-hook completion event as a **pure reader** and exits with a one-line `aiterm.agent-wait-result.v1` receipt (`done` / `timeout` / `closed`). The `--cursor` boundary makes it start-order independent: no completion can slip past even if the waiter starts late.
310
+ 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.
311
+ 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.
310
312
 
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.
312
-
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.
313
+ `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
314
 
315
315
  ### Token reduction
316
316
 
@@ -380,7 +380,7 @@ MIT
380
380
 
381
381
  ## Grok OAuth isolation
382
382
 
383
- For `agent_done`, Grok/Composer keep launch-local `GROK_HOME` and fake `HOME`.
383
+ For managed completion, Grok/Composer keep launch-local `GROK_HOME` and fake `HOME`.
384
384
  The child receives `GROK_AUTH_PATH` pointing at the validated normal auth
385
385
  canonical file; managed homes never contain auth or lock symlinks/copies.
386
386
  aiterm does not create locks or copy credentials back. An inherited
@@ -7,14 +7,15 @@ import * as fs from "node:fs";
7
7
  import { AitermError, observeAgentDone } from "./core.js";
8
8
  const SESSION_RE = /^[A-Za-z0-9_-]{1,64}$/;
9
9
  const OPERATION_RE = /^sha256:[0-9a-f]{64}$/;
10
- const USAGE = "usage: aiterm-wait --session <name> [--operation sha256:<64hex>] [--timeout <sec>]";
10
+ const USAGE = "usage: aiterm-wait --session <name> [--cursor <event_cursor>] [--operation sha256:<64hex>] [--timeout <sec>]";
11
11
  export function parseArgs(argv) {
12
12
  let session = null;
13
13
  let operationId = null;
14
14
  let timeout = null;
15
+ let cursor = null;
15
16
  for (let i = 0; i < argv.length; i++) {
16
17
  const a = argv[i];
17
- if (a === "--session" || a === "--operation" || a === "--timeout") {
18
+ if (a === "--session" || a === "--operation" || a === "--timeout" || a === "--cursor") {
18
19
  const v = argv[i + 1];
19
20
  if (v === undefined)
20
21
  throw new Error(`${a} に値がありません。${USAGE}`);
@@ -33,6 +34,13 @@ export function parseArgs(argv) {
33
34
  throw new Error(`--operation が不正です。${USAGE}`);
34
35
  operationId = v;
35
36
  }
37
+ else if (a === "--cursor") {
38
+ if (cursor !== null)
39
+ throw new Error(`--cursor が重複しています。${USAGE}`);
40
+ if (!/^\d+$/.test(v))
41
+ throw new Error(`--cursor は0以上の整数byte offsetだけを受理します。${USAGE}`);
42
+ cursor = Number(v);
43
+ }
36
44
  else {
37
45
  if (timeout !== null)
38
46
  throw new Error(`--timeout が重複しています。${USAGE}`);
@@ -49,7 +57,7 @@ export function parseArgs(argv) {
49
57
  }
50
58
  if (session === null)
51
59
  throw new Error(`--session は必須です。${USAGE}`);
52
- return { session, operationId, timeout: timeout ?? 600 };
60
+ return { session, operationId, timeout: timeout ?? 600, cursor };
53
61
  }
54
62
  function emit(value) {
55
63
  process.stdout.write(JSON.stringify(value) + "\n");
@@ -59,6 +67,7 @@ export async function main(argv) {
59
67
  const result = await observeAgentDone(cmd.session, {
60
68
  operation_id: cmd.operationId,
61
69
  timeout: cmd.timeout,
70
+ cursor: cmd.cursor,
62
71
  });
63
72
  emit(result);
64
73
  }
package/dist/core.js CHANGED
@@ -209,12 +209,27 @@ function resolveTmux(observe = true) {
209
209
  }
210
210
  ptyDependencyError(tmuxMissingMessage(), observe);
211
211
  }
212
+ // tmux は locale が C/POSIX/未設定だと UTF-8 を扱えない: server は send-keys/paste のマルチバイト
213
+ // 入力を破壊し(文字消失・周辺バイトの並べ替えを実測)、client は format 出力のタブ等を "_" へ
214
+ // サニタイズする。GUI 起動の MCP client は LANG を持たないことが多く、その環境で立った tmux server は
215
+ // 以後すべての入力を壊すため、UTF-8 locale が確定しない場合だけ LC_CTYPE を明示注入する。
216
+ // 利用者が C/POSIX 以外を明示設定している場合はその選択を尊重して触らない。
217
+ export function tmuxSpawnEnv() {
218
+ const effective = process.env.LC_ALL || process.env.LC_CTYPE || process.env.LANG || "";
219
+ // 素の "C"/"POSIX" だけを壊れた既定とみなす。"C.UTF-8" を含む charset 付きの明示設定は尊重する。
220
+ if (effective && !/^(C|POSIX)$/i.test(effective))
221
+ return undefined;
222
+ const env = { ...process.env, LC_CTYPE: process.platform === "darwin" ? "UTF-8" : "C.UTF-8" };
223
+ // LC_ALL は LC_CTYPE より優先されるため、C/POSIX の LC_ALL が残ると注入が無効になる
224
+ delete env.LC_ALL;
225
+ return env;
226
+ }
212
227
  function tmuxCommandWithInput(observe, input, ...args) {
213
228
  // maxBuffer は既定 1MiB。capture-pane(大きなスクロールバック)や多セッションの list-sessions で
214
229
  // 頭打ちになり stdout が切れる/空になる。Python の subprocess.run は無制限だったので 64MiB へ広げる。
215
230
  // Windows は同じ tmux を WSL 経由(-e でログインシェル非経由=$ 展開やクオート崩れを防ぐ)で叩く。
216
231
  let r;
217
- const spawnOpts = { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, input };
232
+ const spawnOpts = { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, input, env: tmuxSpawnEnv() };
218
233
  if (isWin) {
219
234
  ensureWinBridge(observe);
220
235
  r = spawnSync("wsl.exe", ["-e", "tmux", "-S", SOCK, ...args], spawnOpts);
@@ -1200,9 +1215,6 @@ export function readOnlyPtyListDiagnostic() {
1200
1215
  }
1201
1216
  function closeSessionInternal(name, observeDependency = true) {
1202
1217
  assertSessionName(name);
1203
- if (agentWaitLocks.has(name)) {
1204
- throw new AitermError(`agent session '${name}' は agent_done 待機中のため close できません`, 2);
1205
- }
1206
1218
  {
1207
1219
  // 別プロセスの待機は in-memory Set に映らない。生きた file lock があれば close で state を消さない
1208
1220
  const foreign = liveWaitLocks(name);
@@ -1251,9 +1263,6 @@ export function closeSessionResult(name) {
1251
1263
  };
1252
1264
  }
1253
1265
  export function killAll() {
1254
- if (agentWaitLocks.size > 0) {
1255
- throw new AitermError(`agent_done 待機中の session があるため killAll できません: ${Array.from(agentWaitLocks).join(",")}`, 2);
1256
- }
1257
1266
  {
1258
1267
  // 別プロセスの待機(file lock が生きているもの)も巻き添えにしない
1259
1268
  const foreign = liveWaitLocks(null);
@@ -1317,16 +1326,8 @@ export function killAll() {
1317
1326
  return "killed all sessions on this socket";
1318
1327
  }
1319
1328
  const DEFAULT_AGENT_DONE_TIMEOUT = 600;
1320
- const agentWaitLocks = new Set();
1321
1329
  const agentMetadataNegativeCache = new Map();
1322
1330
  let agentTuiReadyStableSamplesTestOverride = null;
1323
- function normalizeAgentLauncherWait(v) {
1324
- if (v == null || v === "none")
1325
- return "none";
1326
- if (v === "agent_done")
1327
- return "agent_done";
1328
- throw new AitermError('wait は "none" または "agent_done" を指定してください', 2);
1329
- }
1330
1331
  function codexHookScriptPath() {
1331
1332
  return path.join(path.dirname(fileURLToPath(import.meta.url)), "codex-stop-hook.js");
1332
1333
  }
@@ -2016,61 +2017,12 @@ function liveWaitLocks(name) {
2016
2017
  const session = f.slice(0, f.indexOf("."));
2017
2018
  if (name != null && session !== name)
2018
2019
  continue;
2019
- if (agentWaitLocks.has(session))
2020
- continue; // in-process 待機は呼び出し側の既存ガードが担当
2021
2020
  const probe = probeWaitLock(path.join(dir, f));
2022
2021
  if (probe.live)
2023
2022
  out.push({ session, pid: probe.pid, at: probe.at });
2024
2023
  }
2025
2024
  return out;
2026
2025
  }
2027
- function waitLockBusyError(session, probe) {
2028
- const detail = probe.pid != null ? `(pid ${probe.pid}${probe.at ? ` / ${probe.at} 開始` : ""})` : "";
2029
- return new AitermError(`agent session '${session}' は別プロセスの agent_done 待機中です${detail}`, 2);
2030
- }
2031
- function acquireAgentWaitFileLock(meta) {
2032
- const p = agentWaitLockPath(meta.aiterm_session, meta.launch_id);
2033
- const nofollow = fs.constants.O_NOFOLLOW ?? 0;
2034
- let fd = null;
2035
- for (let attempt = 0; attempt < 2 && fd == null; attempt++) {
2036
- try {
2037
- fd = fs.openSync(p, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | nofollow, 0o600);
2038
- }
2039
- catch (e) {
2040
- if (e.code !== "EEXIST")
2041
- throw e;
2042
- const probe = probeWaitLock(p);
2043
- // 生きた待機、または回収後の再取得でも EEXIST(=直後に別プロセスが取得した race)は拒否
2044
- if (probe.live || attempt > 0)
2045
- throw waitLockBusyError(meta.aiterm_session, probe);
2046
- unlinkStaleWaitLock(p);
2047
- }
2048
- }
2049
- if (fd == null)
2050
- throw new AitermError(`agent session '${meta.aiterm_session}' は別プロセスの agent_done 待機中です`, 2);
2051
- try {
2052
- fs.writeSync(fd, JSON.stringify({ pid: process.pid, at: new Date().toISOString() }) + "\n", undefined, "utf8");
2053
- }
2054
- finally {
2055
- fs.closeSync(fd);
2056
- }
2057
- try {
2058
- fs.chmodSync(p, 0o600);
2059
- }
2060
- catch {
2061
- /* noop */
2062
- }
2063
- return () => {
2064
- try {
2065
- const st = fs.lstatSync(p);
2066
- if (st.isFile() && !st.isSymbolicLink() && st.uid === currentUid())
2067
- fs.unlinkSync(p);
2068
- }
2069
- catch {
2070
- /* noop */
2071
- }
2072
- };
2073
- }
2074
2026
  function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId, launchRequestDigest) {
2075
2027
  const launchId = randomBytes(16).toString("hex");
2076
2028
  const eventFile = agentEventPath(name, launchId);
@@ -2696,42 +2648,6 @@ function assertInitialPromptNotPendingForSend(name, force) {
2696
2648
  throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。通常 pty_send は混入防止のため送信しません。` +
2697
2649
  `完了後に pty_send(wait:"agent_done") するか、手動介入が必要な場合だけ force:true を明示してください。`, 2);
2698
2650
  }
2699
- async function waitAgentDoneEvent(meta, startOffset, timeout, expectedOperationId = null) {
2700
- const deadline = performance.now() + timeout * 1000;
2701
- let cursor = startOffset;
2702
- let carry = "";
2703
- let malformedEvents = 0;
2704
- for (;;) {
2705
- const size = safeStatSize(meta.event_file);
2706
- if (size < cursor) {
2707
- cursor = 0;
2708
- carry = "";
2709
- }
2710
- if (size > cursor) {
2711
- if (size - cursor > AGENT_EVENT_MAX_BYTES) {
2712
- throw new AitermError("agent event file の増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
2713
- }
2714
- carry += readFileRange(meta.event_file, cursor, size).toString("utf8");
2715
- cursor = size;
2716
- const parts = carry.split("\n");
2717
- carry = parts.pop() ?? "";
2718
- const scanned = scanAgentDoneLines(parts, meta, expectedOperationId);
2719
- malformedEvents += scanned.malformedEvents;
2720
- if (scanned.ambiguousVendorSession) {
2721
- throw new AitermError("agent event file に複数の vendor_session_id が混在しています。該当セッションを閉じて起動し直してください。", 2);
2722
- }
2723
- if (scanned.event) {
2724
- bindAgentVendorSession(meta, scanned.event);
2725
- if (meta.vendor_session_id)
2726
- writeAgentMetadata(meta);
2727
- return { event: scanned.event, malformedEvents };
2728
- }
2729
- }
2730
- if (performance.now() >= deadline)
2731
- return { event: null, malformedEvents };
2732
- await sleep(AGENT_DONE_POLL_MS);
2733
- }
2734
- }
2735
2651
  // 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
2736
2652
  // event fileのtail規律(未終端行保持・増分上限)はwaitAgentDoneEventと同一だが、
2737
2653
  // vendor_session_idのbind永続化を行わない点だけ意図的に異なる(waiterは観測者であって所有者でない)。
@@ -2741,11 +2657,14 @@ export async function observeAgentDone(name, o = {}) {
2741
2657
  if (operationId && meta.kind !== "claude") {
2742
2658
  throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
2743
2659
  }
2660
+ if (o.cursor != null && (!Number.isInteger(o.cursor) || o.cursor < 0)) {
2661
+ throw new AitermError("cursor は0以上の整数byte offsetで指定してください", 2);
2662
+ }
2744
2663
  const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
2745
2664
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
2746
- // operation相関があるならoperation_idの一意性で誤帰属を防げるため先頭から全走査できる
2747
- // (waiter起動がdispatchより遅れても取りこぼさない)。相関なしはwaiter起動時EOFを境界にする。
2748
- const startOffset = operationId ? 0 : safeStatSize(meta.event_file);
2665
+ // 境界の優先順: dispatch receipt の event_cursor(起動順序に依存しない)→ operation相関
2666
+ // (operation_idの一意性で先頭から全走査できる)→ waiter起動時EOF(waiter先行起動が前提)。
2667
+ const startOffset = o.cursor ?? (operationId ? 0 : safeStatSize(meta.event_file));
2749
2668
  const deadline = performance.now() + timeout * 1000;
2750
2669
  let cursor = startOffset;
2751
2670
  let carry = "";
@@ -2791,24 +2710,6 @@ export async function observeAgentDone(name, o = {}) {
2791
2710
  await sleep(AGENT_DONE_POLL_MS);
2792
2711
  }
2793
2712
  }
2794
- function agentDoneSuffix(wait, vendor, operationId = null) {
2795
- const ev = wait.event;
2796
- if (!ev) {
2797
- const malformed = wait.malformedEvents ? ` malformed_events=${wait.malformedEvents}` : "";
2798
- const operation = operationId ? ` operation_id=${operationId}` : "";
2799
- return ` [is_complete=False via agent_timeout vendor=${vendor}${operation}${malformed}]`;
2800
- }
2801
- const bits = [
2802
- "is_complete=True",
2803
- "via agent_done",
2804
- `vendor=${ev.vendor}`,
2805
- ev.turn_id ? `turn_id=${ev.turn_id}` : null,
2806
- ev.vendor_session_id ? `vendor_session_id=${ev.vendor_session_id}` : null,
2807
- ev.operation_id ? `operation_id=${ev.operation_id}` : null,
2808
- `done_status=${ev.done_status}`,
2809
- ].filter(Boolean);
2810
- return ` [${bits.join(" ")}]`;
2811
- }
2812
2713
  function isAgentTuiReady(kind, screen) {
2813
2714
  if (kind === "claude") {
2814
2715
  return screen.includes("Claude Code") && /(^|\n)\s*❯/.test(screen);
@@ -2875,12 +2776,6 @@ async function settleAgentDoneScreenImpl(sample, sleepFn, opts = {}) {
2875
2776
  }
2876
2777
  return { unstable: true, samples };
2877
2778
  }
2878
- async function settleAgentDoneScreen(name, lines) {
2879
- return settleAgentDoneScreenImpl(() => ({
2880
- screen: captureScreen(name, lines),
2881
- logSize: safeStatSize(logpath(name)),
2882
- }), sleep);
2883
- }
2884
2779
  export async function __testSettleAgentDoneScreen(samples, opts = {}) {
2885
2780
  if (samples.length === 0)
2886
2781
  throw new AitermError("screen settle test samples が空です", 2);
@@ -2924,135 +2819,90 @@ async function sendAgentPromptText(name, text) {
2924
2819
  }
2925
2820
  export async function sendInitialAgentPrompt(name, text, o = {}) {
2926
2821
  assertSessionName(name);
2927
- const waitMode = normalizeAgentLauncherWait(o.wait);
2928
2822
  const meta = loadAgentMetadata(name);
2929
- if (agentWaitLocks.has(name))
2930
- throw new AitermError(`agent session '${name}' は別の agent_done 待機中です`, 2);
2931
2823
  if (meta.initial_prompt === "done") {
2932
2824
  throw new AitermError(`agent session '${name}' の起動時 prompt は既に完了しています`, 2);
2933
2825
  }
2934
2826
  if (meta.initial_prompt === "pending" || meta.initial_prompt === "sent") {
2935
2827
  throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。初回応答完了後に再度操作してください。`, 2);
2936
2828
  }
2937
- const releaseFileLock = acquireAgentWaitFileLock(meta);
2938
- const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
2939
- const screen = o.screen ?? true;
2940
- agentWaitLocks.add(name);
2829
+ setInitialPromptState(meta, "not_sent");
2830
+ const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2831
+ if (!ready.ready) {
2832
+ return (`initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples}\n` +
2833
+ `agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。prompt は送信していません。`);
2834
+ }
2835
+ const startOffset = safeStatSize(meta.event_file);
2941
2836
  try {
2942
- setInitialPromptState(meta, "not_sent");
2943
- const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2944
- if (!ready.ready) {
2945
- return (`initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples}\n` +
2946
- `agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。prompt は送信していません。`);
2947
- }
2948
- const startOffset = safeStatSize(meta.event_file);
2949
- try {
2950
- if (meta.kind === "claude") {
2951
- prepareSendText(text, { raw: false, force: true });
2952
- reserveAnonymousClaudeTurn(meta);
2953
- }
2954
- await sendAgentPromptText(name, text);
2955
- setInitialPromptState(meta, "pending");
2956
- }
2957
- catch (e) {
2958
- setInitialPromptState(meta, "failed");
2959
- throw e;
2960
- }
2961
- if (waitMode === "none") {
2962
- return (`initial_prompt=pending vendor=${meta.kind}\n` +
2963
- `起動時 prompt を送信しました。完了後の follow-up は pty_send(wait:"agent_done") を使ってください。`);
2964
- }
2965
- const wait = await waitAgentDoneEvent(meta, startOffset, timeout);
2966
- if (wait.event) {
2967
- setInitialPromptState(meta, "done");
2837
+ if (meta.kind === "claude") {
2838
+ prepareSendText(text, { raw: false, force: true });
2839
+ reserveAnonymousClaudeTurn(meta);
2968
2840
  }
2969
- else {
2970
- setInitialPromptState(meta, "pending");
2971
- }
2972
- const settled = wait.event
2973
- ? await settleAgentDoneScreen(name, o.lines ?? 0)
2974
- : { unstable: false, samples: 0 };
2975
- const out = await readOutput(name, {
2976
- screen,
2977
- lines: o.lines ?? null,
2978
- timeout: 0,
2979
- });
2980
- writeOffset(name, safeStatSize(logpath(name)));
2981
- return out + agentDoneSuffix(wait, meta.kind) + (settled.unstable ? " [agent_done_but_screen_unstable]" : "");
2841
+ await sendAgentPromptText(name, text);
2842
+ setInitialPromptState(meta, "pending");
2982
2843
  }
2983
- finally {
2984
- agentWaitLocks.delete(name);
2985
- releaseFileLock();
2844
+ catch (e) {
2845
+ setInitialPromptState(meta, "failed");
2846
+ throw e;
2986
2847
  }
2848
+ return (`initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset}\n` +
2849
+ `起動時 prompt を送信した。完了通知は aiterm-wait --session ${name} --cursor ${startOffset}(ホストのバックグラウンドタスクとして実行し、exit を完了通知にする)、` +
2850
+ `回収は pty_read(agent_transcript:true) を使う。`);
2851
+ }
2852
+ export function isAgentSession(name) {
2853
+ assertSessionName(name);
2854
+ return tryLoadAgentMetadata(name) !== null;
2987
2855
  }
2988
- export async function sendAndWaitAgentDone(name, text, o = {}) {
2856
+ // v0.16.0: 親をブロックする wait 経路は廃止した。send は ready gate と submit 分離を内蔵した
2857
+ // dispatch として即返り、event_cursor(送信直前の event file 境界)を receipt で返す。
2858
+ // 完了通知は aiterm-wait(--cursor で境界を渡す)、回収は pty_read / claude_turn recover が担う。
2859
+ export async function dispatchAgentTurn(name, text, o = {}) {
2989
2860
  assertSessionName(name);
2990
- if (o.enter === false)
2991
- throw new AitermError('wait:"agent_done" は enter:false と併用できません', 2);
2992
- if (o.mark)
2993
- throw new AitermError('wait:"agent_done" と mark:true は併用できません', 2);
2994
- if (o.rtk)
2995
- throw new AitermError('wait:"agent_done" と rtk:true は併用できません', 2);
2996
2861
  const meta = loadAgentMetadata(name);
2997
2862
  const operationId = o.operation_id == null ? null : validateOperationId(o.operation_id);
2998
2863
  if (operationId && meta.kind !== "claude") {
2999
2864
  throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
3000
2865
  }
3001
- if (agentWaitLocks.has(name))
3002
- throw new AitermError(`agent session '${name}' は別の agent_done 待機中です`, 2);
3003
- const releaseFileLock = acquireAgentWaitFileLock(meta);
3004
- const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
3005
- const screen = o.screen ?? true;
3006
- agentWaitLocks.add(name);
3007
- try {
3008
- bindCompletedInitialPrompt(meta);
3009
- if (!meta.vendor_session_id) {
3010
- const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
3011
- if (!ready.ready) {
3012
- throw new AitermError(`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。文字列は送信していません。` +
3013
- "少し後で pty_read(screen:true) を確認し、TUI が起動済みなら再度 pty_send(wait:\"agent_done\") してください。", 2);
3014
- }
3015
- }
3016
- const startOffset = safeStatSize(meta.event_file);
3017
- if (meta.kind === "claude") {
3018
- // durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
3019
- // receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
3020
- prepareSendText(text, { raw: o.raw, force: o.force });
3021
- if (operationId) {
3022
- reserveClaudeOperation(meta, operationId);
3023
- }
3024
- else
3025
- reserveAnonymousClaudeTurn(meta);
2866
+ bindCompletedInitialPrompt(meta);
2867
+ if (!meta.vendor_session_id) {
2868
+ const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2869
+ if (!ready.ready) {
2870
+ throw new AitermError(`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。文字列は送信していません。` +
2871
+ "少し後で pty_read(screen:true) を確認し、TUI が起動済みなら再度 pty_send してください。", 2);
3026
2872
  }
3027
- send(name, text, {
3028
- enter: false,
3029
- force: o.force,
3030
- raw: o.raw,
3031
- mark: false,
3032
- rtk: false,
3033
- preserveAgentOperation: meta.kind === "claude",
3034
- });
3035
- // Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
3036
- await sleep(AGENT_SUBMIT_DELAY_MS);
3037
- sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" });
3038
- const wait = await waitAgentDoneEvent(meta, startOffset, timeout, operationId);
3039
- const settled = wait.event
3040
- ? await settleAgentDoneScreen(name, o.lines ?? 0)
3041
- : { unstable: false, samples: 0 };
3042
- const out = await readOutput(name, {
3043
- screen,
3044
- lines: o.lines ?? null,
3045
- timeout: 0,
3046
- });
3047
- writeOffset(name, safeStatSize(logpath(name)));
3048
- return out + agentDoneSuffix(wait, meta.kind, operationId) + (settled.unstable ? " [agent_done_but_screen_unstable]" : "");
3049
2873
  }
3050
- finally {
3051
- agentWaitLocks.delete(name);
3052
- releaseFileLock();
2874
+ const startOffset = safeStatSize(meta.event_file);
2875
+ if (meta.kind === "claude") {
2876
+ // durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
2877
+ // receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
2878
+ prepareSendText(text, { raw: o.raw, force: o.force });
2879
+ if (operationId) {
2880
+ reserveClaudeOperation(meta, operationId);
2881
+ }
2882
+ else
2883
+ reserveAnonymousClaudeTurn(meta);
3053
2884
  }
2885
+ send(name, text, {
2886
+ enter: false,
2887
+ force: o.force,
2888
+ raw: o.raw,
2889
+ mark: false,
2890
+ rtk: false,
2891
+ preserveAgentOperation: meta.kind === "claude",
2892
+ });
2893
+ // Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
2894
+ await sleep(AGENT_SUBMIT_DELAY_MS);
2895
+ sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" });
2896
+ return {
2897
+ schema: "aiterm.agent-dispatch.v1",
2898
+ session_id: meta.aiterm_session,
2899
+ launch_id: meta.launch_id,
2900
+ vendor: meta.kind,
2901
+ event_cursor: startOffset,
2902
+ operation_id: operationId,
2903
+ };
3054
2904
  }
3055
- export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, timeout, }) {
2905
+ export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, }) {
3056
2906
  assertSessionName(name);
3057
2907
  if (action !== "issue" && action !== "recover") {
3058
2908
  throw new AitermError('action は "issue" または "recover" を指定してください', 2);
@@ -3065,22 +2915,12 @@ export async function runClaudeOperation({ session_id: name, action, operation_i
3065
2915
  if (typeof text !== "string" || text.length === 0) {
3066
2916
  throw new AitermError("claude_turn issueには空でないtextが必要です", 2);
3067
2917
  }
3068
- const waitTimeout = timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
3069
- if (!Number.isFinite(waitTimeout) || waitTimeout < 0 || waitTimeout > 3600) {
3070
- throw new AitermError("claude_turn timeoutは0〜3600秒で指定してください", 2);
3071
- }
3072
- await sendAndWaitAgentDone(name, text, {
3073
- operation_id: operationId,
3074
- timeout: waitTimeout,
3075
- screen: false,
3076
- lines: 0,
3077
- });
2918
+ // v0.16.0: issue は dispatch-only。完了通知は aiterm-wait --operation、回収は recover が担う。
2919
+ await dispatchAgentTurn(name, text, { operation_id: operationId });
3078
2920
  }
3079
2921
  else {
3080
2922
  if (text != null)
3081
2923
  throw new AitermError("claude_turn recoverにtextは指定できません", 2);
3082
- if (timeout != null)
3083
- throw new AitermError("claude_turn recoverにtimeoutは指定できません", 2);
3084
2924
  }
3085
2925
  const inspected = inspectClaudeOperation(meta, operationId, action);
3086
2926
  if (action === "issue" && inspected.status === "pending") {
@@ -3482,37 +3322,23 @@ export function openAgent(kind, opts = {}) {
3482
3322
  `起動直後に増分 pty_read すると空/半描画になり得るので screen:true を使う。`,
3483
3323
  ];
3484
3324
  }
3485
- async function sendInitialPromptWithoutAgentDone(name, kind, text, opts = {}) {
3486
- const ready = await waitAgentTuiReadyByKind(name, kind, opts.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
3487
- if (!ready.ready) {
3488
- return (`initial_prompt=not_sent vendor=${kind} ready=false samples=${ready.samples}\n` +
3489
- `agent session '${name}' の ${agentLabel(kind)} TUI が入力受付状態になりません。prompt は送信していません。`);
3490
- }
3491
- await sendAgentPromptText(name, text);
3492
- return `initial_prompt=sent vendor=${kind}\n起動時 prompt を送信しました。完了待ちは通常の pty_read で行ってください。`;
3493
- }
3494
3325
  export async function openAgentWithInitialPrompt(kind, opts = {}) {
3495
- const waitMode = normalizeAgentLauncherWait(opts.wait);
3496
3326
  const prompt = opts.prompt ?? null;
3497
- const agentDone = !!opts.agent_done;
3498
- if (opts.launch_operation_id != null && (prompt !== null || waitMode !== "none")) {
3499
- throw new AitermError('launch_operation_idはpromptなし・wait:"none"のmanaged Claude launchだけで指定できます', 2);
3500
- }
3501
- if (waitMode === "agent_done" && !prompt) {
3502
- throw new AitermError('wait:"agent_done" は prompt 指定時だけ使えます', 2);
3503
- }
3504
- if (waitMode === "agent_done" && !agentDone) {
3505
- throw new AitermError('wait:"agent_done" には agent_done:true が必要です', 2);
3506
- }
3507
- if (!prompt) {
3508
- return openAgent(kind, opts);
3509
- }
3510
- if (kind !== "codex" && kind !== "claude") {
3511
- if (waitMode === "agent_done") {
3512
- throw new AitermError(`${agentLabel(kind)} の起動時 prompt wait は未対応です。` +
3513
- `agent_done:true で prompt なし起動後、TUI のログイン/ready を確認してから pty_send(wait:"agent_done") を使ってください。`, 2);
3514
- }
3515
- return openAgent(kind, opts);
3327
+ if (opts.launch_operation_id != null && prompt !== null) {
3328
+ throw new AitermError("launch_operation_idはpromptなしのmanaged Claude launchだけで指定できます", 2);
3329
+ }
3330
+ // v0.16.0: launcher は常に managed(Stop hook つき)で立つ。手動運転したい場合は
3331
+ // pty_open で素の PTY を開き、vendor CLI を自分で send する。
3332
+ if (!prompt || (kind !== "codex" && kind !== "claude")) {
3333
+ return openAgent(kind, {
3334
+ session_name: opts.session_name ?? null,
3335
+ model: opts.model ?? null,
3336
+ reasoning_effort: opts.reasoning_effort ?? null,
3337
+ cwd: opts.cwd ?? null,
3338
+ prompt,
3339
+ agent_done: true,
3340
+ launch_operation_id: opts.launch_operation_id ?? null,
3341
+ });
3516
3342
  }
3517
3343
  const [sid, hint] = openAgent(kind, {
3518
3344
  session_name: opts.session_name ?? null,
@@ -3520,21 +3346,13 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3520
3346
  reasoning_effort: opts.reasoning_effort ?? null,
3521
3347
  cwd: opts.cwd ?? null,
3522
3348
  prompt: null,
3523
- agent_done: agentDone,
3349
+ agent_done: true,
3524
3350
  launch_operation_id: opts.launch_operation_id ?? null,
3525
3351
  });
3526
3352
  try {
3527
- const initial = agentDone
3528
- ? await sendInitialAgentPrompt(sid, prompt, {
3529
- wait: waitMode,
3530
- timeout: opts.timeout ?? undefined,
3531
- ready_timeout: opts.ready_timeout ?? undefined,
3532
- screen: opts.screen ?? undefined,
3533
- lines: opts.lines ?? null,
3534
- })
3535
- : await sendInitialPromptWithoutAgentDone(sid, kind, prompt, {
3536
- ready_timeout: opts.ready_timeout ?? undefined,
3537
- });
3353
+ const initial = await sendInitialAgentPrompt(sid, prompt, {
3354
+ ready_timeout: opts.ready_timeout ?? undefined,
3355
+ });
3538
3356
  return [sid, `${hint}\n${initial}`];
3539
3357
  }
3540
3358
  catch (e) {
package/dist/index.js CHANGED
@@ -86,27 +86,18 @@ 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 で受ける(ポーリング不要)。" +
93
+ "結果回収は pty_read(agent_transcript:true)、Claude の durable turn は claude_turn を使う。" +
94
+ "force:true は agent session への手動介入用の素送信。",
92
95
  inputSchema: {
93
96
  session_id: z.string(),
94
97
  text: z
95
98
  .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後の同一結果回収へ使う"),
99
+ .describe("送る文字列(コマンド/prompt)。UTF-8で最大64KiB"),
100
+ enter: z.boolean().default(true).describe("末尾で Enter を送る(agent dispatch では常に submit)"),
110
101
  mark: z
111
102
  .boolean()
112
103
  .default(false)
@@ -116,28 +107,58 @@ server.registerTool("pty_send", {
116
107
  force: z
117
108
  .boolean()
118
109
  .default(false)
119
- .describe("破壊的コマンドゲートを越える。agent 起動時 prompt の完了待ち中の混入防止ガードも同時に解除する"),
110
+ .describe("破壊的コマンドゲートを越える。agent session では dispatch せず素送信する(手動介入用)"),
120
111
  rtk: z.boolean().default(false).describe("既知コマンドを rtk 形へ委譲して送る(rtk 不在なら素通し)"),
121
112
  raw: z.boolean().default(false).describe("送信前サニタイズを無効化"),
122
113
  },
123
- }, async ({ session_id, text, enter, wait, timeout, screen, lines, operation_id, mark, force, rtk, raw }) => {
114
+ outputSchema: {
115
+ schema: z.literal("aiterm.pty-send-result.v1"),
116
+ mode: z.enum(["sent", "agent_dispatch"]),
117
+ session_id: z.string(),
118
+ event_cursor: z.number().int().nullable(),
119
+ launch_id: z.string().nullable(),
120
+ vendor: z.enum(["claude", "codex", "grok", "composer"]).nullable(),
121
+ },
122
+ }, async ({ session_id, text, enter, mark, force, rtk, raw }) => {
124
123
  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
- }));
124
+ if (!force && core.isAgentSession(session_id)) {
125
+ if (enter === false)
126
+ throw new Error("agent session への dispatch は enter:false と併用できません(手動介入は force:true)");
127
+ if (mark)
128
+ throw new Error("agent session への dispatch は mark:true と併用できません");
129
+ if (rtk)
130
+ throw new Error("agent session への dispatch は rtk:true と併用できません");
131
+ const receipt = await core.dispatchAgentTurn(session_id, text, { raw });
132
+ return {
133
+ content: [
134
+ {
135
+ type: "text",
136
+ text: `dispatchした(vendor=${receipt.vendor})。完了通知: aiterm-wait --session ${receipt.session_id} --cursor ${receipt.event_cursor} を` +
137
+ "ホストのバックグラウンドタスクとして実行し exit を待つ。回収: pty_read(agent_transcript:true)",
138
+ },
139
+ ],
140
+ structuredContent: {
141
+ schema: "aiterm.pty-send-result.v1",
142
+ mode: "agent_dispatch",
143
+ session_id: receipt.session_id,
144
+ event_cursor: receipt.event_cursor,
145
+ launch_id: receipt.launch_id,
146
+ vendor: receipt.vendor,
147
+ },
148
+ };
137
149
  }
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 }));
150
+ const out = core.send(session_id, text, { enter, mark, force, rtk, raw });
151
+ return {
152
+ content: [{ type: "text", text: out }],
153
+ structuredContent: {
154
+ schema: "aiterm.pty-send-result.v1",
155
+ mode: "sent",
156
+ session_id,
157
+ event_cursor: null,
158
+ launch_id: null,
159
+ vendor: null,
160
+ },
161
+ };
141
162
  }
142
163
  catch (e) {
143
164
  return fail(e);
@@ -279,7 +300,6 @@ server.registerTool("claude_turn", {
279
300
  session_id: z.string(),
280
301
  operation_id: z.string().regex(/^sha256:[0-9a-f]{64}$/),
281
302
  text: z.string().optional().describe("issueだけに指定するbounded turn本文"),
282
- timeout: z.number().min(0).max(3600).optional().describe("issueだけに指定するStop待ち秒数"),
283
303
  },
284
304
  outputSchema: {
285
305
  schema: z.literal("aiterm.claude-operation-result.v1"),
@@ -290,14 +310,13 @@ server.registerTool("claude_turn", {
290
310
  raw_output: z.string().nullable(),
291
311
  reason: z.enum(["operation_not_found", "result_unknown"]).nullable(),
292
312
  },
293
- }, async ({ action, session_id, operation_id, text, timeout }) => {
313
+ }, async ({ action, session_id, operation_id, text }) => {
294
314
  try {
295
315
  const result = await core.runClaudeOperation({
296
316
  action,
297
317
  session_id,
298
318
  operation_id,
299
319
  text: text ?? undefined,
300
- timeout,
301
320
  });
302
321
  return {
303
322
  content: [{ type: "text", text: JSON.stringify(result) }],
@@ -324,40 +343,25 @@ const agentEffortDesc = (kind) => kind === "claude"
324
343
  : "reasoning effort(思考レベル)。low/medium/high/xhigh/max/ultra(CLI 版依存)。" +
325
344
  "ultra は max 推論+proactive 自動委譲 ON=使用量急増注意(明示要求時のみ)。省略時は端末 config/CLI 既定。";
326
345
  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
346
  const correlatedLaunchSchema = {};
338
347
  if (kind === "claude") {
339
348
  correlatedLaunchSchema.launch_operation_id = z
340
349
  .string()
341
350
  .regex(/^sha256:[0-9a-f]{64}$/)
342
351
  .optional()
343
- .describe("promptless managed launchのexact replay相関ID。session_nameとagent_done:trueが必須");
352
+ .describe("promptless managed launchのexact replay相関ID。session_name必須");
344
353
  }
345
354
  server.registerTool(toolName, {
346
355
  description: desc,
347
356
  inputSchema: {
348
- prompt: z.string().nullish().describe("起動時に渡す初手プロンプト(任意)。省略で素のTUI起動"),
357
+ prompt: z.string().nullish().describe("起動時に渡す初手プロンプト(任意)。送信後は待たずに即返る"),
349
358
  model: z.string().nullish().describe(agentModelDesc(kind)),
350
359
  // grok/composer の effort は対話 TUI で無効(headless 専用)=core 側が起動前に明示エラーで拒否。
351
360
  // codex は CLI 側の値集合が版で変わるため縛らない(core 側も同方針)。
352
361
  reasoning_effort: z.string().nullish().describe(agentEffortDesc(kind)),
353
362
  cwd: z.string().nullish().describe("作業ディレクトリ(対象リポのルート等・任意)"),
354
363
  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
364
  ...correlatedLaunchSchema,
360
- ...initialPromptWaitSchema,
361
365
  },
362
366
  outputSchema: {
363
367
  schema: z.literal("aiterm.agent-launch-result.v1"),
@@ -365,7 +369,7 @@ function registerAgentTool(toolName, kind, desc) {
365
369
  session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
366
370
  managed_completion: z.boolean(),
367
371
  },
368
- }, async ({ prompt, model, reasoning_effort, cwd, session_name, agent_done, launch_operation_id, wait, timeout, screen, lines }) => {
372
+ }, async ({ prompt, model, reasoning_effort, cwd, session_name, launch_operation_id }) => {
369
373
  try {
370
374
  const [sid, hint] = await core.openAgentWithInitialPrompt(kind, {
371
375
  prompt: prompt ?? undefined,
@@ -373,18 +377,13 @@ function registerAgentTool(toolName, kind, desc) {
373
377
  reasoning_effort: reasoning_effort ?? undefined,
374
378
  cwd: cwd ?? undefined,
375
379
  session_name: session_name ?? undefined,
376
- agent_done: agent_done ?? false,
377
380
  launch_operation_id: launch_operation_id ?? undefined,
378
- wait: wait ?? "none",
379
- timeout,
380
- screen,
381
- lines,
382
381
  });
383
382
  const structured = {
384
383
  schema: "aiterm.agent-launch-result.v1",
385
384
  provider: kind,
386
385
  session_id: sid,
387
- managed_completion: agent_done ?? false,
386
+ managed_completion: true,
388
387
  };
389
388
  return {
390
389
  content: [{ type: "text", text: `session_id: ${sid}\n${hint}` }],
@@ -397,7 +396,7 @@ function registerAgentTool(toolName, kind, desc) {
397
396
  });
398
397
  }
399
398
  registerAgentTool("claude_agent", "claude", "【Claude Code (Anthropic)】の対話エージェントTUIを永続端末に起動する。`claude -p`ではなく、" +
400
- "同じ利用者可視sessionへpty_sendで継続入力する。agent_done:trueではisolated settingsのmanaged Stop hookで完了と結果を回収する。");
399
+ "同じ利用者可視sessionへpty_sendで継続入力する。常にmanaged(isolated settingsのStop hook)で起動し、完了通知はaiterm-wait、結果はpty_read(agent_transcript)/claude_turnで回収する。");
401
400
  registerAgentTool("codex_agent", "codex", "【Codex (OpenAI)】の対話エージェント TUI を永続端末に起動する。実装・レビュー・調査を対話で回す。" +
402
401
  "起動後は pty_read で画面を読み pty_send で操作する。model / reasoning_effort を引数で指定可" +
403
402
  "(省略時は端末 config/CLI 既定を継承。実効値は起動応答に明示)。");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.15.0",
3
+ "version": "0.16.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": [