aiterm-mcp 0.16.0 → 0.18.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,19 @@
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.18.0 を 2026-07-18 に公開。** 実運用障害の還流による agent dispatch の hardening:
28
+ 全 dispatch/launch receipt に **submit 座礁観測** `submit_residue` を追加
29
+ (送信 prompt が vendor TUI の composer に未 submit のまま残存していないかを有界に観測して報告する。
30
+ 陽性証拠のみ・auto-retry なし)。agent への prompt paste は tmux **bracketed paste**(pane ごとの
31
+ negotiation)で包み、語中の文字化けと Enter 取り落としを抑制。初回 prompt 前の ready gate は
32
+ busy 表示中(esc to interrupt)の Codex/Claude を ready と数えない。
33
+ v0.16/0.17 以来、親エージェントは aiterm 上で一切ブロックしない:
34
+ agent session への send は常に非ブロック dispatch になり、完了待ちは `aiterm-wait` 一本
35
+ (exit code が receipt の outcome を映す: 0=done / 3=timeout=未完了 / 4=closed)、初回 prompt 付き
36
+ launch は structured receipt にコピペ可能な `wait_command` を含む。factory diagnostics と local
28
37
  runtime-error store は canonical dotagents config の `collection.enabled: true` が明示された
29
- 場合だけ収集し、既定OFF、network送信は行いません。npm公開、tag、CI、registry登録、registry由来install
30
- の確認は未実施です。
38
+ 場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
39
+ Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
31
40
 
32
41
  **状態:** 開発継続中 · この分野では新参で、別の形に賭けている([既存手段との比較](#既存手段との比較)参照)· 動作対象は Linux · WSL2 · macOS · Windows ネイティブ(core PTY ツール。`agent_done` は現時点では POSIX/WSL/macOS のみ)· MIT · [変更履歴](CHANGELOG.md)。
33
42
 
@@ -52,7 +61,7 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
52
61
 
53
62
  ### 2. その端末の中に他のコーディングエージェントを起動する — オーケストレーションの旗艦
54
63
 
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 を自分で起動する。
64
+ 同じ 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`、そして `submit_residue` 観測を含む(true=prompt が composer に未 submit で残存している疑い=案内に従い画面確認から復旧 / false=残存観測せず・成立の保証ではない / null=対象外)。dispatch receipt にも同じ観測が付く。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
65
 
57
66
  ```text
58
67
  codex_agent({ session_name: "codex1", cwd: "/repo",
@@ -60,7 +69,7 @@ codex_agent({ session_name: "codex1", cwd: "/repo",
60
69
  → { session_id: "codex1", … } # Codex が永続端末で稼働開始
61
70
  pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
62
71
  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)
72
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeout(未完了) / 4=closed。回収は pty_read(agent_transcript:true)
64
73
  → 操舵し、Codex の次の入力境界で返る
65
74
  ```
66
75
 
@@ -298,7 +307,7 @@ consumer は `aiterm-runtime-errors snapshot` を読み、durable ingestion 後
298
307
 
299
308
  `pty_send` は送信前に破壊的コマンド(`rm -rf /`, `mkfs`, `dd of=/dev/…`, `DROP TABLE` 等)を遮断し(`force: true` で越える)、ESC・ブラケットペースト終端などをサニタイズする。`pty_read` は既定で制御文字を無害化して返す(`raw: true` はバイトをそのまま返す)。これは**サンドボックスではなく tripwire**([既知の制約](#既知の制約バグではなく仕様)参照)。
300
309
 
301
- 1回の `pty_send` が受理する本文はUTF-8で最大64KiB。同一sessionへの送信はaiterm processをまたいで直列化し、chunk同士の混線を防ぐ。macOSでは長いPTY入力の欠落を避けるためUTF-8境界を壊さない256-byte単位でtmux pasteし、Linux/WSLでは上限内を1回でpasteする。途中chunkが失敗した場合は部分送信済みであることを明示し、自動でEnterを押さない。送信processの異常終了でlockが残った場合は送信前にfail-closedする。そのsessionを `pty_close` して作り直すか、全sessionを破棄できる場合だけ `pty_kill_all` で安全に掃除する。
310
+ 1回の `pty_send` が受理する本文はUTF-8で最大64KiB。同一sessionへの送信はaiterm processをまたいで直列化し、chunk同士の混線を防ぐ。macOSでは長いPTY入力の欠落を避けるためUTF-8境界を壊さない256-byte単位でtmux pasteし、Linux/WSLでは上限内を1回でpasteする。agent dispatch の paste はさらに tmux bracketed paste(`paste-buffer -p`)を使う: bracketed paste mode を要求している pane(vendor TUI)へは各 chunk を `ESC[200~/201~` で包んで届け、チャンク投入中のキー解釈による語中文字化け・submit 取り落としを抑える。通常の shell 送信は不変。途中chunkが失敗した場合は部分送信済みであることを明示し、自動でEnterを押さない。送信processの異常終了でlockが残った場合は送信前にfail-closedする。そのsessionを `pty_close` して作り直すか、全sessionを破棄できる場合だけ `pty_kill_all` で安全に掃除する。
302
311
 
303
312
  ## 人が覗く
304
313
 
package/README.md CHANGED
@@ -24,14 +24,24 @@
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.
27
+ **v0.18.0 was published on 2026-07-18.** Field-failure hardening for agent
28
+ dispatch: every dispatch/launch receipt now carries a **submit-strand
29
+ observation** `submit_residue` (a bounded screen poll that reports when the
30
+ sent prompt is still sitting unsubmitted in the vendor TUI's composer —
31
+ positive evidence only, no auto-retry), agent prompt pastes are wrapped in
32
+ tmux **bracketed paste** (negotiated per pane) against mid-word corruption and
33
+ dropped submits, and the pre-prompt ready gate no longer counts a busy
34
+ Codex/Claude screen ("esc to interrupt") as ready. As of v0.16/0.17 a parent
35
+ agent never blocks on aiterm: every send to an agent session is a non-blocking
36
+ dispatch, completion is one universal `aiterm-wait` waiter whose exit codes
37
+ mirror the receipt outcome (`0`=done / `3`=timeout, not finished /
38
+ `4`=closed), and a launch with an initial prompt returns a ready-made
39
+ `wait_command` in its structured receipt.
40
+ Factory diagnostics and the local runtime-error store collect only when
41
+ canonical dotagents config explicitly sets `collection.enabled: true`;
42
+ collection is off by default and performs no network I/O. It ships via
43
+ tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
44
+ Release re-registers the Official MCP Registry entry.
35
45
 
36
46
  **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
47
 
@@ -62,7 +72,7 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
62
72
 
63
73
  ### 2. Launch other coding agents into that terminal — the orchestration flagship
64
74
 
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).
75
+ 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`, a ready-made `wait_command` for the completion waiter, and a `submit_residue` observation (`true` = the prompt is likely still sitting unsubmitted in the composer — the hint explains recovery; `false` = no residue observed, not a proof of submission; `null` = not applicable). 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
76
 
67
77
  ```text
68
78
  codex_agent({ session_name: "codex1", cwd: "/repo",
@@ -71,7 +81,7 @@ codex_agent({ session_name: "codex1", cwd: "/repo",
71
81
  pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
72
82
  pty_send("codex1", "also fix the imports it broke")
73
83
  → non-blocking dispatch; receipt carries event_cursor
74
- $ aiterm-wait --session codex1 --cursor <event_cursor> # host background task; its exit = completion push
84
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # host background task; exit code 0=done, 3=timeout (not done), 4=closed
75
85
  pty_read("codex1", { agent_transcript: true }) → collect the full answer
76
86
  ```
77
87
 
@@ -303,10 +313,10 @@ When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lin
303
313
 
304
314
  ### Completion push for parent agents (`aiterm-wait`)
305
315
 
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:
316
+ 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:
307
317
 
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.
318
+ 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 — plus a `submit_residue` observation: `true` means the sent text still lingered in the composer after submit (likely stranded; inspect the screen before re-pressing Enter), `false` means no residue was observed (not a proof of submission), `null` means not applicable.
319
+ 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.
310
320
  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
321
  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
322
 
@@ -322,7 +332,7 @@ As of v0.16.0 a parent agent **never blocks** on aiterm — there is no wait par
322
332
 
323
333
  Before sending, `pty_send` blocks destructive commands (`rm -rf /`, `mkfs`, `dd of=/dev/…`, `DROP TABLE`, …) — pass `force: true` to override — and sanitizes ESC / bracketed-paste terminators. `pty_read` neutralizes control characters in what it returns by default (`raw: true` returns the bytes verbatim). This is a **tripwire, not a sandbox** (see [Known constraints](#known-constraints-by-design-not-bugs)).
324
334
 
325
- Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session are serialized across aiterm processes so chunks cannot interleave. On macOS, text is pasted through tmux in UTF-8-safe 256-byte chunks to avoid the platform PTY truncation observed with long input; Linux and WSL use one bounded paste. If a later chunk fails, aiterm reports the partial-send state and does not press Enter automatically. A lock left by a terminated sender fails closed before sending; close and recreate that session (or use `pty_kill_all` when every session is disposable) to clean it up safely.
335
+ Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session are serialized across aiterm processes so chunks cannot interleave. On macOS, text is pasted through tmux in UTF-8-safe 256-byte chunks to avoid the platform PTY truncation observed with long input; Linux and WSL use one bounded paste. Agent dispatches additionally paste with tmux bracketed paste (`paste-buffer -p`): panes that requested bracketed-paste mode (the vendor TUIs) receive each chunk wrapped in `ESC[200~/201~`, hardening prompt injection against mid-word key-interpretation corruption and dropped submits; ordinary shell sends are unchanged. If a later chunk fails, aiterm reports the partial-send state and does not press Enter automatically. A lock left by a terminated sender fails closed before sending; close and recreate that session (or use `pty_kill_all` when every session is disposable) to clean it up safely.
326
336
 
327
337
  ## A human can watch
328
338
 
@@ -1,7 +1,9 @@
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";
@@ -62,6 +64,8 @@ export function parseArgs(argv) {
62
64
  function emit(value) {
63
65
  process.stdout.write(JSON.stringify(value) + "\n");
64
66
  }
67
+ // outcome → exit code。exit status しか見えないホストでも done とそれ以外を誤読できないようにする。
68
+ const OUTCOME_EXIT_CODES = { done: 0, timeout: 3, closed: 4 };
65
69
  export async function main(argv) {
66
70
  const cmd = parseArgs(argv);
67
71
  const result = await observeAgentDone(cmd.session, {
@@ -70,6 +74,7 @@ export async function main(argv) {
70
74
  cursor: cmd.cursor,
71
75
  });
72
76
  emit(result);
77
+ process.exitCode = OUTCOME_EXIT_CODES[result.outcome];
73
78
  }
74
79
  function isDirectExecution() {
75
80
  const entry = process.argv[1];
package/dist/core.js CHANGED
@@ -56,6 +56,12 @@ const AGENT_TUI_READY_TIMEOUT_MS = 30_000;
56
56
  const AGENT_TUI_READY_POLL_MS = 500;
57
57
  const AGENT_TUI_READY_STABLE_SAMPLES = 11;
58
58
  const AGENT_TUI_READY_LINES = 45;
59
+ // submit座礁観測(dispatch後にcomposerへ送信textが残存していないかの有界チェック)
60
+ const AGENT_SUBMIT_RESIDUE_DELAY_MS = 250;
61
+ const AGENT_SUBMIT_RESIDUE_POLL_MS = 300;
62
+ const AGENT_SUBMIT_RESIDUE_MAX_SAMPLES = 5;
63
+ const AGENT_SUBMIT_RESIDUE_TAIL_CHARS = 32;
64
+ const AGENT_SUBMIT_RESIDUE_MIN_TAIL_CHARS = 8;
59
65
  const GROK_AUTH_MAX_BYTES = 64 * 1024;
60
66
  const CLAUDE_RESULT_MAX_BYTES = 4 * 1024 * 1024;
61
67
  // 出力削減(RTK の CAP 思想を移植)
@@ -936,7 +942,7 @@ export function send(name, text, o = {}) {
936
942
  // sentinelを待つため、公開上の拒否は副作用ゼロでなければならない。
937
943
  if (!o.preserveAgentOperation && managedClaudeOperation(name) !== undefined) {
938
944
  throw new AitermError("managed Claude agent sessionへの通常送信はturn境界を失うため拒否します。" +
939
- 'pty_send(wait:"agent_done")を使うか、通常対話へ切り替えるならsessionをcloseしてagent_done:falseで起動し直してください。', 2);
945
+ "pty_send(forceなし=自動dispatch)を使うか、通常対話へ切り替えるならsessionをcloseしてpty_openから手動起動し直してください。", 2);
940
946
  }
941
947
  writeLastcmd(name, text); // read rtk の reducer 分類用(書換/mark 前の素のコマンド)
942
948
  if (o.rtk)
@@ -983,6 +989,8 @@ export function send(name, text, o = {}) {
983
989
  // -r: LF→CR 置換を無効化。-Sは対応新版だけでvis(3)制御文字変換を無効化する。
984
990
  // send 自身の raw/sanitize 契約だけを真実とし、tmux 側で黙って再変換させない。
985
991
  const pasteArgs = ["paste-buffer", "-d", "-r"];
992
+ if (o.bracketedPaste)
993
+ pasteArgs.push("-p");
986
994
  if (pasteSupportsNoSanitize)
987
995
  pasteArgs.push("-S");
988
996
  pasteArgs.push("-b", bufferName, "-t", name);
@@ -1015,7 +1023,7 @@ export function sendKey(name, key, o = {}) {
1015
1023
  const k = KEYMAP[key.toLowerCase()] ?? key;
1016
1024
  if (!o.preserveAgentOperation && managedClaudeOperation(name) !== undefined && k !== "C-c") {
1017
1025
  throw new AitermError("managed Claude agent sessionではturn相関を壊さないC-cだけをpty_keyで送れます。" +
1018
- "他の対話操作はpty_send(wait:\"agent_done\")、終了はpty_closeを使ってください。", 2);
1026
+ "他の対話操作はpty_send(自動dispatch)、終了はpty_closeを使ってください。", 2);
1019
1027
  }
1020
1028
  tmux("send-keys", "-t", name, k);
1021
1029
  return `sent key ${k} to ${name}`;
@@ -2284,6 +2292,9 @@ function bindAgentVendorSession(meta, ev) {
2284
2292
  meta.vendor_session_id = ev.vendor_session_id;
2285
2293
  }
2286
2294
  }
2295
+ // 復旧案内の aiterm-wait は --cursor 0 を明示する: cursor 省略時の既定は waiter 起動時 EOF のため、
2296
+ // 案内表示〜実行の間に done event が書かれていると読み飛ばして timeout まで座る。event file は
2297
+ // per-launch 新規作成+launch_id フィルタ付き走査なので、0 起点は取りこぼしゼロかつ安全。
2287
2298
  function bindCompletedInitialPrompt(meta) {
2288
2299
  if (meta.initial_prompt !== "pending" && meta.initial_prompt !== "sent")
2289
2300
  return;
@@ -2293,7 +2304,7 @@ function bindCompletedInitialPrompt(meta) {
2293
2304
  }
2294
2305
  const size = safeStatSize(meta.event_file);
2295
2306
  if (size === 0) {
2296
- throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。初回応答完了後に再度 pty_send(wait:"agent_done") してください。`, 2);
2307
+ throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。aiterm-wait --session ${meta.aiterm_session} --cursor 0 で完了(outcome=done)を確認してから再度操作してください。`, 2);
2297
2308
  }
2298
2309
  const text = readFileRange(meta.event_file, 0, size).toString("utf8");
2299
2310
  const lines = text.split("\n");
@@ -2305,7 +2316,7 @@ function bindCompletedInitialPrompt(meta) {
2305
2316
  if (!scanned.event) {
2306
2317
  const malformed = scanned.malformedEvents ? ` malformed_events=${scanned.malformedEvents}` : "";
2307
2318
  const partial = tail.trim() ? " partial_event=true" : "";
2308
- throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了 event をまだ確認できません。初回応答完了後に再度 pty_send(wait:"agent_done") してください。${malformed}${partial}`, 2);
2319
+ throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了 event をまだ確認できません。aiterm-wait --session ${meta.aiterm_session} --cursor 0 で完了(outcome=done)を確認してから再度操作してください。${malformed}${partial}`, 2);
2309
2320
  }
2310
2321
  bindAgentVendorSession(meta, scanned.event);
2311
2322
  setInitialPromptState(meta, "done");
@@ -2426,8 +2437,11 @@ function findLatestCodexTranscript(codexHome, vendorSessionId) {
2426
2437
  visit(sessionsDir);
2427
2438
  return latestFile;
2428
2439
  }
2440
+ // 未完了系エラーの共通出口案内。pollingへ誘導せず、正規の完了待ち手段を必ず指す。
2441
+ const AGENT_WAIT_GUIDE = "完了待ちは aiterm-wait --session <session_id> のバックグラウンド実行で受ける(polling不要。" +
2442
+ "receiptのoutcome=doneを確認してから再取得)。";
2429
2443
  function transcriptUnavailable() {
2430
- throw new AitermError("transcript がまだありません。ターン完了後に再取得してください。", 2);
2444
+ throw new AitermError(`transcript がまだありません。ターン完了後に再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2431
2445
  }
2432
2446
  function transcriptNotFound(vendor) {
2433
2447
  throw new AitermError(`最終 assistant メッセージを特定できませんでした(vendor=${vendor})。screen で確認してください。`, 2);
@@ -2494,18 +2508,18 @@ export async function readAgentTranscript(name, o = {}) {
2494
2508
  const active = readClaudeOperationMarker(meta);
2495
2509
  if (active) {
2496
2510
  const label = active.operationId ? `operation ${active.operationId}` : "operation_idなしのClaude turn";
2497
- throw new AitermError(`${label} はまだ完了していません。Stop完了後に同じsessionから再取得してください。`, 2);
2511
+ throw new AitermError(`${label} はまだ完了していません。Stop完了後に同じsessionから再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2498
2512
  }
2499
2513
  }
2500
2514
  // wait timeout は「失敗」ではなく状態不明。後着した同一launchの完了eventから
2501
2515
  // vendor session をbindし、promptを再送せず結果だけ回収できるようにする。
2502
2516
  recoverAgentVendorSession(meta);
2503
2517
  if (!meta.vendor_session_id) {
2504
- throw new AitermError(`agent session '${name}' はまだターンが完了していません。agent_done 完了後に再取得してください。`, 2);
2518
+ throw new AitermError(`agent session '${name}' はまだターンが完了していません。agent_done 完了後に再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2505
2519
  }
2506
2520
  const done = latestAgentDoneEvent(meta, operationId);
2507
2521
  if (operationId && !done) {
2508
- throw new AitermError(`operation ${operationId} はまだ完了していません。同じoperation_idで後から再取得してください。`, 2);
2522
+ throw new AitermError(`operation ${operationId} はまだ完了していません。同じoperation_idで後から再取得してください。${AGENT_WAIT_GUIDE}`, 2);
2509
2523
  }
2510
2524
  const turnId = done?.turn_id ?? null;
2511
2525
  let text = "";
@@ -2646,8 +2660,10 @@ function assertInitialPromptNotPendingForSend(name, force) {
2646
2660
  if (meta.initial_prompt !== "pending" && meta.initial_prompt !== "sent")
2647
2661
  return;
2648
2662
  throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。通常 pty_send は混入防止のため送信しません。` +
2649
- `完了後に pty_send(wait:"agent_done") するか、手動介入が必要な場合だけ force:true を明示してください。`, 2);
2663
+ `aiterm-wait --session ${name} --cursor 0 で完了(outcome=done)を確認してから再度 pty_send するか、手動介入が必要な場合だけ force:true を明示してください。`, 2);
2650
2664
  }
2665
+ // aiterm-wait の exit 契約(CLI と各所の案内文で共有する正)。exit≠完了: outcome が done の時だけ完了。
2666
+ export const AITERM_WAIT_OUTCOME_NOTE = `exit 0=done / 3=timeout(既定${DEFAULT_AGENT_DONE_TIMEOUT}秒・未完了) / 4=closed。receiptのoutcomeが正で、done以外は未完了`;
2651
2667
  // 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
2652
2668
  // event fileのtail規律(未終端行保持・増分上限)はwaitAgentDoneEventと同一だが、
2653
2669
  // vendor_session_idのbind永続化を行わない点だけ意図的に異なる(waiterは観測者であって所有者でない)。
@@ -2719,6 +2735,18 @@ function isAgentTuiReady(kind, screen) {
2719
2735
  }
2720
2736
  return screen.includes("Grok Build") && /(^|\n|\s)❯/.test(screen);
2721
2737
  }
2738
+ // Codex/Claude は実行中に「(esc to interrupt)」を表示する(実機採取)。startup 側の処理
2739
+ // (MCP initialize 等)が走ったまま composer だけ描画されている画面は入力受付とみなさない。
2740
+ // Grok/Composer は busy 表示文字列の実機根拠が未採取のため対象外(誤ブロックで起動不能にしない)。
2741
+ const AGENT_TUI_BUSY_KINDS = new Set(["codex", "claude"]);
2742
+ const AGENT_TUI_BUSY_RE = /esc to interrupt/i;
2743
+ // ready gate 用: 入力欄マーカーがあっても busy 表示中は ready と数えない。
2744
+ // frontend 推定(inferAgentFrontend)は「agent TUI が前面か」を見るだけなので isAgentTuiReady のまま。
2745
+ function isAgentTuiIdleReady(kind, screen) {
2746
+ if (!isAgentTuiReady(kind, screen))
2747
+ return false;
2748
+ return !(AGENT_TUI_BUSY_KINDS.has(kind) && AGENT_TUI_BUSY_RE.test(screen));
2749
+ }
2722
2750
  async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
2723
2751
  const timeoutMs = opts.timeoutMs ?? AGENT_TUI_READY_TIMEOUT_MS;
2724
2752
  const pollMs = opts.pollMs ?? AGENT_TUI_READY_POLL_MS;
@@ -2730,7 +2758,7 @@ async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
2730
2758
  for (;;) {
2731
2759
  lastScreen = sample();
2732
2760
  samples++;
2733
- if (isAgentTuiReady(kind, lastScreen)) {
2761
+ if (isAgentTuiIdleReady(kind, lastScreen)) {
2734
2762
  readyStreak++;
2735
2763
  if (readyStreak >= stableSamples)
2736
2764
  return { ready: true, samples, lastScreen };
@@ -2749,6 +2777,68 @@ async function waitAgentTuiReady(name, meta, timeoutMs = AGENT_TUI_READY_TIMEOUT
2749
2777
  async function waitAgentTuiReadyByKind(name, kind, timeoutMs = AGENT_TUI_READY_TIMEOUT_MS) {
2750
2778
  return waitAgentTuiReadyImpl(kind, () => captureScreen(name, AGENT_TUI_READY_LINES), sleep, { timeoutMs });
2751
2779
  }
2780
+ function normalizeResidueText(s) {
2781
+ return s.replace(/\s+/g, "");
2782
+ }
2783
+ function agentSubmitResidueTail(text) {
2784
+ // 行単位でなく text 全体の正規化末尾から取る: 最終行が短い prompt(「以上」等の締め行)でも
2785
+ // 直前行の内容を含む末尾 32 codepoint で観測できる。composer は末尾(カーソル位置)を表示し、
2786
+ // submit 済みの transcript echo は長文では先頭側を表示するため、末尾一致は座礁側に偏る。
2787
+ const cps = [...normalizeResidueText(text)];
2788
+ if (cps.length < AGENT_SUBMIT_RESIDUE_MIN_TAIL_CHARS)
2789
+ return null;
2790
+ return cps.slice(-AGENT_SUBMIT_RESIDUE_TAIL_CHARS).join("");
2791
+ }
2792
+ function agentSubmitResidueOnScreen(kind, screen, tail) {
2793
+ const lines = screen.split("\n");
2794
+ // 入力欄マーカーは ready 判定と同じ記号を行頭基準で探す。submit 済みの transcript echo は
2795
+ // マーカー行より上に出るため、最後のマーカー行以降だけを composer 領域として見る。
2796
+ const markerRe = kind === "codex" ? /^\s*[›>]/ : kind === "claude" ? /^\s*❯/ : /(^|\s)❯/;
2797
+ let markerIdx = -1;
2798
+ for (let i = lines.length - 1; i >= 0; i--) {
2799
+ if (markerRe.test(lines[i])) {
2800
+ markerIdx = i;
2801
+ break;
2802
+ }
2803
+ }
2804
+ if (markerIdx < 0)
2805
+ return null;
2806
+ return normalizeResidueText(lines.slice(markerIdx).join("")).includes(tail);
2807
+ }
2808
+ async function detectAgentSubmitResidueImpl(kind, text, sample, sleepFn, opts = {}) {
2809
+ const tail = agentSubmitResidueTail(text);
2810
+ if (!tail)
2811
+ return { residue: null, samples: 0 };
2812
+ const delayMs = opts.delayMs ?? AGENT_SUBMIT_RESIDUE_DELAY_MS;
2813
+ const pollMs = opts.pollMs ?? AGENT_SUBMIT_RESIDUE_POLL_MS;
2814
+ const maxSamples = opts.maxSamples ?? AGENT_SUBMIT_RESIDUE_MAX_SAMPLES;
2815
+ if (delayMs > 0)
2816
+ await sleepFn(delayMs);
2817
+ let samples = 0;
2818
+ let last = null;
2819
+ for (let i = 0; i < maxSamples; i++) {
2820
+ last = agentSubmitResidueOnScreen(kind, sample(), tail);
2821
+ samples++;
2822
+ // 残存が消えた(または判定不能になった)時点で確定。true だけは描画遅延と区別するため
2823
+ // 全サンプル持続した場合にのみ報告する。
2824
+ if (last !== true)
2825
+ return { residue: last, samples };
2826
+ if (i < maxSamples - 1)
2827
+ await sleepFn(pollMs);
2828
+ }
2829
+ return { residue: true, samples };
2830
+ }
2831
+ async function detectAgentSubmitResidue(name, kind, text) {
2832
+ return detectAgentSubmitResidueImpl(kind, text, () => captureScreen(name, AGENT_TUI_READY_LINES), sleep);
2833
+ }
2834
+ export function agentSubmitResidueWarning(name, residue) {
2835
+ if (residue !== true)
2836
+ return "";
2837
+ return (`\n警告: submit_residue=true=送信 text が composer に残存しており submit 未成立の疑いがある` +
2838
+ `(実行中 turn への queued message が表示されている可能性もある)。` +
2839
+ `pty_read(${name}, screen:true) で状態を確認してから、座礁していれば pty_key(${name}, "Enter") で再 submit、` +
2840
+ `破棄するなら pty_key(${name}, "Escape") を使う。盲目的に Enter を送らない(queued だった場合の二重 submit 防止)。`);
2841
+ }
2752
2842
  async function settleAgentDoneScreenImpl(sample, sleepFn, opts = {}) {
2753
2843
  const minDelayMs = opts.minDelayMs ?? AGENT_DONE_SETTLE_MIN_MS;
2754
2844
  const pollMs = opts.pollMs ?? AGENT_DONE_SCREEN_SETTLE_POLL_MS;
@@ -2799,6 +2889,17 @@ export async function __testWaitAgentTuiReady(kind, samples, opts = {}) {
2799
2889
  }, opts);
2800
2890
  return { ...result, sleeps };
2801
2891
  }
2892
+ export function __testIsAgentTuiIdleReady(kind, screen) {
2893
+ return isAgentTuiIdleReady(kind, screen);
2894
+ }
2895
+ export async function __testDetectAgentSubmitResidue(kind, text, samples, opts = {}) {
2896
+ let i = 0;
2897
+ const sleeps = [];
2898
+ const result = await detectAgentSubmitResidueImpl(kind, text, () => samples[Math.min(i++, samples.length - 1)], async (ms) => {
2899
+ sleeps.push(ms);
2900
+ }, opts);
2901
+ return { ...result, sleeps };
2902
+ }
2802
2903
  export function __testSetAgentTuiReadyStableSamples(value) {
2803
2904
  if (value !== null && (!Number.isInteger(value) || value < 1 || value > 1000)) {
2804
2905
  throw new AitermError("test ready stable samplesが不正です", 2);
@@ -2813,6 +2914,7 @@ async function sendAgentPromptText(name, text) {
2813
2914
  mark: false,
2814
2915
  rtk: false,
2815
2916
  preserveAgentOperation: true,
2917
+ bracketedPaste: true,
2816
2918
  });
2817
2919
  await sleep(AGENT_SUBMIT_DELAY_MS);
2818
2920
  sendKey(name, "Enter", { preserveAgentOperation: true });
@@ -2829,8 +2931,12 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2829
2931
  setInitialPromptState(meta, "not_sent");
2830
2932
  const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
2831
2933
  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 は送信していません。`);
2934
+ return {
2935
+ text: `initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples}\n` +
2936
+ `agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。prompt は送信していません。`,
2937
+ event_cursor: null,
2938
+ submit_residue: null,
2939
+ };
2834
2940
  }
2835
2941
  const startOffset = safeStatSize(meta.event_file);
2836
2942
  try {
@@ -2845,9 +2951,15 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
2845
2951
  setInitialPromptState(meta, "failed");
2846
2952
  throw e;
2847
2953
  }
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) を使う。`);
2954
+ const residue = await detectAgentSubmitResidue(name, meta.kind, text);
2955
+ return {
2956
+ text: `initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset}\n` +
2957
+ `起動時 prompt を送信した。完了通知は aiterm-wait --session ${name} --cursor ${startOffset} をホストのバックグラウンドタスクとして実行し、` +
2958
+ `exit 時に receipt の outcome で判定する(${AITERM_WAIT_OUTCOME_NOTE})。回収は pty_read(agent_transcript:true) を使う。` +
2959
+ agentSubmitResidueWarning(name, residue.residue),
2960
+ event_cursor: startOffset,
2961
+ submit_residue: residue.residue,
2962
+ };
2851
2963
  }
2852
2964
  export function isAgentSession(name) {
2853
2965
  assertSessionName(name);
@@ -2889,10 +3001,12 @@ export async function dispatchAgentTurn(name, text, o = {}) {
2889
3001
  mark: false,
2890
3002
  rtk: false,
2891
3003
  preserveAgentOperation: meta.kind === "claude",
3004
+ bracketedPaste: true,
2892
3005
  });
2893
3006
  // Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
2894
3007
  await sleep(AGENT_SUBMIT_DELAY_MS);
2895
3008
  sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" });
3009
+ const residue = await detectAgentSubmitResidue(name, meta.kind, text);
2896
3010
  return {
2897
3011
  schema: "aiterm.agent-dispatch.v1",
2898
3012
  session_id: meta.aiterm_session,
@@ -2900,6 +3014,7 @@ export async function dispatchAgentTurn(name, text, o = {}) {
2900
3014
  vendor: meta.kind,
2901
3015
  event_cursor: startOffset,
2902
3016
  operation_id: operationId,
3017
+ submit_residue: residue.residue,
2903
3018
  };
2904
3019
  }
2905
3020
  export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, }) {
@@ -2911,22 +3026,25 @@ export async function runClaudeOperation({ session_id: name, action, operation_i
2911
3026
  const meta = loadAgentMetadata(name);
2912
3027
  if (meta.kind !== "claude")
2913
3028
  throw new AitermError("claude_turnはmanaged Claude agent sessionだけで使用できます", 2);
3029
+ let dispatchReceipt = null;
2914
3030
  if (action === "issue") {
2915
3031
  if (typeof text !== "string" || text.length === 0) {
2916
3032
  throw new AitermError("claude_turn issueには空でないtextが必要です", 2);
2917
3033
  }
2918
3034
  // v0.16.0: issue は dispatch-only。完了通知は aiterm-wait --operation、回収は recover が担う。
2919
- await dispatchAgentTurn(name, text, { operation_id: operationId });
3035
+ dispatchReceipt = await dispatchAgentTurn(name, text, { operation_id: operationId });
2920
3036
  }
2921
3037
  else {
2922
3038
  if (text != null)
2923
3039
  throw new AitermError("claude_turn recoverにtextは指定できません", 2);
2924
3040
  }
2925
3041
  const inspected = inspectClaudeOperation(meta, operationId, action);
2926
- if (action === "issue" && inspected.status === "pending") {
2927
- return { ...inspected, status: "accepted" };
3042
+ // issue は dispatch の submit 座礁観測を捨てずに返す(観測を払ったのに信号を返さない契約矛盾を作らない)。
3043
+ const result = dispatchReceipt ? { ...inspected, submit_residue: dispatchReceipt.submit_residue } : inspected;
3044
+ if (action === "issue" && result.status === "pending") {
3045
+ return { ...result, status: "accepted" };
2928
3046
  }
2929
- return inspected;
3047
+ return result;
2930
3048
  }
2931
3049
  function inspectClaudeOperation(meta, operationId, action) {
2932
3050
  const base = {
@@ -2934,6 +3052,7 @@ function inspectClaudeOperation(meta, operationId, action) {
2934
3052
  action,
2935
3053
  session_id: meta.aiterm_session,
2936
3054
  operation_id: operationId,
3055
+ submit_residue: null,
2937
3056
  };
2938
3057
  const active = readClaudeOperationMarker(meta);
2939
3058
  if (active) {
@@ -3310,7 +3429,7 @@ export function openAgent(kind, opts = {}) {
3310
3429
  }
3311
3430
  const driveHint = agentDone && kind === "claude"
3312
3431
  ? `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
3313
- `turnはpty_send(${sid}, "...", wait:"agent_done")で送る。中断はpty_key(${sid}, "C-c")、` +
3432
+ `turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch・完了通知はaiterm-wait)。中断はpty_key(${sid}, "C-c")、` +
3314
3433
  `Stopが来ない場合の解除はpty_close(${sid})を使う。`
3315
3434
  : `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
3316
3435
  `pty_send(${sid}, "...") で入力・pty_key(${sid}, "Enter"/"Up"/"C-c" 等) で操作する(対話)。`;
@@ -3329,8 +3448,10 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3329
3448
  }
3330
3449
  // v0.16.0: launcher は常に managed(Stop hook つき)で立つ。手動運転したい場合は
3331
3450
  // pty_open で素の PTY を開き、vendor CLI を自分で send する。
3451
+ // 第3要素は「起動時点でturnが走っているか」の event_cursor: Grok/Composer の argv prompt は
3452
+ // event file 新規作成直後の起動=境界0、prompt なしの起動は turn なし=null。
3332
3453
  if (!prompt || (kind !== "codex" && kind !== "claude")) {
3333
- return openAgent(kind, {
3454
+ const [sid, hint] = openAgent(kind, {
3334
3455
  session_name: opts.session_name ?? null,
3335
3456
  model: opts.model ?? null,
3336
3457
  reasoning_effort: opts.reasoning_effort ?? null,
@@ -3339,6 +3460,8 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3339
3460
  agent_done: true,
3340
3461
  launch_operation_id: opts.launch_operation_id ?? null,
3341
3462
  });
3463
+ // argv prompt(grok/composer)は composer を経由しないため submit 座礁観測の対象外。
3464
+ return [sid, hint, prompt ? 0 : null, null];
3342
3465
  }
3343
3466
  const [sid, hint] = openAgent(kind, {
3344
3467
  session_name: opts.session_name ?? null,
@@ -3353,7 +3476,7 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3353
3476
  const initial = await sendInitialAgentPrompt(sid, prompt, {
3354
3477
  ready_timeout: opts.ready_timeout ?? undefined,
3355
3478
  });
3356
- return [sid, `${hint}\n${initial}`];
3479
+ return [sid, `${hint}\n${initial.text}`, initial.event_cursor, initial.submit_residue];
3357
3480
  }
3358
3481
  catch (e) {
3359
3482
  const code = e instanceof AitermError ? e.code : 1;
package/dist/index.js CHANGED
@@ -89,7 +89,8 @@ server.registerTool("pty_send", {
89
89
  description: "セッションへテキストを送る。通常PTYへは送信のみ(出力は pty_read で取得)。" +
90
90
  "agent session(launcher起動)への send は自動で dispatch になる: TUI の ready gate と submit 分離を通して即返り、" +
91
91
  "receipt の event_cursor を返す=親はブロックしない。完了通知は `aiterm-wait --session <id> --cursor <event_cursor>` を" +
92
- "ホストのバックグラウンドタスクとして実行し、その exit で受ける(ポーリング不要)。" +
92
+ "ホストのバックグラウンドタスクとして実行し、exit時にreceiptのoutcomeで判定する" +
93
+ `(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
93
94
  "結果回収は pty_read(agent_transcript:true)、Claude の durable turn は claude_turn を使う。" +
94
95
  "force:true は agent session への手動介入用の素送信。",
95
96
  inputSchema: {
@@ -118,6 +119,9 @@ server.registerTool("pty_send", {
118
119
  event_cursor: z.number().int().nullable(),
119
120
  launch_id: z.string().nullable(),
120
121
  vendor: z.enum(["claude", "codex", "grok", "composer"]).nullable(),
122
+ // dispatch後のsubmit座礁観測(additive)。true=composerに送信textの残存を確認(submit未成立の疑い)/
123
+ // false=残存を観測せず(成立の保証ではない)/ null=通常送信・判定不能。
124
+ submit_residue: z.boolean().nullable(),
121
125
  },
122
126
  }, async ({ session_id, text, enter, mark, force, rtk, raw }) => {
123
127
  try {
@@ -134,7 +138,9 @@ server.registerTool("pty_send", {
134
138
  {
135
139
  type: "text",
136
140
  text: `dispatchした(vendor=${receipt.vendor})。完了通知: aiterm-wait --session ${receipt.session_id} --cursor ${receipt.event_cursor} を` +
137
- "ホストのバックグラウンドタスクとして実行し exit を待つ。回収: pty_read(agent_transcript:true)",
141
+ `ホストのバックグラウンドタスクとして実行し、exit時にreceiptのoutcomeで判定(${core.AITERM_WAIT_OUTCOME_NOTE})。` +
142
+ "回収: pty_read(agent_transcript:true)" +
143
+ core.agentSubmitResidueWarning(receipt.session_id, receipt.submit_residue),
138
144
  },
139
145
  ],
140
146
  structuredContent: {
@@ -144,6 +150,7 @@ server.registerTool("pty_send", {
144
150
  event_cursor: receipt.event_cursor,
145
151
  launch_id: receipt.launch_id,
146
152
  vendor: receipt.vendor,
153
+ submit_residue: receipt.submit_residue,
147
154
  },
148
155
  };
149
156
  }
@@ -157,6 +164,7 @@ server.registerTool("pty_send", {
157
164
  event_cursor: null,
158
165
  launch_id: null,
159
166
  vendor: null,
167
+ submit_residue: null,
160
168
  },
161
169
  };
162
170
  }
@@ -309,6 +317,9 @@ server.registerTool("claude_turn", {
309
317
  operation_id: z.string().regex(/^sha256:[0-9a-f]{64}$/),
310
318
  raw_output: z.string().nullable(),
311
319
  reason: z.enum(["operation_not_found", "result_unknown"]).nullable(),
320
+ // issue時のみdispatch由来のsubmit座礁観測(additive)。true=composerに残存を確認(submit未成立の疑い)/
321
+ // false=残存を観測せず(成立の保証ではない)/ recover等はnull。
322
+ submit_residue: z.boolean().nullable(),
312
323
  },
313
324
  }, async ({ action, session_id, operation_id, text }) => {
314
325
  try {
@@ -342,6 +353,13 @@ const agentEffortDesc = (kind) => kind === "claude"
342
353
  "composer は effort 自体非対応)。指定すると起動前にエラーを返す"
343
354
  : "reasoning effort(思考レベル)。low/medium/high/xhigh/max/ultra(CLI 版依存)。" +
344
355
  "ultra は max 推論+proactive 自動委譲 ON=使用量急増注意(明示要求時のみ)。省略時は端末 config/CLI 既定。";
356
+ // 全launcher共通の完了受信ガイド。launch応答のwait_command(起動時promptあり時)/pty_send dispatchの
357
+ // event_cursorから組んだaiterm-waitをホストのバックグラウンドタスクとして1本実行し、exit時にreceiptの
358
+ // outcomeで判定する。
359
+ const agentCompletionDesc = `完了通知は起動応答の wait_command(初回prompt時)または pty_send dispatch 後の ` +
360
+ `aiterm-wait --session <id> --cursor <event_cursor> をホストのバックグラウンドタスクとして実行し、` +
361
+ `exit時にreceiptのoutcomeで判定する(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
362
+ `結果回収は pty_read(agent_transcript:true)。`;
345
363
  function registerAgentTool(toolName, kind, desc) {
346
364
  const correlatedLaunchSchema = {};
347
365
  if (kind === "claude") {
@@ -368,10 +386,17 @@ function registerAgentTool(toolName, kind, desc) {
368
386
  provider: z.literal(kind),
369
387
  session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
370
388
  managed_completion: z.boolean(),
389
+ // 起動時 prompt でturnが走っている時だけ非null(additive拡張)。wait_command はそのままホストの
390
+ // バックグラウンドタスクとして実行できる完了待ちコマンド。
391
+ event_cursor: z.number().int().nullable(),
392
+ wait_command: z.string().nullable(),
393
+ // 初回prompt dispatch後のsubmit座礁観測(additive)。true=composerに残存を確認(submit未成立の疑い)/
394
+ // false=残存を観測せず(submit成立の保証ではない)/ null=promptなし・argv prompt・判定不能。
395
+ submit_residue: z.boolean().nullable(),
371
396
  },
372
397
  }, async ({ prompt, model, reasoning_effort, cwd, session_name, launch_operation_id }) => {
373
398
  try {
374
- const [sid, hint] = await core.openAgentWithInitialPrompt(kind, {
399
+ const [sid, hint, eventCursor, submitResidue] = await core.openAgentWithInitialPrompt(kind, {
375
400
  prompt: prompt ?? undefined,
376
401
  model: model ?? undefined,
377
402
  reasoning_effort: reasoning_effort ?? undefined,
@@ -384,6 +409,9 @@ function registerAgentTool(toolName, kind, desc) {
384
409
  provider: kind,
385
410
  session_id: sid,
386
411
  managed_completion: true,
412
+ event_cursor: eventCursor,
413
+ wait_command: eventCursor === null ? null : `aiterm-wait --session ${sid} --cursor ${eventCursor}`,
414
+ submit_residue: submitResidue,
387
415
  };
388
416
  return {
389
417
  content: [{ type: "text", text: `session_id: ${sid}\n${hint}` }],
@@ -396,14 +424,22 @@ function registerAgentTool(toolName, kind, desc) {
396
424
  });
397
425
  }
398
426
  registerAgentTool("claude_agent", "claude", "【Claude Code (Anthropic)】の対話エージェントTUIを永続端末に起動する。`claude -p`ではなく、" +
399
- "同じ利用者可視sessionへpty_sendで継続入力する。常にmanaged(isolated settingsのStop hook)で起動し、完了通知はaiterm-wait、結果はpty_read(agent_transcript)/claude_turnで回収する。");
427
+ "同じ利用者可視sessionへpty_sendで継続入力する。常にmanaged(isolated settingsのStop hook)で起動する。" +
428
+ agentCompletionDesc +
429
+ "Claude の durable turn は claude_turn でも回収できる。");
400
430
  registerAgentTool("codex_agent", "codex", "【Codex (OpenAI)】の対話エージェント TUI を永続端末に起動する。実装・レビュー・調査を対話で回す。" +
401
- "起動後は pty_read で画面を読み pty_send で操作する。model / reasoning_effort を引数で指定可" +
431
+ "turn は pty_send で送る(自動で非ブロック dispatch になる)。" +
432
+ agentCompletionDesc +
433
+ "model / reasoning_effort を引数で指定可" +
402
434
  "(省略時は端末 config/CLI 既定を継承。実効値は起動応答に明示)。");
403
435
  registerAgentTool("grok_agent", "grok", "【Grok Build の Grok モデル (既定 grok-4.5)】の対話エージェント TUI を永続端末に起動する。" +
404
- "起動後は pty_read/pty_send で対話操作。model を引数で指定可。reasoning_effort は対話 TUI 非対応(指定はエラー)。");
436
+ "turn は pty_send で送る(自動で非ブロック dispatch になる)。" +
437
+ agentCompletionDesc +
438
+ "model を引数で指定可。reasoning_effort は対話 TUI 非対応(指定はエラー)。");
405
439
  registerAgentTool("composer_agent", "composer", "【Grok Build の Composer モデル (既定 grok-composer-2.5-fast)】の対話エージェント TUI を永続端末に起動する。" +
406
- "起動後は pty_read/pty_send で対話操作。model を引数で指定可。reasoning_effort は非対応(指定はエラー)。");
440
+ "turn は pty_send で送る(自動で非ブロック dispatch になる)。" +
441
+ agentCompletionDesc +
442
+ "model を引数で指定可。reasoning_effort は非対応(指定はエラー)。");
407
443
  async function main() {
408
444
  const transport = new StdioServerTransport();
409
445
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.16.0",
3
+ "version": "0.18.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": [