aiterm-mcp 0.33.0 → 0.34.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/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.34.0] - 2026-09-10
11
+
12
+ ### Added
13
+
14
+ - Codex親への子の回答自動配送を追加した。起動・通常dispatch・Claude durable turnの完了をAitermが観測し、回答本文を公式受信キューへ送る。親の待機コマンドと回答回収、子への送信指示は不要になった。
15
+ - 配送IDと状態をreceipt/`pty_observe`へ追加した。再接続後は未送信の記録を再開し、送信結果が不明な場合は本文を保持して自動再送しない。
16
+ - `aiterm-setup`はCodexの公式queue入口を確認する。各dispatchでMCP要求の親threadを確認し、未対応の受信口へ子を送らない。
17
+
18
+ ### Compatibility
19
+
20
+ - Codex親は公式の`_meta.threadId`と`thread/queue` APIを提供する環境が必要。Codex CLI 0.154.0で確認した。native sub-agentを親とする外部queue入力はCodexの制約により未対応。
21
+ - Claude等の親の既存waiter契約は維持する。旧版へ戻すと新しい配送記録は処理されないが、既存PTY/harness stateの形式は変わらない。
22
+
23
+ ## [0.33.1] - 2026-09-09
24
+
25
+ ### Fixed
26
+
27
+ - macOS・Linux上のAitermからSSH先のPowerShellへ`pty_send(mark:true)`するとPOSIXの`printf`を送っていた不具合を修理した。現在のPowerShell promptで方言を判定し、成功・失敗の完了マーカーを生成する。古いpromptや入力途中の行は使わない。
28
+
10
29
  ## [0.33.0] - 2026-09-09
11
30
 
12
31
  ### Added
@@ -1598,7 +1617,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1598
1617
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1599
1618
  provenance.
1600
1619
 
1601
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.33.0...HEAD
1620
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.34.0...HEAD
1621
+ [0.34.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.33.1...v0.34.0
1622
+ [0.33.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.33.0...v0.33.1
1602
1623
  [0.33.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.32.0...v0.33.0
1603
1624
  [0.32.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.31.2...v0.32.0
1604
1625
  [0.31.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.31.1...v0.31.2
package/README.ja.md CHANGED
@@ -163,7 +163,7 @@ v0.20では、待たずに一度だけ観測する
163
163
  `running`(exit 5)で返すようにしました。v0.19系では相関済みClaude approval中継を追加し、
164
164
  複数行shell配送を維持し、native Windowsのfactory diagnosticsを拡張しました。
165
165
  v0.16/0.17以来、親エージェントはaiterm上で一切ブロックしません:
166
- agent session への send は常に非ブロック dispatch になり、完了待ちは `aiterm-wait` 一本
166
+ agent session への send は常に非ブロック dispatch になり、Codex親には回答本文を自動配送し、それ以外の親の完了待ちは `aiterm-wait`
167
167
  (exit code が receipt の outcome を映す: 0=done / 3=timeout=未完了 /
168
168
  4=closed / 5=running=待たない観測)、初回 prompt 付き
169
169
  launch は structured receipt にコピペ可能な `wait_command` を含む。factory diagnostics と local
@@ -171,7 +171,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
171
171
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
172
172
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
173
173
 
174
- **状態:** 開発継続中 · 現行公開版 **v0.33.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
174
+ **状態:** 開発継続中 · 現行公開版 **v0.34.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
175
175
 
176
176
  ### 更新と巻き戻し
177
177
 
@@ -205,7 +205,7 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
205
205
 
206
206
  同じprimitiveが別エージェントのTUIを宿す。`agent_launch`の`harness`はagent loop・認証・hook・session・transcriptを所有する実行基盤、`model`は独立した選択。起動processは直接CLIと同じproject/user環境を使い、通常config、MCP、plugin、skill、permission、trust、memory、historyをcopy・filter・置換しない。
207
207
 
208
- `aiterm.agent-launch-result.v1`は正規`harness`を返し、旧`provider`は互換fieldとして残す。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`のagent行にも載り、旧vendor/provider/agent fieldは互換用に残る。Codexは通常rollout、Grok CLIは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcript末尾の`turn_ended`を完了正本に使う。`pty_send`は非ブロックdispatchで、vendor別完了境界を表すopaqueな整数`event_cursor`を返し、完了通知は`aiterm-wait`を親のターンを塞がない別processで受ける。Cursorのsubmitキーはadapterが現行CLIのextended keyboard protocolへ変換する。送信textがCursorのcomposerへ残った場合は成功receiptを返さず失敗する。
208
+ `aiterm.agent-launch-result.v1`は正規`harness`を返し、旧`provider`は互換fieldとして残す。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`のagent行にも載り、旧vendor/provider/agent fieldは互換用に残る。Codexは通常rollout、Grok CLIは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcript末尾の`turn_ended`を完了正本に使う。`pty_send`は非ブロックdispatchで、vendor別完了境界を表すopaqueな整数`event_cursor`を返し、Codex親には回答本文を公式受信キューへ自動配送する。それ以外の親は`aiterm-wait`を親のターンを塞がない別processで受ける。Cursorのsubmitキーはadapterが現行CLIのextended keyboard protocolへ変換する。送信textがCursorのcomposerへ残った場合は成功receiptを返さず失敗する。
209
209
 
210
210
  `agent_launch`・`pty_send`(agent dispatch)・`agent_steer`は任意の`image`(画像ファイルの絶対パスの配列。png/jpg/jpeg/gif/webp)を受ける。aitermが本文末尾へ添付行を付け、どのharnessも自分のfile読取toolでそのpathを画像として開く。呼出し側はharness別の添付手順を覚えない。不正なpathは送信前に拒否する。
211
211
 
@@ -225,6 +225,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
225
225
  → { session_id: "codex1", … } # Codex が永続端末で稼働開始
226
226
  pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
227
227
  pty_send("codex1", "also fix the imports it broke") # 非ブロックdispatch=event_cursor入りreceipt
228
+ # Codex親には回答が自動で届く。それ以外の親:
228
229
  $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeout(未完了) / 4=closed / 7=error(APIエラー等でturn打ち切り)。回収は pty_read(agent_transcript:true)
229
230
  → 操舵し、Codex の次の入力境界で返る
230
231
  ```
@@ -509,8 +510,19 @@ handoff contextを前置きできる。この任意経路は`throughline >= 0.9.
509
510
 
510
511
  ### 完了検出(5 層)
511
512
 
513
+ SSH先がPowerShellの場合、`mark:true`は現在の標準`PS ...>`プロンプトから方言を判定する。Aiterm自身がmacOS・Linux上でもPowerShell構文を送り、過去の出力に残ったプロンプトは判定に使わない。
514
+
512
515
  `pty_read({ wait: true })`は通常PTYを、process終了/`mark:true` sentinel/`until`一致/shell復帰を伴う出力静止/timeoutの5層で判定する。agent sessionは第6の正確な層を使い、Codexは通常rollout、Grokは通常session event、Claudeはlaunch相関Stop event、Cursorは通常agent transcriptの`turn_ended`を`aiterm-wait --cursor`が観測する。親はブロックもポーリングもしない。
513
516
 
517
+ ### Codex親への回答自動配送
518
+
519
+ Codexから子を起動・通常dispatchした後は、別作業へ進むか親のturnを終了するだけでよい。Aitermが完了を観測し、加工前の回答を保存して、依頼元Codexの公式受信キューへ送る。親はidleになった後に回答を処理する。waiter、`pty_read`による回答回収、子への送信指示は不要。Desktop固有の接続を使わず、CLIでも同じ経路になる。子は全対応harnessから選べる。Claude等の親は既存のwaiter経路を使う。
520
+
521
+ 自動配送時はreceiptに`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はキューの受付済みであり、modelの読了ではない。MCP再接続後は未送信の記録を再開し、送信中に接続が切れて結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
522
+
523
+ CodexにはMCPの`_meta.threadId`と公式`thread/queue` APIが必要で、Codex CLI 0.154.0で確認している。`aiterm-setup`はインストールされた公式queue入口を確認し、各dispatchでは実際の親threadの受入可否を確認する。Codexのnative sub-agentは外部からのqueue入力を拒否するため、自動配送の親としては未対応。
524
+
525
+
514
526
  ### トークン削減
515
527
 
516
528
  - `pty_read` は既定で制御文字除去・連続重複圧縮・head+tail 折りたたみ(+復元ヒント・メタ併記)をかける。
package/README.md CHANGED
@@ -176,8 +176,8 @@ a non-blocking `aiterm-wait --timeout 0` observation (`running`, exit 5) from a
176
176
  wait. The v0.19 line added the correlated Claude approval relay,
177
177
  preserved multiline shell delivery, and extended factory diagnostics on native
178
178
  Windows. As of v0.16/0.17 a parent agent never blocks on aiterm:
179
- every send to an agent session is a non-blocking dispatch, completion is one
180
- universal `aiterm-wait` waiter whose exit codes mirror the receipt outcome
179
+ every send to an agent session is a non-blocking dispatch. Codex parents now receive the complete answer automatically; other parents use the
180
+ `aiterm-wait` waiter whose exit codes mirror the receipt outcome
181
181
  (`0`=done / `3`=timeout, not finished / `4`=closed / `5`=running for a
182
182
  zero-time observation), and a launch with an
183
183
  initial prompt returns a ready-made `wait_command` in its structured receipt.
@@ -187,7 +187,7 @@ collection is off by default and performs no network I/O. It ships via
187
187
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
188
188
  Release re-registers the Official MCP Registry entry.
189
189
 
190
- **Status:** actively maintained · current public release **v0.33.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
190
+ **Status:** actively maintained · current public release **v0.34.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
191
191
 
192
192
  ### Update and rollback
193
193
 
@@ -227,7 +227,7 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
227
227
 
228
228
  The same primitive hosts another agent's TUI. `agent_launch` starts a selected execution harness inside a fresh persistent terminal and returns a `session_id`. `harness` names the component that owns the agent loop, authentication, hooks, session, and transcript; `model` remains an independent choice. The launched process sees the same project and user environment as a direct CLI invocation: normal configuration, MCPs, plugins, skills, permissions, trust decisions, memory, and history are not copied, filtered, or replaced. Aiterm adds only completion correlation and a non-user sub-agent context containing `role=subagent`, the parent session, delegation depth, lineage, and `delegation_allowed=true`.
229
229
 
230
- The human-readable launch text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt containing the canonical `harness`; the old `provider` field remains for compatibility. The same `harness` is carried by agent dispatch, `aiterm-wait`, `agent_configure`, and agent rows in `pty_list`, while their old vendor/provider/agent fields remain compatibility fields. Codex completion comes from its normal durable rollout transcript, Grok CLI from its normal session events, Claude Code from a launch-specific Stop hook settings addition, and Cursor from its normal agent transcript's terminal `turn_ended` record. Sending to any agent session is a non-blocking **dispatch** — the call returns immediately with an opaque, harness-specific integer `event_cursor`, and completion arrives via [`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait). The Cursor adapter translates submit into the current CLI's extended keyboard protocol. If submitted text remains in Cursor's composer, its dispatch fails instead of returning a successful receipt.
230
+ The human-readable launch text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt containing the canonical `harness`; the old `provider` field remains for compatibility. The same `harness` is carried by agent dispatch, `aiterm-wait`, `agent_configure`, and agent rows in `pty_list`, while their old vendor/provider/agent fields remain compatibility fields. Codex completion comes from its normal durable rollout transcript, Grok CLI from its normal session events, Claude Code from a launch-specific Stop hook settings addition, and Cursor from its normal agent transcript's terminal `turn_ended` record. Sending to any agent session is a non-blocking **dispatch** — the call returns immediately with an opaque, harness-specific integer `event_cursor`, and Codex parents receive the answer automatically through the official input queue. Other parents use [`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait). The Cursor adapter translates submit into the current CLI's extended keyboard protocol. If submitted text remains in Cursor's composer, its dispatch fails instead of returning a successful receipt.
231
231
 
232
232
  `agent_launch`, `pty_send` (agent dispatch), and `agent_steer` accept an optional `image`: an array of absolute paths to image files (png/jpg/jpeg/gif/webp). Aiterm appends an attachment block to the prompt, and every harness opens the path with its own file-reading tool and sees the image; the caller never learns harness-specific attachment tricks. Invalid paths are rejected before anything is sent.
233
233
 
@@ -250,6 +250,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
250
250
  pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
251
251
  pty_send("codex1", "also fix the imports it broke")
252
252
  → non-blocking dispatch; receipt carries event_cursor
253
+ # Codex parents receive the answer automatically. For other parents:
253
254
  $ aiterm-wait --session codex1 --cursor <event_cursor> # never in the parent's foreground; exit 0=done, 3=timeout (not done), 4=closed, 7=error (turn aborted by an API error)
254
255
  pty_read("codex1", { agent_transcript: true }) → collect the full answer
255
256
  ```
@@ -544,11 +545,19 @@ When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lin
544
545
 
545
546
  ### Completion detection (5 layers)
546
547
 
548
+ For PowerShell over SSH, `mark:true` recognizes the current standard `PS ...>` prompt and emits PowerShell syntax even when Aiterm runs on macOS or Linux. A prompt left in earlier output is not used to select the syntax.
549
+
547
550
  `pty_read({ wait: true })` decides "is the command done?" via five layers: process exit / a `mark:true` sentinel / an `until` match / output quiescence with shell return / timeout. `mark` emits the shell's exit status on POSIX shells and `0` (success) or `1` (failure) on PowerShell; fish/csh/tcsh are rejected before send because they do not share either status syntax. When `mark` or `until` is active, that requested evidence takes precedence and a momentarily quiet shell cannot complete the read as quiescent. Agent sessions add a sixth exact layer: Codex observes normal rollout `task_complete`; Grok/Composer observe normal session `turn_ended`; Claude observes its additive launch-correlated Stop event; Cursor observes `turn_ended(status:"success")` in the launch-bound normal agent transcript. `aiterm-wait --cursor` performs that harness-specific observation without the parent blocking or polling. Pre-send readiness failures are MCP errors, and late completion remains recoverable without resending.
548
551
 
549
552
  ### Completion push for parent agents (`aiterm-wait`)
550
553
 
551
- 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:
554
+ **Codex parents receive child answers automatically.** Launch or send a request, then continue other work or end the turn. Aiterm observes the child, saves the unabridged answer, and submits it to the requesting parent's official Codex input queue. The parent processes it when idle. No waiter, `pty_read`, child callback instructions, or Desktop-specific connection is needed. This covers all supported child harnesses; Claude and other parent hosts retain the waiter flow below.
555
+
556
+ Automatic receipts include `parent_delivery` and set `wait_process` / `wait_command` to null. `pty_observe` exposes `parent_deliveries`: `waiting`, `ready`, `sending`, `submitted`, `failed`, or `unknown`. `submitted` means accepted by the queue, not read by the model. Aiterm retains the answer and resumes unsent work after an MCP restart. An interrupted submission becomes `unknown` and is not blindly retried.
557
+
558
+ Use a Codex runtime that supplies MCP `_meta.threadId` and the official `thread/queue` API (verified with Codex CLI 0.154.0). `aiterm-setup` checks the installed queue entry point; Aiterm verifies the requesting thread before each dispatch. Codex native sub-agents reject external queue input and cannot be automatic-delivery parents. Ordinary CLI and Desktop parents use the same supported route.
559
+
560
+ **For other parent hosts**, dispatch and start the receipt's waiter in a separate process:
552
561
 
553
562
  1. Launch the child with `agent_launch({ harness: ... })`; every launch shares the normal project/user environment and adds only completion correlation plus lineage. Send a turn with plain `pty_send` (or `claude_turn issue` for durable Claude operations). The call returns immediately with an `event_cursor` in its structured receipt.
554
563
  2. Pass the receipt's `wait_process.executable` and `wait_process.args` unchanged to a true argv process API. PowerShell 7's `Start-Process` is the exception because it joins `-ArgumentList` arrays; pass `windows_start_process_argument_list` as its one ready-made argument string instead. This invokes the bundled waiter through the exact Node runtime that is already running aiterm, including on native Windows where npm's human-facing bin is a PowerShell script shim and install paths may contain spaces. `wait_command` remains a compatibility display string for humans. The waiter observes the harness-owned completion source, plus Claude's additive launch hook, as a **pure reader** and exits with a one-line `aiterm.agent-wait-result.v1` receipt. **Exit ≠ done**: the receipt's `outcome` is authoritative (`0` = `done`, `3` = `timeout`, `4` = `closed`, `1` = error).
@@ -0,0 +1,135 @@
1
+ // Codex親の公式受信キューへの接続。親threadのload/resumeやDesktop固有通信は行わない。
2
+ import { spawn } from "node:child_process";
3
+ import { createInterface } from "node:readline";
4
+ import * as path from "node:path";
5
+ import { resolveAgentBin } from "./agent-resolver.js";
6
+ import { realCodexHome } from "./harnesses/codex.js";
7
+ import { AitermError } from "./errors.js";
8
+ export class CodexDeliveryError extends AitermError {
9
+ delivery_code;
10
+ outcome_unknown;
11
+ constructor(delivery_code, message, outcome_unknown = false) {
12
+ super(`${delivery_code}: ${message}`, 2);
13
+ this.delivery_code = delivery_code;
14
+ this.outcome_unknown = outcome_unknown;
15
+ }
16
+ }
17
+ /** modelの引数ではなく、CodexがMCP要求へ付けるmetadataだけを宛先にする。 */
18
+ export function codexParentFromRequest(clientName, metadata) {
19
+ if (clientName !== "codex-mcp-client")
20
+ return null;
21
+ const threadId = metadata?.threadId;
22
+ if (typeof threadId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(threadId)) {
23
+ throw new CodexDeliveryError("CODEX_PARENT_ID_UNAVAILABLE", "MCP要求に親のthreadIdがありません。対応するCodexへ更新してください");
24
+ }
25
+ return { thread_id: threadId, codex_home: path.resolve(realCodexHome()) };
26
+ }
27
+ async function withCodexReceiver(parent, action, runtime = {}) {
28
+ const executable = runtime.executable ?? resolveAgentBin("codex");
29
+ if (!executable)
30
+ throw new CodexDeliveryError("CODEX_RECEIVER_UNAVAILABLE", "Codexの実行ファイルを確認できません");
31
+ const child = spawn(executable, runtime.args ?? ["app-server", "--listen", "stdio://"], {
32
+ stdio: ["pipe", "pipe", "ignore"],
33
+ env: { ...process.env, CODEX_HOME: parent.codex_home },
34
+ windowsHide: true,
35
+ });
36
+ const pending = new Map();
37
+ let sequence = 0;
38
+ let stopped = false;
39
+ let transportError = null;
40
+ const failTransport = (message) => {
41
+ transportError = message;
42
+ for (const item of pending.values()) {
43
+ clearTimeout(item.timer);
44
+ item.reject(new CodexDeliveryError("CODEX_RECEIVER_TRANSPORT_FAILED", message, item.method === "thread/queue/add"));
45
+ }
46
+ pending.clear();
47
+ };
48
+ child.on("error", (error) => failTransport(`Codexを起動できません(${error.code ?? "unknown"})`));
49
+ child.stdin.on("error", (error) => failTransport(`Codexへの書込みに失敗しました(${error.code ?? "unknown"})`));
50
+ const exited = new Promise((resolve) => child.once("close", (code, signal) => {
51
+ if (!stopped)
52
+ failTransport(`Codexの接続が終了しました(exit=${code}, signal=${signal})`);
53
+ resolve();
54
+ }));
55
+ const reader = createInterface({ input: child.stdout });
56
+ reader.on("line", (line) => {
57
+ let value;
58
+ try {
59
+ value = JSON.parse(line);
60
+ }
61
+ catch {
62
+ failTransport("Codexが不正なJSON応答を返しました");
63
+ return;
64
+ }
65
+ const item = pending.get(value?.id);
66
+ if (!item)
67
+ return;
68
+ clearTimeout(item.timer);
69
+ pending.delete(value.id);
70
+ if (value.error) {
71
+ item.reject(new CodexDeliveryError("CODEX_RECEIVER_REJECTED", typeof value.error.message === "string" ? value.error.message : "公式受信口が要求を拒否しました"));
72
+ }
73
+ else if ("result" in value)
74
+ item.resolve(value.result);
75
+ else
76
+ item.reject(new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "公式受信口の応答にresultがありません", item.method === "thread/queue/add"));
77
+ });
78
+ const request = (method, params) => new Promise((resolve, reject) => {
79
+ if (transportError) {
80
+ reject(new CodexDeliveryError("CODEX_RECEIVER_TRANSPORT_FAILED", transportError));
81
+ return;
82
+ }
83
+ const id = ++sequence;
84
+ const timer = setTimeout(() => {
85
+ pending.delete(id);
86
+ reject(new CodexDeliveryError("CODEX_RECEIVER_TIMEOUT", `${method}の応答を確認できません`, method === "thread/queue/add"));
87
+ }, runtime.timeout_ms ?? 15_000);
88
+ pending.set(id, { resolve, reject, timer, method });
89
+ child.stdin.write(JSON.stringify({ id, method, params }) + "\n");
90
+ });
91
+ try {
92
+ await request("initialize", { clientInfo: { name: "aiterm_parent_delivery", version: "1" }, capabilities: { experimentalApi: true } });
93
+ child.stdin.write(JSON.stringify({ method: "initialized" }) + "\n");
94
+ return await action(request);
95
+ }
96
+ finally {
97
+ stopped = true;
98
+ for (const item of pending.values())
99
+ clearTimeout(item.timer);
100
+ pending.clear();
101
+ child.stdin.end();
102
+ // stdio終了を公式processへ伝える。終了しない外部processだけを明示的に停止する。
103
+ const terminate = setTimeout(() => child.kill("SIGKILL"), 2_000);
104
+ await exited;
105
+ clearTimeout(terminate);
106
+ reader.close();
107
+ }
108
+ }
109
+ /** 子へ送る前に、同じstoreの宛先と公式キューの対応を確認する。本文は保存・表示しない。 */
110
+ export async function verifyCodexParent(parent, runtime) {
111
+ await withCodexReceiver(parent, async (request) => {
112
+ const response = await request("thread/read", { threadId: parent.thread_id, includeTurns: false });
113
+ if (response?.thread?.id !== parent.thread_id) {
114
+ throw new CodexDeliveryError("CODEX_PARENT_UNAVAILABLE", "同じCodex環境で親threadを確認できません");
115
+ }
116
+ const subagent = response.thread.source?.subAgent;
117
+ if (subagent && typeof subagent === "object" && "thread_spawn" in subagent) {
118
+ throw new CodexDeliveryError("CODEX_PARENT_UNSUPPORTED", "Codexのnative sub-agentは外部processからのキュー入力を受け付けません");
119
+ }
120
+ await request("thread/queue/list", { threadId: parent.thread_id, limit: 1 });
121
+ }, runtime);
122
+ }
123
+ export async function submitCodexParentAnswer(parent, deliveryId, text, runtime) {
124
+ return withCodexReceiver(parent, async (request) => {
125
+ const result = await request("thread/queue/add", {
126
+ threadId: parent.thread_id,
127
+ input: [{ type: "text", text, text_elements: [] }],
128
+ clientUserMessageId: deliveryId,
129
+ });
130
+ if (typeof result?.queuedSubmission?.id !== "string") {
131
+ throw new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "キューの受付IDを確認できません", true);
132
+ }
133
+ return { queued_submission_id: result.queuedSubmission.id };
134
+ }, runtime);
135
+ }
package/dist/core.js CHANGED
@@ -16,7 +16,7 @@ import * as rtk from "./rtk.js";
16
16
  import { paneTokenHint } from "./harnesses/pane-tokens.js";
17
17
  import { readRuntimeProcesses, processSubtree, processIdentity, backgroundProcesses } from "./process-runtime.js";
18
18
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
19
- import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
19
+ import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, markShellCommand, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
20
20
  import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
21
21
  import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
22
22
  import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
@@ -235,12 +235,9 @@ export async function ensureAgentOwnsPaneInput(name, kind) {
235
235
  }
236
236
  function paneCurrentCommandForMark(name) {
237
237
  const foreground = paneCurrentCommand(name);
238
- if (!isWin || foreground === "powershell" || foreground === "pwsh")
238
+ if ((!isWin && foreground !== "ssh") || foreground === "powershell" || foreground === "pwsh")
239
239
  return foreground;
240
- // psmuxはnew-session直後だけpane_current_commandへ起動元shellを返すことがある。
241
- // 現在の末尾画面にPowerShell promptが描画済みなら、mark構文はその実効shellへ合わせる。
242
- const screen = captureScreen(name, 4).replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "");
243
- return /(?:^|\n)PS [^\r\n]*>\s*$/m.test(screen) ? "pwsh" : foreground;
240
+ return markShellCommand(foreground, captureScreen(name, 4));
244
241
  }
245
242
  function pasteBufferSupportsNoSanitizeFlag() {
246
243
  const listed = tmux("list-commands");
@@ -2390,11 +2387,14 @@ async function settlePublishedClaudeCompletionMarker(meta, marker) {
2390
2387
  /** agent harness の構造化 transcript から直近完了ターンの最終回答と相関情報を読む。 */
2391
2388
  export async function readAgentTranscriptResult(name, o = {}) {
2392
2389
  const meta = loadAgentMetadata(name);
2390
+ if (o.completion && (o.completion.outcome !== "done" || o.completion.launch_id !== meta.launch_id || o.completion.session_id !== name)) {
2391
+ throw new AitermError("回収対象の完了情報がagent launchと一致しません", 2);
2392
+ }
2393
2393
  const operationId = o.operation_id == null ? null : validateOperationId(o.operation_id);
2394
2394
  if (operationId && meta.kind !== "claude") {
2395
2395
  throw new AitermError("operation_id付き回収はClaude agent sessionだけで使用できます", 2);
2396
2396
  }
2397
- if (meta.kind === "claude") {
2397
+ if (meta.kind === "claude" && !o.completion) {
2398
2398
  let active = readClaudeOperationMarker(meta);
2399
2399
  if (active)
2400
2400
  active = await settlePublishedClaudeCompletionMarker(meta, active);
@@ -2410,10 +2410,13 @@ export async function readAgentTranscriptResult(name, o = {}) {
2410
2410
  throw new AitermError(`agent session '${name}' はまだターンが完了していません。agent_done 完了後に再取得してください。${agentWaitGuide(name)}`, 2);
2411
2411
  }
2412
2412
  const done = latestAgentDoneEvent(meta, operationId);
2413
+ if (o.completion && meta.kind !== "codex" && (!done || done.turn_id !== o.completion.turn_id || done.operation_id !== o.completion.operation_id)) {
2414
+ throw new AitermError("回収対象の完了情報が置換されました。別の回答は配送しません", 2);
2415
+ }
2413
2416
  if (operationId && !done) {
2414
2417
  throw new AitermError(`operation ${operationId} はまだ完了していません。同じoperation_idで後から再取得してください。${agentWaitGuide(name)}`, 2);
2415
2418
  }
2416
- const turnId = done?.turn_id ?? null;
2419
+ const turnId = o.completion?.turn_id ?? done?.turn_id ?? null;
2417
2420
  let text = "";
2418
2421
  if (meta.kind === "claude") {
2419
2422
  if (!done)
@@ -2424,7 +2427,7 @@ export async function readAgentTranscriptResult(name, o = {}) {
2424
2427
  text = cursorTranscriptText(meta, readTranscriptLines, transcriptUnavailable);
2425
2428
  }
2426
2429
  else if (meta.kind === "codex") {
2427
- text = codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable);
2430
+ text = codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable, o.completion !== undefined);
2428
2431
  }
2429
2432
  else {
2430
2433
  if (!done)
@@ -2436,7 +2439,7 @@ export async function readAgentTranscriptResult(name, o = {}) {
2436
2439
  if (o.lines != null)
2437
2440
  text = text.split("\n").slice(-o.lines).join("\n");
2438
2441
  const rawChars = text.length;
2439
- const [body, outputMeta] = reduceOutput(text, name, true);
2442
+ const [body, outputMeta] = o.raw ? [text, ""] : reduceOutput(text, name, true);
2440
2443
  const transcriptMeta = [
2441
2444
  "agent_transcript",
2442
2445
  `vendor=${meta.kind}`,
@@ -2571,6 +2574,10 @@ export function agentWaitLaunchForm(command) {
2571
2574
  }
2572
2575
  // dispatch / 起動時 prompt 送信後の共通案内。第一文で「待たない」を宣言し、待ち方は後段に置く。
2573
2576
  export function agentDispatchGuide(session, cursor) {
2577
+ if (parentClientName === "codex-mcp-client") {
2578
+ return "回答本文はAitermがこのCodex親へ自動配送する。wait起動・ポーリング・通常の回答回収は不要。" +
2579
+ "親は作業を続けるかターンを終え、順番待ちから届く子の回答で続行する。";
2580
+ }
2574
2581
  const cmd = `aiterm-wait --session ${session} --cursor ${cursor}`;
2575
2582
  return (`投げっぱなしでよい=ここで待たない。親は自分の作業へ戻るか、このターンを終える。\n` +
2576
2583
  `完了通知: ${agentWaitLaunchForm(cmd)}。exit が完了通知(${AITERM_WAIT_OUTCOME_NOTE})。\n` +
@@ -2578,6 +2585,8 @@ export function agentDispatchGuide(session, cursor) {
2578
2585
  }
2579
2586
  // 未完了 session へ触った時の共通案内。ここでも待つのは waiter プロセスであって親ではない。
2580
2587
  export function agentWaitGuide(session) {
2588
+ if (parentClientName === "codex-mcp-client")
2589
+ return "回答本文はこのCodex親へ自動配送される。親は作業を続けるかターンを終える。";
2581
2590
  const cmd = `aiterm-wait --session ${session ?? "<session_id>"} --cursor 0`;
2582
2591
  return `完了通知は ${agentWaitLaunchForm(cmd)} で受ける(親はここで待たない・polling 不要)。receipt の outcome=done を確認してから再取得する。`;
2583
2592
  }
@@ -2637,13 +2646,13 @@ export async function observeAgentDone(name, o = {}) {
2637
2646
  }
2638
2647
  const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
2639
2648
  if (meta.kind === "codex" && meta.completion_route === "codex_transcript") {
2640
- return observeCodexDone(meta, timeout, o.cursor, detectAgentRateLimit);
2649
+ return observeCodexDone(meta, timeout, o.cursor, detectAgentRateLimit, o.signal);
2641
2650
  }
2642
2651
  if (meta.kind === "cursor" && meta.completion_route === "cursor_transcript") {
2643
- return observeCursorDone(meta, timeout, o.cursor, detectAgentRateLimit);
2652
+ return observeCursorDone(meta, timeout, o.cursor, detectAgentRateLimit, o.signal);
2644
2653
  }
2645
2654
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
2646
- return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit);
2655
+ return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit, o.signal);
2647
2656
  }
2648
2657
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
2649
2658
  // 境界の優先順: dispatch receipt の event_cursor(起動順序に依存しない)→ operation相関
@@ -2674,6 +2683,7 @@ export async function observeAgentDone(name, o = {}) {
2674
2683
  error: apiError?.text ?? null,
2675
2684
  });
2676
2685
  for (;;) {
2686
+ o.signal?.throwIfAborted();
2677
2687
  if (!fs.existsSync(metadataFile))
2678
2688
  return observation("closed");
2679
2689
  const size = safeStatSize(meta.event_file);
@@ -3106,6 +3116,9 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
3106
3116
  const promptText = meta.kind === "cursor" ? cursorPromptWithLineage(meta, text) : text;
3107
3117
  const startOffset = agentCompletionCursor(meta);
3108
3118
  try {
3119
+ prepareSendText(promptText, { raw: false });
3120
+ await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
3121
+ harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: null });
3109
3122
  if (meta.kind === "claude") {
3110
3123
  prepareSendText(text, { raw: false });
3111
3124
  reserveAnonymousClaudeTurn(meta);
@@ -3440,6 +3453,9 @@ export async function dispatchAgentTurn(name, text, o = {}) {
3440
3453
  const dispatchText = meta.kind === "cursor" && !meta.vendor_session_id
3441
3454
  ? `${subagentInstruction(meta)}\n\n${text}`
3442
3455
  : text;
3456
+ prepareSendText(dispatchText, { raw: o.raw });
3457
+ await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
3458
+ harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: operationId });
3443
3459
  if (meta.kind === "claude") {
3444
3460
  // durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
3445
3461
  // receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
@@ -3520,7 +3536,7 @@ export async function steerAgentTurn(name, text) {
3520
3536
  sendKey(name, "Enter");
3521
3537
  return { ...receipt, delivery: "steered", pane_input_recovery: paneInputRecovery };
3522
3538
  }
3523
- export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, }) {
3539
+ export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, before_send, }) {
3524
3540
  assertSessionName(name);
3525
3541
  if (action !== "issue" && action !== "recover") {
3526
3542
  throw new AitermError('action は "issue" または "recover" を指定してください', 2);
@@ -3535,7 +3551,7 @@ export async function runClaudeOperation({ session_id: name, action, operation_i
3535
3551
  throw new AitermError("claude_turn issueには空でないtextが必要です", 2);
3536
3552
  }
3537
3553
  // v0.16.0: issue は dispatch-only。完了通知は aiterm-wait --operation、回収は recover が担う。
3538
- dispatchReceipt = await dispatchAgentTurn(name, text, { operation_id: operationId });
3554
+ dispatchReceipt = await dispatchAgentTurn(name, text, { operation_id: operationId, before_send });
3539
3555
  }
3540
3556
  else {
3541
3557
  if (text != null)
@@ -3876,7 +3892,7 @@ export function openAgent(kind, opts = {}) {
3876
3892
  }
3877
3893
  const driveHint = agentDone
3878
3894
  ? `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
3879
- `turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch=投げっぱなしでよい・完了通知はaiterm-wait)。中断はpty_key(${sid}, "C-c")、` +
3895
+ `turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch。${parentClientName === "codex-mcp-client" ? "回答本文はCodex親へ自動配送する" : "完了通知はaiterm-wait"})。中断はpty_key(${sid}, "C-c")、` +
3880
3896
  `Stopが来ない場合の解除はpty_close(${sid})を使う。`
3881
3897
  : `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
3882
3898
  `pty_send(${sid}, "...") で入力・pty_key(${sid}, "Enter"/"Up"/"C-c" 等) で操作する(対話)。`;
@@ -3974,6 +3990,7 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
3974
3990
  const initial = await sendInitialAgentPrompt(sid, prompt, {
3975
3991
  ready_timeout: opts.ready_timeout ?? undefined,
3976
3992
  trust_project: opts.trust_project,
3993
+ before_send: opts.before_send,
3977
3994
  });
3978
3995
  return [sid, `${hint}\n${initial.text}`, initial.event_cursor, initial.submit_residue, initial.initial_prompt,
3979
3996
  { status: "ready", reason: "composer_ready" }];
@@ -249,7 +249,7 @@ export function latestCodexCompletion(meta, readTranscriptLines) {
249
249
  }
250
250
  return latest;
251
251
  }
252
- export async function observeCodexDone(meta, timeout, requestedCursor, detectRateLimit) {
252
+ export async function observeCodexDone(meta, timeout, requestedCursor, detectRateLimit, signal) {
253
253
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
254
254
  let transcript = codexRootTranscript(meta);
255
255
  const startOffset = requestedCursor ?? (transcript ? safeStatSize(transcript) : 0);
@@ -275,6 +275,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, detectRat
275
275
  error: null,
276
276
  });
277
277
  for (;;) {
278
+ signal?.throwIfAborted();
278
279
  if (!fs.existsSync(metadataFile))
279
280
  return observation("closed");
280
281
  transcript ??= codexRootTranscript(meta);
@@ -539,13 +540,42 @@ export function codexMoreReasoningChoice(screen) {
539
540
  return null;
540
541
  }
541
542
  // 回収対象turnの最終assistantメッセージをroot rollout transcriptから抽出する。
542
- export function codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable) {
543
+ export function codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable, exactCompletion = false) {
543
544
  if (!meta.codex_home || !meta.vendor_session_id)
544
545
  transcriptUnavailable();
545
546
  const transcript = findLatestCodexTranscript(meta.codex_home, meta.vendor_session_id);
546
547
  if (!transcript)
547
548
  transcriptUnavailable();
548
549
  const lines = readTranscriptLines(transcript);
550
+ if (exactCompletion) {
551
+ // 同じturnの本文だけを使う。完了event内の本文と、turn ID付きoutput_textの両形式を扱う。
552
+ const matching = [];
553
+ for (const line of lines) {
554
+ let record;
555
+ try {
556
+ record = JSON.parse(line);
557
+ }
558
+ catch {
559
+ continue;
560
+ }
561
+ const payload = record?.payload;
562
+ if (record?.type === "response_item" && payload?.type === "message" && payload?.role === "assistant"
563
+ && payload?.internal_chat_message_metadata_passthrough?.turn_id === turnId && Array.isArray(payload?.content)) {
564
+ for (const item of payload.content) {
565
+ if (item?.type === "output_text" && typeof item.text === "string")
566
+ matching.push(item.text);
567
+ }
568
+ }
569
+ if (record?.type === "event_msg" && payload?.type === "task_complete" && payload.turn_id === turnId) {
570
+ if (typeof payload.last_agent_message === "string")
571
+ return payload.last_agent_message;
572
+ if (matching.length > 0)
573
+ return matching.join("\n");
574
+ transcriptUnavailable();
575
+ }
576
+ }
577
+ transcriptUnavailable();
578
+ }
549
579
  const matching = [];
550
580
  let finalAnswer = "";
551
581
  for (const line of lines) {
@@ -183,7 +183,7 @@ export function latestCursorCompletion(meta, readTranscriptLines) {
183
183
  ? cursorCompletionEvent(meta, harnessSessionId, state.terminalRecord, `cursor:${state.userTurns}`)
184
184
  : null;
185
185
  }
186
- export async function observeCursorDone(meta, timeout, requestedCursor, detectRateLimit) {
186
+ export async function observeCursorDone(meta, timeout, requestedCursor, detectRateLimit, signal) {
187
187
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
188
188
  let transcript = cursorTranscript(meta);
189
189
  const startBoundary = requestedCursor ?? (transcript ? cursorTranscriptState(transcript).userTurns : 0);
@@ -205,6 +205,7 @@ export async function observeCursorDone(meta, timeout, requestedCursor, detectRa
205
205
  error: null,
206
206
  });
207
207
  for (;;) {
208
+ signal?.throwIfAborted();
208
209
  if (!fs.existsSync(metadataFile))
209
210
  return observation("closed");
210
211
  transcript ??= cursorTranscript(meta);
@@ -146,7 +146,7 @@ export function grokInitializationComplete(meta) {
146
146
  }
147
147
  return false;
148
148
  }
149
- export async function observeGrokDone(meta, timeout, requestedCursor, detectRateLimit) {
149
+ export async function observeGrokDone(meta, timeout, requestedCursor, detectRateLimit, signal) {
150
150
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
151
151
  const transcript = grokEventsTranscript(meta);
152
152
  if (!transcript)
@@ -174,6 +174,7 @@ export async function observeGrokDone(meta, timeout, requestedCursor, detectRate
174
174
  error: ev?.done_status === "turn_error" ? ev.reason : null,
175
175
  });
176
176
  for (;;) {
177
+ signal?.throwIfAborted();
177
178
  if (!fs.existsSync(metadataFile))
178
179
  return observation("closed");
179
180
  if (fs.existsSync(transcript)) {