aiterm-mcp 0.21.0 → 0.21.4
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 +28 -17
- package/README.md +37 -21
- package/dist/aiterm-wait-cli.js +0 -0
- package/dist/core.js +292 -38
- package/dist/index.js +6 -3
- package/dist/runtime-error-store.js +14 -9
- package/dist/runtime-errors-cli.js +0 -0
- package/package.json +2 -2
- package/dist/codex-stop-hook.js +0 -128
package/README.ja.md
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
> **Claude Code から Codex CLI の対話 TUI を操作する——スラッシュコマンドや [`$imagegen`](https://learn.chatgpt.com/docs/image-generation#generate-or-edit-an-image) のようなスキルまで、MCP越しに使える。**
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src=".github/og.
|
|
4
|
+
<img src=".github/og.png" alt="Aiterm — 異なる知性が一つの持続する実行現場を共有する森の観測拠点" width="100%">
|
|
5
|
+
<br>
|
|
6
|
+
<sub><em>この画像は、異なる知性がひとつの持続する実行現場を共有し、それぞれの視点から同じ仕事を前へ進める姿を表しています。</em></sub>
|
|
5
7
|
</p>
|
|
6
8
|
|
|
7
|
-
#
|
|
9
|
+
# Aiterm
|
|
8
10
|
|
|
9
11
|
[](https://github.com/kitepon-rgb/aiterm-mcp/actions/workflows/ci.yml)
|
|
10
12
|
[](https://www.npmjs.com/package/aiterm-mcp)
|
|
11
13
|
[](https://www.npmjs.com/package/aiterm-mcp)
|
|
12
14
|
[](https://nodejs.org)
|
|
13
15
|
[](LICENSE)
|
|
14
|
-
[](https://packagephobia.com/result?p=aiterm-mcp)
|
|
15
16
|
|
|
16
17
|
> *(English: [README.md](README.md))*
|
|
17
18
|
|
|
@@ -91,12 +92,20 @@ claude mcp add --scope user --transport stdio aiterm -- npx -y aiterm-mcp
|
|
|
91
92
|
host統合は、kitepon.devの製品開発を支える内部基盤
|
|
92
93
|
[dotagents](https://github.com/kitepon-rgb/dotagents)が担当します。
|
|
93
94
|
|
|
94
|
-
**言葉でなく実測で:**
|
|
95
|
+
**言葉でなく実測で:** 記録済み203テストのベンチマークでは、`pty_read` はコンテキストに載るトークンを生ログの **約 7.1 分の 1** に減らす。しかも pass/fail の判定は畳んでも残る。→ [組み込みシェルツールとの使い分け](#組み込みシェルツールとの使い分け)
|
|
95
96
|
|
|
96
97
|
13 ツール: 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を、`claude_approval`がmanaged Claudeの相関済み承認UI中継を、`diagnostics`が安全なfactory readinessを返す。バックエンドは **tmux** なので、MCP サーバや AI クライアントが再起動してもセッションは生き残る。
|
|
97
98
|
|
|
98
|
-
**v0.
|
|
99
|
-
|
|
99
|
+
**v0.21.4ではfresh managed Claudeからuser scope MCPを復元。** 通常hook、plugin、permission、
|
|
100
|
+
project/local MCPの隔離は維持し、`~/.claude.json`で既にuser scope登録された`mcpServers`だけを
|
|
101
|
+
owner-onlyのlaunch設定へsnapshotする。破損したuser MCP設定は、toolなしで黙って起動せずsession作成前に失敗する。
|
|
102
|
+
|
|
103
|
+
**v0.21.3ではCodexの完了経路からStop hookを撤去。** Codexの完了通知と最終回答の帰属は、
|
|
104
|
+
root rollout transcriptへ永続化される`task_complete.turn_id`をdispatch byte境界以後から観測する。
|
|
105
|
+
hookの実行ファイルが壊れたり消えたりしても`aiterm-wait`は座礁しない。v0.21.0では外部agent launcherへ
|
|
106
|
+
明示的な`write_scope`能力宣言を追加し、v0.21.3で指定したscopeと実効性がstructured launch receiptへ
|
|
107
|
+
確実に残るよう修正した。v0.20.3では、壊れた認証から複数のmanaged Claude/Fable
|
|
108
|
+
sessionが同時にloginへ流れる問題を修理し、新規Claude起動はPTY作成前にvendor所有の共有認証を検証する。
|
|
100
109
|
v0.20では、待たずに一度だけ観測する
|
|
101
110
|
`aiterm-wait --timeout 0` の未完了を、実際に待って終わらなかった`timeout`と区別し、
|
|
102
111
|
`running`(exit 5)で返すようにしました。v0.19系では相関済みmanaged Claude approval中継を追加し、
|
|
@@ -133,13 +142,15 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
|
|
|
133
142
|
|
|
134
143
|
### 2. その端末の中に他のコーディングエージェントを起動する — オーケストレーションの旗艦
|
|
135
144
|
|
|
136
|
-
同じ 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
|
|
145
|
+
同じ 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**で、Codexはroot rollout transcriptの`task_complete`、Claude/Grokは隔離されたmanaged 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`後もClaude markerを保持し、Stopが来なければsessionをcloseする。`claude_agent` と `codex_agent` の初回 `prompt` は ready gate 経由で送信して待たずに返る(Grok/Composer は argv 渡し)。手動でキー操作したい場合は `pty_open` で素の端末を開き vendor CLI を自分で起動する。
|
|
137
146
|
|
|
138
147
|
`codex_agent`・`grok_agent`・`composer_agent`は任意の`write_scope`(`"read-only"`または書込み許可パスの説明)も受ける。指定値はlaunch receipt・session metadata・`pty_list`へ保存する。Codexの`write_scope:"read-only"`だけは実効能力壁であり、aitermがCLIの`--sandbox read-only`を付ける。Grok/Composerには対応する対話起動sandboxがなく、Codexにもパス説明をallowlistへ変換するフラグがないため、それらは強制済みと偽らず`write_scope_enforcement:"declaration_only_unsupported"`を返す。`write_scope`を省略した起動は従来どおりである。
|
|
139
148
|
|
|
140
149
|
```text
|
|
141
150
|
codex_agent({ session_name: "codex1", cwd: "/repo",
|
|
142
|
-
prompt: "port test/legacy.py to vitest"
|
|
151
|
+
prompt: "port test/legacy.py to vitest",
|
|
152
|
+
model: "gpt-5.6-sol", reasoning_effort: "high",
|
|
153
|
+
write_scope: "test/ only; no commit" })
|
|
143
154
|
→ { session_id: "codex1", … } # Codex が永続端末で稼働開始
|
|
144
155
|
pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
|
|
145
156
|
pty_send("codex1", "also fix the imports it broke") # 非ブロックdispatch=event_cursor入りreceipt
|
|
@@ -152,11 +163,11 @@ $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeo
|
|
|
152
163
|
| ツール | 起動するもの | 主な引数 |
|
|
153
164
|
| --- | --- | --- |
|
|
154
165
|
| `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
|
|
155
|
-
| `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?` |
|
|
156
|
-
| `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
|
|
157
|
-
| `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
|
|
166
|
+
| `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?`, `write_scope?` |
|
|
167
|
+
| `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?`, `write_scope?` |
|
|
168
|
+
| `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?`, `write_scope?` |
|
|
158
169
|
|
|
159
|
-
各ベンダーの CLI が導入・認証済みであること(`claude_agent` は `claude`、`codex_agent` は `codex`、Grok 系は `grok`)。バイナリは `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、各既定path、`PATH` の順で解決する。CLI不在・不正なmodel/effort・実在しない`cwd`はsession作成前に失敗し、残骸を残さない。Claudeはさらに、PTY作成前に同じCLIの`auth status --json`が`loggedIn:true`を返すことを要求する。未認証・malformed・失敗exit・timeoutは残骸ゼロで失敗し、正常なvendor所有の共有認証は複数sessionから利用する。managed Claudeへのexact `/login`・`/logout`は通常dispatchとforce送信の双方で副作用前に拒否するため、認証は通常端末で一度だけ修理する。Claudeは通常settingsを継承しないlaunch専用settingsとStop hook
|
|
170
|
+
各ベンダーの CLI が導入・認証済みであること(`claude_agent` は `claude`、`codex_agent` は `codex`、Grok 系は `grok`)。バイナリは `CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、各既定path、`PATH` の順で解決する。CLI不在・不正なmodel/effort・実在しない`cwd`はsession作成前に失敗し、残骸を残さない。Claudeはさらに、PTY作成前に同じCLIの`auth status --json`が`loggedIn:true`を返すことを要求する。未認証・malformed・失敗exit・timeoutは残骸ゼロで失敗し、正常なvendor所有の共有認証は複数sessionから利用する。managed Claudeへのexact `/login`・`/logout`は通常dispatchとforce送信の双方で副作用前に拒否するため、認証は通常端末で一度だけ修理する。Claudeは通常settingsを継承しないlaunch専用settingsとStop hookを使う一方、user scopeの`mcpServers`だけは`~/.claude.json`から別のowner-only launch configへsnapshotし`--mcp-config`で渡す。project/local MCPは継承しない。本文なしeventとowner-only bounded resultを分離し、`pty_read({ agent_transcript:true })`はdigestとbyte数を検証したresultだけを返してClaude private transcriptを読まない。後着resultは同じsessionからprompt再送なしで回収できる。managed Claudeのactive turn中はC-c以外の`pty_key`と素送信を拒否する。Claudeが`Do you want to proceed?`を表示したら、`claude_approval(action:"inspect", ...)`で画面digestを取得し、表示内容を判断してから、そのdigestと`approve_once`または`deny`を`respond`へ渡す。同じoperation・同じ画面が維持されている時だけ入力し、任意文字列や恒久許可選択肢は中継しない。中断は`C-c`、解除は`pty_close`。
|
|
160
171
|
|
|
161
172
|
エージェント間の隠れたプロトコルは無い。起動したClaude/Codex/Grok/Composerは利用者がattachできるもう1本の永続sessionであり、MCPクライアントが通常のPTY操作で駆動する。
|
|
162
173
|
|
|
@@ -354,17 +365,17 @@ consumer は `aiterm-runtime-errors snapshot` を読み、durable ingestion 後
|
|
|
354
365
|
| ツール | 起動するもの | 主な引数 |
|
|
355
366
|
| --- | --- | --- |
|
|
356
367
|
| `claude_agent` | Claude Code CLI(Anthropic) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?` |
|
|
357
|
-
| `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?` |
|
|
358
|
-
| `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
|
|
359
|
-
| `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?` |
|
|
368
|
+
| `codex_agent` | Codex CLI(OpenAI・端末設定/CLI既定、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`(`low`/`medium`/`high`/`xhigh`/`max`/`ultra`), `cwd?`, `session_name?`, `write_scope?` |
|
|
369
|
+
| `grok_agent` | Grok Build(xAI、既定`grok-4.5`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?`, `write_scope?` |
|
|
370
|
+
| `composer_agent` | Grok Build(xAI、既定`grok-composer-2.5-fast`、`model?`で上書き) | `prompt?`, `model?`, `reasoning_effort?`は非対応(指定時は明示エラー), `cwd?`, `session_name?`, `write_scope?` |
|
|
360
371
|
|
|
361
372
|
対応するCLI(`claude` / `codex` / `grok`)の導入・認証が必要。解決順は`CLAUDE_BIN` / `CODEX_BIN` / `GROK_BIN`、既定path、`PATH`。前提違反はsession作成前に明示失敗する。ClaudeはPTY作成前に構造化認証statusも検証し、managed session内の`/login`・`/logout`を拒否する。4 launcherすべてが同じ非ブロックdispatch契約を使い、Claude/Codexの初回promptはready gate経由で送信される。Claudeはisolated managed settingsとhook-captured resultを使い、private transcriptへ依存しない。Claude/Codex/Grok/Composerのlive smokeはすべてgreenであり、fixtureによる検証とは区別して記録する。
|
|
362
373
|
|
|
363
|
-
エージェントの回答が画面 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は
|
|
374
|
+
エージェントの回答が画面 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はroot rollout transcriptの`task_complete.turn_id`で完了と最終回答を同じturnへ帰属し、Grok/Composerは最後の実user行より後ろのassistant行を採る。不在・非agent・抽出不能は明示エラー。
|
|
364
375
|
|
|
365
376
|
### 完了検出(5 層)
|
|
366
377
|
|
|
367
|
-
`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の正確な層を使う:
|
|
378
|
+
`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の正確な層を使う: Codexは`pty_send` dispatchが返したtranscript byte境界以後の`task_complete`を、Claude/Grokはevent-file境界以後のmanaged hook eventを`aiterm-wait --cursor`が観測する(親はブロックもポーリングもしない)。`pty_send` の送信前 ready 失敗は MCP エラー、launcher の初回 prompt ready 失敗は `initial_prompt=not_sent` を返す。完結した構造化JSONL行が壊れていた場合は、`aiterm-wait` receipt の `malformed_events` に数えられる。
|
|
368
379
|
|
|
369
380
|
### トークン削減
|
|
370
381
|
|
package/README.md
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
> **Drive Codex CLI's interactive TUI from Claude Code — including slash commands and skills such as [`$imagegen`](https://learn.chatgpt.com/docs/image-generation#generate-or-edit-an-image) — through MCP.**
|
|
2
2
|
|
|
3
3
|
<p align="center">
|
|
4
|
-
<img src=".github/og.
|
|
4
|
+
<img src=".github/og.png" alt="Aiterm — a shared forest observatory where different intelligences work in one persistent execution space" width="100%">
|
|
5
|
+
<br>
|
|
6
|
+
<sub><em>This image represents different intelligences sharing one persistent workspace and advancing the same work from their own perspectives.</em></sub>
|
|
5
7
|
</p>
|
|
6
8
|
|
|
7
|
-
#
|
|
9
|
+
# Aiterm
|
|
8
10
|
|
|
9
11
|
[](https://github.com/kitepon-rgb/aiterm-mcp/actions/workflows/ci.yml)
|
|
10
12
|
[](https://www.npmjs.com/package/aiterm-mcp)
|
|
11
13
|
[](https://www.npmjs.com/package/aiterm-mcp)
|
|
12
14
|
[](https://nodejs.org)
|
|
13
15
|
[](LICENSE)
|
|
14
|
-
[](https://packagephobia.com/result?p=aiterm-mcp)
|
|
15
16
|
|
|
16
17
|
> *(日本語: [README.ja.md](README.ja.md))*
|
|
17
18
|
|
|
@@ -91,12 +92,25 @@ execution lane. Cross-product installation and host integration are handled by
|
|
|
91
92
|
[dotagents](https://github.com/kitepon-rgb/dotagents), the internal development
|
|
92
93
|
toolchain behind kitepon.dev's products.
|
|
93
94
|
|
|
94
|
-
**Measured, not claimed:**
|
|
95
|
+
**Measured, not claimed:** in the recorded 203-test benchmark, a `pty_read` puts **~7.1× fewer tokens** in your context than the raw log — and the pass/fail verdict survives the fold. → [When to reach for it vs. the built-in shell](#when-to-reach-for-it-vs-the-built-in-shell)
|
|
95
96
|
|
|
96
97
|
Thirteen 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, `claude_approval` for correlated managed-Claude approval prompts, and `diagnostics` for safe factory readiness. The backend is **tmux**, so sessions survive even if the MCP server or the AI client restarts.
|
|
97
98
|
|
|
98
|
-
**v0.
|
|
99
|
-
|
|
99
|
+
**v0.21.4 restores user-scoped MCPs in fresh managed Claude sessions.** Aiterm keeps
|
|
100
|
+
normal hooks, plugins, permissions, and project/local MCPs isolated, while snapshotting only
|
|
101
|
+
the already user-scoped `mcpServers` from `~/.claude.json` into an owner-only launch config.
|
|
102
|
+
Malformed user MCP config fails before a session is created instead of silently launching
|
|
103
|
+
Claude without its tools.
|
|
104
|
+
|
|
105
|
+
**v0.21.3 removes Codex Stop hooks from the completion path.** Codex completion and
|
|
106
|
+
final-message attribution now come from the root rollout transcript's durable
|
|
107
|
+
`task_complete.turn_id`, observed after the dispatch byte boundary. A broken or stale
|
|
108
|
+
hook executable can no longer strand `aiterm-wait`. v0.21.0 added explicit
|
|
109
|
+
`write_scope` declarations for external-agent launchers; v0.21.3 also fixes their
|
|
110
|
+
structured launch receipts so a supplied scope and its enforcement status are retained.
|
|
111
|
+
v0.20.3 prevents concurrent
|
|
112
|
+
managed Claude/Fable sessions from turning one broken login into many competing login
|
|
113
|
+
flows. Every new Claude launch verifies the
|
|
100
114
|
vendor-owned shared credential store before creating a PTY, while healthy credentials
|
|
101
115
|
remain reusable across concurrent and repeated sessions. The v0.20 line also distinguishes
|
|
102
116
|
a non-blocking `aiterm-wait --timeout 0` observation (`running`, exit 5) from a real timed-out
|
|
@@ -124,7 +138,7 @@ A lot of 2026's agent tooling is converging on orchestration: a lead model deleg
|
|
|
124
138
|
|
|
125
139
|
aiterm predates Build Week, so the event work is kept visible in dated commits. During the submission window (July 14–16, 2026), I extended it with safe serialized delivery for long PTY input, correlated operation IDs and bounded result recovery, machine-readable launch and idempotent close receipts, and a hardened readiness gate that prevents prompts from disappearing during TUI startup redraws. The public comparison from the pre-event release is [`v0.12.2...main`](https://github.com/kitepon-rgb/aiterm-mcp/compare/v0.12.2...main).
|
|
126
140
|
|
|
127
|
-
I used **Codex with GPT-5.6** as an engineering collaborator: it inspected the implementation, challenged the API and recovery contracts, generated focused regression cases, and helped verify race, security, timeout, and malformed-event paths. I reviewed the diffs and test evidence and retained the final product and architecture decisions.
|
|
141
|
+
I used **Codex with GPT-5.6** as an engineering collaborator: it inspected the implementation, challenged the API and recovery contracts, generated focused regression cases, and helped verify race, security, timeout, and malformed-event paths. I reviewed the diffs and test evidence and retained the final product and architecture decisions. At that Build Week checkpoint, the regression suite contained 262 tests covering normal operation as well as failure and recovery behavior; current release receipts live in the [CHANGELOG](CHANGELOG.md) and release ADRs.
|
|
128
142
|
|
|
129
143
|
## Two ways to use it
|
|
130
144
|
|
|
@@ -143,7 +157,7 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
|
|
|
143
157
|
|
|
144
158
|
### 2. Launch other coding agents into that terminal — the orchestration flagship
|
|
145
159
|
|
|
146
|
-
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**:
|
|
160
|
+
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**: Codex completion comes from its own durable rollout transcript's `task_complete`; Claude and Grok use isolated managed Stop hooks. 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 Claude 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).
|
|
147
161
|
|
|
148
162
|
`codex_agent`, `grok_agent`, and `composer_agent` also accept an optional `write_scope`: either `"read-only"` or a human-readable description of writable paths. A supplied value is retained in the launch receipt, session metadata, and `pty_list`. For Codex, `write_scope: "read-only"` is an effective boundary: aiterm adds the CLI's `--sandbox read-only` flag. Grok/Composer have no corresponding interactive-launch sandbox, and Codex has no path-description allowlist flag; those cases return `write_scope_enforcement: "declaration_only_unsupported"` rather than claiming enforcement. Omitting `write_scope` preserves prior behavior.
|
|
149
163
|
|
|
@@ -151,7 +165,9 @@ For a managed Claude turn stopped at `Do you want to proceed?`, use `claude_appr
|
|
|
151
165
|
|
|
152
166
|
```text
|
|
153
167
|
codex_agent({ session_name: "codex1", cwd: "/repo",
|
|
154
|
-
prompt: "port test/legacy.py to vitest"
|
|
168
|
+
prompt: "port test/legacy.py to vitest",
|
|
169
|
+
model: "gpt-5.6-sol", reasoning_effort: "high",
|
|
170
|
+
write_scope: "test/ only; no commit" })
|
|
155
171
|
→ { session_id: "codex1", … } # Codex now live in a persistent terminal
|
|
156
172
|
pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
|
|
157
173
|
pty_send("codex1", "also fix the imports it broke")
|
|
@@ -165,13 +181,13 @@ One call per model, so the tool name itself tells you which model you get:
|
|
|
165
181
|
| Tool | Launches | Key args |
|
|
166
182
|
| --- | --- | --- |
|
|
167
183
|
| `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `launch_operation_id?` |
|
|
168
|
-
| `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?` |
|
|
169
|
-
| `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?` |
|
|
170
|
-
| `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?` |
|
|
184
|
+
| `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?`, `write_scope?` |
|
|
185
|
+
| `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?`, `write_scope?` |
|
|
186
|
+
| `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?`, `write_scope?` |
|
|
171
187
|
|
|
172
|
-
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. Before creating a Claude session, aiterm also requires a successful structured `claude auth status --json` result with `loggedIn: true`; unavailable, malformed, or failed authentication leaves **zero leftover session**. Claude sessions share the vendor-owned credential store rather than copying credentials per launch, so multiple sessions can reuse one healthy login. Managed Claude rejects exact `/login` and `/logout` dispatches, including forced sends: repair authentication once in a normal terminal, then relaunch any stale unauthenticated sessions. 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
|
|
188
|
+
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. Before creating a Claude session, aiterm also requires a successful structured `claude auth status --json` result with `loggedIn: true`; unavailable, malformed, or failed authentication leaves **zero leftover session**. Claude sessions share the vendor-owned credential store rather than copying credentials per launch, so multiple sessions can reuse one healthy login. Managed Claude rejects exact `/login` and `/logout` dispatches, including forced sends: repair authentication once in a normal terminal, then relaunch any stale unauthenticated sessions. 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. User-scoped `mcpServers` are copied separately from `~/.claude.json` into a launch-local owner-only config and passed through `--mcp-config`; project/local MCPs remain isolated. 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. While a managed Claude turn is active, raw sends and non-interrupt keys are rejected. If Claude displays `Do you want to proceed?`, call `claude_approval(action:"inspect", ...)`, decide from the visible prompt, then call `respond` with the returned digest and either `approve_once` or `deny`. The response is accepted only while the same operation and screen digest remain current; arbitrary text and persistent-allow choices are never relayed. Use `pty_key("C-c")` to interrupt and `pty_close` to abandon the session. 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).
|
|
173
189
|
|
|
174
|
-
The managed Codex home links authentication, privately snapshots `config.toml` and `agents/*.toml` custom-role definitions, and keeps sessions/caches isolated. A symlinked role definition is resolved into a regular-file snapshot rather than shared with the source home.
|
|
190
|
+
The managed Codex home links authentication, privately snapshots `config.toml` and `agents/*.toml` custom-role definitions, and keeps sessions/caches isolated. A symlinked role definition is resolved into a regular-file snapshot rather than shared with the source home. aiterm does not install a Codex Stop hook: the root rollout transcript is the completion source, so a missing hook executable cannot strand `aiterm-wait`.
|
|
175
191
|
|
|
176
192
|
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.
|
|
177
193
|
|
|
@@ -293,7 +309,7 @@ The second round-trip pays for itself once the output runs long, or the state ha
|
|
|
293
309
|
| `find node_modules -type f` | ~500 tok¹ | ~456 tok | tokens tie; aiterm keeps head and tail + `line_range` |
|
|
294
310
|
| `grep -rn "session" src/` | ~2,989 tok | ~1,096 tok | **aiterm** (~2.7×; long lines get clipped²) |
|
|
295
311
|
|
|
296
|
-
|
|
312
|
+
In the recorded 203-test benchmark the reduction is real and safe. The built-in tool drops the whole 223-line log — ~4,292 tokens — into context. aiterm folds its own capture of the run down to ~607:
|
|
297
313
|
|
|
298
314
|
```text
|
|
299
315
|
[aiterm demo: 51 行 / ~607 tok (raw 223 行 / ~4292 tok); 172 行 hidden] [is_complete=True via mark]
|
|
@@ -371,24 +387,24 @@ Each launcher starts a specific vendor's interactive coding-agent TUI inside a f
|
|
|
371
387
|
| Tool | Launches | Key args |
|
|
372
388
|
| --- | --- | --- |
|
|
373
389
|
| `claude_agent` | Claude Code CLI (Anthropic) | `prompt?`, `model?`, `reasoning_effort?` (`low`/`medium`/`high`/`xhigh`/`max`), `cwd?`, `session_name?`, `launch_operation_id?` |
|
|
374
|
-
| `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?` |
|
|
375
|
-
| `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?` |
|
|
376
|
-
| `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?` |
|
|
390
|
+
| `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?`, `write_scope?` |
|
|
391
|
+
| `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?`, `write_scope?` |
|
|
392
|
+
| `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?`, `write_scope?` |
|
|
377
393
|
|
|
378
394
|
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 additionally requires a structured healthy authentication status before any PTY exists, and managed Claude rejects `/login` and `/logout`; repair authentication once in a normal terminal. 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. Claude, Codex, Grok, and Composer live smokes are green; fixture coverage remains a separate claim. Native Windows can launch agents but managed completion is not supported yet.
|
|
379
395
|
|
|
380
|
-
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
|
|
396
|
+
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 uses the root rollout transcript's `task_complete.turn_id` both for completion and final-message attribution; 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.
|
|
381
397
|
|
|
382
398
|
### Completion detection (5 layers)
|
|
383
399
|
|
|
384
|
-
`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
|
|
400
|
+
`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 a sixth exact layer: Codex observes `task_complete` after the dispatch's transcript byte boundary; Claude/Grok observe their managed hook event after the event-file boundary. `aiterm-wait --cursor` performs that vendor-specific observation 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 completion remains recoverable from the same session with `pty_read({ agent_transcript:true })`, without resending. Malformed complete JSONL records are counted in `malformed_events` for diagnosis rather than treated as done.
|
|
385
401
|
|
|
386
402
|
### Completion push for parent agents (`aiterm-wait`)
|
|
387
403
|
|
|
388
404
|
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:
|
|
389
405
|
|
|
390
406
|
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.
|
|
391
|
-
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
|
|
407
|
+
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 completion source as a **pure reader**—Codex rollout `task_complete`, or a managed hook event for the other vendors—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.
|
|
392
408
|
3. **The parent never runs the waiter in its own foreground.** Waiting is correct — but the waiter is a separate process, not the parent's turn. 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. So that this is not left to interpretation, aiterm reads `clientInfo.name` from the MCP `initialize` handshake and its receipts name the concrete invocation for the detected host — for Claude Code, literally `Bash(command: "aiterm-wait …", run_in_background: true)`. Unknown or undeclared hosts get the generic "start it as a process that does not block the parent's turn" wording; nothing else about the contract changes. Every receipt leads with the same rule: dispatch and let go, then go do something else or end the turn.
|
|
393
409
|
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.
|
|
394
410
|
|
package/dist/aiterm-wait-cli.js
CHANGED
|
File without changes
|
package/dist/core.js
CHANGED
|
@@ -56,6 +56,7 @@ const AGENT_DONE_SCREEN_SETTLE_MIN_SAMPLES = 3;
|
|
|
56
56
|
const AGENT_SUBMIT_DELAY_MS = 250;
|
|
57
57
|
const AGENT_EVENT_MAX_BYTES = 1024 * 1024;
|
|
58
58
|
const AGENT_EVENT_TAIL_BYTES = 64 * 1024;
|
|
59
|
+
const CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES = 16 * 1024 * 1024;
|
|
59
60
|
const AGENT_METADATA_NEGATIVE_CACHE_TTL_MS = 2_000;
|
|
60
61
|
const AGENT_TUI_READY_TIMEOUT_MS = 30_000;
|
|
61
62
|
const AGENT_TUI_READY_POLL_MS = 500;
|
|
@@ -411,6 +412,12 @@ function agentManagedClaudeSettingsPath(name, launchId) {
|
|
|
411
412
|
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
412
413
|
return path.join(agentsDir(), `${name}.${launchId}.claude-settings.json`);
|
|
413
414
|
}
|
|
415
|
+
function agentManagedClaudeMcpConfigPath(name, launchId) {
|
|
416
|
+
assertSessionName(name);
|
|
417
|
+
if (!LAUNCH_ID_RE.test(launchId))
|
|
418
|
+
throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
|
|
419
|
+
return path.join(agentsDir(), `${name}.${launchId}.claude-mcp.json`);
|
|
420
|
+
}
|
|
414
421
|
function agentClaudeResultPath(name, launchId) {
|
|
415
422
|
assertSessionName(name);
|
|
416
423
|
if (!LAUNCH_ID_RE.test(launchId))
|
|
@@ -493,6 +500,7 @@ function cleanupAgentState(name) {
|
|
|
493
500
|
f.endsWith(".events.jsonl") ||
|
|
494
501
|
f.endsWith(".wait.lock") ||
|
|
495
502
|
f.endsWith(".claude-settings.json") ||
|
|
503
|
+
f.endsWith(".claude-mcp.json") ||
|
|
496
504
|
f.endsWith(".claude-result.json") ||
|
|
497
505
|
f.endsWith(".claude-operation.json") ||
|
|
498
506
|
f.endsWith(".claude-approval.json") ||
|
|
@@ -1353,6 +1361,7 @@ export function killAll() {
|
|
|
1353
1361
|
f.endsWith(".events.jsonl") ||
|
|
1354
1362
|
f.endsWith(".wait.lock") ||
|
|
1355
1363
|
f.endsWith(".claude-settings.json") ||
|
|
1364
|
+
f.endsWith(".claude-mcp.json") ||
|
|
1356
1365
|
f.endsWith(".claude-result.json") ||
|
|
1357
1366
|
f.endsWith(".claude-operation.json") ||
|
|
1358
1367
|
f.endsWith(".claude-dispatch") ||
|
|
@@ -1378,15 +1387,18 @@ export function killAll() {
|
|
|
1378
1387
|
const DEFAULT_AGENT_DONE_TIMEOUT = 600;
|
|
1379
1388
|
const agentMetadataNegativeCache = new Map();
|
|
1380
1389
|
let agentTuiReadyStableSamplesTestOverride = null;
|
|
1381
|
-
function codexHookScriptPath() {
|
|
1382
|
-
return path.join(path.dirname(fileURLToPath(import.meta.url)), "codex-stop-hook.js");
|
|
1383
|
-
}
|
|
1384
1390
|
function grokHookScriptPath() {
|
|
1385
1391
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), "grok-stop-hook.js");
|
|
1386
1392
|
}
|
|
1387
1393
|
function claudeHookScriptPath() {
|
|
1388
1394
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), "claude-stop-hook.js");
|
|
1389
1395
|
}
|
|
1396
|
+
// process.execPath は Homebrew 等では Cellar の版付き実体を指す。長寿命 MCP server の起動後に
|
|
1397
|
+
// runtime が更新されるとその実体だけが消え、既に生成済みの hook が exit 127 になる。
|
|
1398
|
+
// hook は server と同じ継承 PATH から node を毎回解決し、安定した package script を実行する。
|
|
1399
|
+
function nodeHookCommand(hookScript) {
|
|
1400
|
+
return `${shq("node")} ${shq(hookScript)}`;
|
|
1401
|
+
}
|
|
1390
1402
|
function safeStatSize(p) {
|
|
1391
1403
|
try {
|
|
1392
1404
|
return fs.statSync(p).size;
|
|
@@ -1514,7 +1526,7 @@ function readCodexConfigPins(configPath) {
|
|
|
1514
1526
|
};
|
|
1515
1527
|
return { model: pick("model"), effort: pick("model_reasoning_effort") };
|
|
1516
1528
|
}
|
|
1517
|
-
function managedCodexConfigSummary(configPath
|
|
1529
|
+
function managedCodexConfigSummary(configPath) {
|
|
1518
1530
|
let body;
|
|
1519
1531
|
try {
|
|
1520
1532
|
body = fs.readFileSync(configPath, "utf8");
|
|
@@ -1543,8 +1555,6 @@ function managedCodexConfigSummary(configPath, hookTrustBypass) {
|
|
|
1543
1555
|
bits.push(`approval_policy=${approvalPolicy}`);
|
|
1544
1556
|
if (sandboxMode)
|
|
1545
1557
|
bits.push(`sandbox_mode=${sandboxMode}`);
|
|
1546
|
-
if (hookTrustBypass)
|
|
1547
|
-
bits.push("hook trust bypass 有効");
|
|
1548
1558
|
return `managed config: ${bits.join(" / ")}`;
|
|
1549
1559
|
}
|
|
1550
1560
|
// Custom agent definitions are configuration, not mutable Codex state. Copy only direct
|
|
@@ -1626,27 +1636,9 @@ function createManagedCodexHome(name, launchId, overrides = {}) {
|
|
|
1626
1636
|
fs.chmodSync(configDst, 0o600);
|
|
1627
1637
|
}
|
|
1628
1638
|
snapshotCodexAgentDefinitions(srcHome, managedHome);
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
}
|
|
1633
|
-
writeJson0600(path.join(managedHome, "hooks.json"), {
|
|
1634
|
-
hooks: {
|
|
1635
|
-
Stop: [
|
|
1636
|
-
{
|
|
1637
|
-
hooks: [
|
|
1638
|
-
{
|
|
1639
|
-
type: "command",
|
|
1640
|
-
command: `${shq(process.execPath)} ${shq(hookScript)}`,
|
|
1641
|
-
timeoutSec: 10,
|
|
1642
|
-
async: false,
|
|
1643
|
-
statusMessage: null,
|
|
1644
|
-
},
|
|
1645
|
-
],
|
|
1646
|
-
},
|
|
1647
|
-
],
|
|
1648
|
-
},
|
|
1649
|
-
});
|
|
1639
|
+
// Codex 自身の rollout transcript に task_complete が永続化されるため、完了検出用の
|
|
1640
|
+
// Stop hook は作らない。hook と transcript の二重正本、および hook failure の単一障害点を
|
|
1641
|
+
// ここでなくす。
|
|
1650
1642
|
return managedHome;
|
|
1651
1643
|
}
|
|
1652
1644
|
function createManagedClaudeSettings(name, launchId) {
|
|
@@ -1662,7 +1654,7 @@ function createManagedClaudeSettings(name, launchId) {
|
|
|
1662
1654
|
hooks: [
|
|
1663
1655
|
{
|
|
1664
1656
|
type: "command",
|
|
1665
|
-
command:
|
|
1657
|
+
command: nodeHookCommand(hookScript),
|
|
1666
1658
|
timeout: 10,
|
|
1667
1659
|
},
|
|
1668
1660
|
],
|
|
@@ -1672,6 +1664,52 @@ function createManagedClaudeSettings(name, launchId) {
|
|
|
1672
1664
|
});
|
|
1673
1665
|
return settings;
|
|
1674
1666
|
}
|
|
1667
|
+
function createManagedClaudeMcpConfig(name, launchId) {
|
|
1668
|
+
const home = process.env.HOME?.trim() || os.homedir();
|
|
1669
|
+
const source = path.join(home, ".claude.json");
|
|
1670
|
+
let canonical;
|
|
1671
|
+
try {
|
|
1672
|
+
canonical = fs.realpathSync(source);
|
|
1673
|
+
}
|
|
1674
|
+
catch (error) {
|
|
1675
|
+
if (error.code === "ENOENT")
|
|
1676
|
+
return null;
|
|
1677
|
+
throw new AitermError(`Claude user MCP設定を読めません: ${error.message}`, 2);
|
|
1678
|
+
}
|
|
1679
|
+
let fd;
|
|
1680
|
+
let parsed;
|
|
1681
|
+
try {
|
|
1682
|
+
fd = fs.openSync(canonical, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
|
|
1683
|
+
const st = fs.fstatSync(fd);
|
|
1684
|
+
if (!st.isFile() || st.uid !== currentUid() || st.size > 16 * 1024 * 1024) {
|
|
1685
|
+
throw new AitermError("Claude user MCP設定の安全検証に失敗しました", 2);
|
|
1686
|
+
}
|
|
1687
|
+
parsed = JSON.parse(fs.readFileSync(fd, "utf8"));
|
|
1688
|
+
}
|
|
1689
|
+
catch (error) {
|
|
1690
|
+
if (error instanceof AitermError)
|
|
1691
|
+
throw error;
|
|
1692
|
+
throw new AitermError(`Claude user MCP設定を読めません: ${error.message}`, 2);
|
|
1693
|
+
}
|
|
1694
|
+
finally {
|
|
1695
|
+
if (fd !== undefined)
|
|
1696
|
+
fs.closeSync(fd);
|
|
1697
|
+
}
|
|
1698
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1699
|
+
throw new AitermError("Claude user MCP設定のtop-level JSONはobjectである必要があります", 2);
|
|
1700
|
+
}
|
|
1701
|
+
const servers = parsed.mcpServers;
|
|
1702
|
+
if (servers === undefined)
|
|
1703
|
+
return null;
|
|
1704
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
|
|
1705
|
+
throw new AitermError("Claude user MCP設定のmcpServersはobjectである必要があります", 2);
|
|
1706
|
+
}
|
|
1707
|
+
if (Object.keys(servers).length === 0)
|
|
1708
|
+
return null;
|
|
1709
|
+
const destination = agentManagedClaudeMcpConfigPath(name, launchId);
|
|
1710
|
+
writeJson0600(destination, { mcpServers: servers });
|
|
1711
|
+
return destination;
|
|
1712
|
+
}
|
|
1675
1713
|
function realGrokHome() {
|
|
1676
1714
|
return path.resolve(process.env.GROK_HOME || path.join(process.env.HOME ?? os.homedir(), ".grok"));
|
|
1677
1715
|
}
|
|
@@ -1775,7 +1813,7 @@ function createManagedGrokHome(name, launchId, authPath) {
|
|
|
1775
1813
|
hooks: [
|
|
1776
1814
|
{
|
|
1777
1815
|
type: "command",
|
|
1778
|
-
command:
|
|
1816
|
+
command: nodeHookCommand(hookScript),
|
|
1779
1817
|
timeout: 10,
|
|
1780
1818
|
},
|
|
1781
1819
|
],
|
|
@@ -2243,6 +2281,7 @@ function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId,
|
|
|
2243
2281
|
createEmpty0600NoFollow(eventFile);
|
|
2244
2282
|
createEmpty0600NoFollow(resultFile);
|
|
2245
2283
|
const claudeSettings = createManagedClaudeSettings(name, launchId);
|
|
2284
|
+
const claudeMcpConfig = createManagedClaudeMcpConfig(name, launchId);
|
|
2246
2285
|
const meta = {
|
|
2247
2286
|
kind: "claude",
|
|
2248
2287
|
aiterm_session: name,
|
|
@@ -2257,6 +2296,7 @@ function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId,
|
|
|
2257
2296
|
hook_route: "managed_claude_settings",
|
|
2258
2297
|
node_platform: process.platform,
|
|
2259
2298
|
claude_settings: claudeSettings,
|
|
2299
|
+
claude_mcp_config: claudeMcpConfig,
|
|
2260
2300
|
result_file: resultFile,
|
|
2261
2301
|
};
|
|
2262
2302
|
writeAgentMetadata(meta);
|
|
@@ -2278,6 +2318,7 @@ function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {}, writ
|
|
|
2278
2318
|
vendor_session_id: null,
|
|
2279
2319
|
initial_prompt: initialPrompt,
|
|
2280
2320
|
hook_route: "managed_codex_home",
|
|
2321
|
+
completion_route: "codex_transcript",
|
|
2281
2322
|
node_platform: process.platform,
|
|
2282
2323
|
codex_home: codexHome,
|
|
2283
2324
|
};
|
|
@@ -2340,11 +2381,14 @@ function loadAgentMetadata(name) {
|
|
|
2340
2381
|
}
|
|
2341
2382
|
if (m.kind === "claude") {
|
|
2342
2383
|
const expectedSettings = agentManagedClaudeSettingsPath(name, m.launch_id);
|
|
2384
|
+
const expectedMcpConfig = agentManagedClaudeMcpConfigPath(name, m.launch_id);
|
|
2343
2385
|
const expectedResult = agentClaudeResultPath(name, m.launch_id);
|
|
2386
|
+
const claudeMcpConfig = m.claude_mcp_config ?? null;
|
|
2344
2387
|
const launchOperationId = m.launch_operation_id ?? null;
|
|
2345
2388
|
const launchRequestDigest = m.launch_request_digest ?? null;
|
|
2346
2389
|
if (m.hook_route !== "managed_claude_settings" ||
|
|
2347
2390
|
m.claude_settings !== expectedSettings ||
|
|
2391
|
+
(claudeMcpConfig !== null && claudeMcpConfig !== expectedMcpConfig) ||
|
|
2348
2392
|
m.result_file !== expectedResult ||
|
|
2349
2393
|
((launchOperationId === null) !== (launchRequestDigest === null)) ||
|
|
2350
2394
|
(launchOperationId !== null && !OPERATION_ID_RE.test(launchOperationId)) ||
|
|
@@ -2366,6 +2410,7 @@ function loadAgentMetadata(name) {
|
|
|
2366
2410
|
hook_route: "managed_claude_settings",
|
|
2367
2411
|
node_platform: process.platform,
|
|
2368
2412
|
claude_settings: expectedSettings,
|
|
2413
|
+
claude_mcp_config: claudeMcpConfig === expectedMcpConfig ? expectedMcpConfig : null,
|
|
2369
2414
|
result_file: expectedResult,
|
|
2370
2415
|
};
|
|
2371
2416
|
}
|
|
@@ -2385,6 +2430,7 @@ function loadAgentMetadata(name) {
|
|
|
2385
2430
|
vendor_session_id: typeof m.vendor_session_id === "string" ? m.vendor_session_id : null,
|
|
2386
2431
|
initial_prompt: normalizeInitialPromptState(m.initial_prompt),
|
|
2387
2432
|
hook_route: "managed_codex_home",
|
|
2433
|
+
...(m.completion_route === "codex_transcript" ? { completion_route: "codex_transcript" } : {}),
|
|
2388
2434
|
node_platform: process.platform,
|
|
2389
2435
|
codex_home: expectedHome,
|
|
2390
2436
|
};
|
|
@@ -2508,6 +2554,15 @@ function bindAgentVendorSession(meta, ev) {
|
|
|
2508
2554
|
function bindCompletedInitialPrompt(meta) {
|
|
2509
2555
|
if (meta.initial_prompt !== "pending" && meta.initial_prompt !== "sent")
|
|
2510
2556
|
return;
|
|
2557
|
+
if (meta.kind === "codex") {
|
|
2558
|
+
const done = latestCodexCompletion(meta);
|
|
2559
|
+
if (!done) {
|
|
2560
|
+
throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。${agentWaitGuide(meta.aiterm_session)}`, 2);
|
|
2561
|
+
}
|
|
2562
|
+
bindAgentVendorSession(meta, done);
|
|
2563
|
+
setInitialPromptState(meta, "done");
|
|
2564
|
+
return;
|
|
2565
|
+
}
|
|
2511
2566
|
if (meta.vendor_session_id) {
|
|
2512
2567
|
setInitialPromptState(meta, "done");
|
|
2513
2568
|
return;
|
|
@@ -2540,6 +2595,8 @@ function tryLoadAgentMetadata(name) {
|
|
|
2540
2595
|
}
|
|
2541
2596
|
}
|
|
2542
2597
|
function latestAgentDoneEvent(meta, expectedOperationId = null) {
|
|
2598
|
+
if (meta.kind === "codex")
|
|
2599
|
+
return latestCodexCompletion(meta);
|
|
2543
2600
|
const size = safeStatSize(meta.event_file);
|
|
2544
2601
|
if (size === 0)
|
|
2545
2602
|
return null;
|
|
@@ -2594,6 +2651,14 @@ function completedClaudeOperationEvent(meta, operationId) {
|
|
|
2594
2651
|
function recoverAgentVendorSession(meta) {
|
|
2595
2652
|
if (meta.vendor_session_id)
|
|
2596
2653
|
return;
|
|
2654
|
+
if (meta.kind === "codex") {
|
|
2655
|
+
const transcript = bindCodexTranscriptSession(meta);
|
|
2656
|
+
if (!transcript)
|
|
2657
|
+
return;
|
|
2658
|
+
if (meta.initial_prompt === "pending" && latestCodexCompletion(meta))
|
|
2659
|
+
setInitialPromptState(meta, "done");
|
|
2660
|
+
return;
|
|
2661
|
+
}
|
|
2597
2662
|
const size = safeStatSize(meta.event_file);
|
|
2598
2663
|
if (size === 0)
|
|
2599
2664
|
return;
|
|
@@ -2647,6 +2712,116 @@ function findLatestCodexTranscript(codexHome, vendorSessionId) {
|
|
|
2647
2712
|
visit(sessionsDir);
|
|
2648
2713
|
return latestFile;
|
|
2649
2714
|
}
|
|
2715
|
+
function listCodexTranscripts(codexHome) {
|
|
2716
|
+
const sessionsDir = path.join(codexHome, "sessions");
|
|
2717
|
+
const files = [];
|
|
2718
|
+
const visit = (dir) => {
|
|
2719
|
+
let entries;
|
|
2720
|
+
try {
|
|
2721
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
2722
|
+
}
|
|
2723
|
+
catch {
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
for (const entry of entries) {
|
|
2727
|
+
const file = path.join(dir, entry.name);
|
|
2728
|
+
if (entry.isDirectory())
|
|
2729
|
+
visit(file);
|
|
2730
|
+
else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl"))
|
|
2731
|
+
files.push(file);
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
visit(sessionsDir);
|
|
2735
|
+
// managed CODEX_HOME では root TUI の rollout が最初に作られる。後から同じ home に
|
|
2736
|
+
// sub-agent rollout が増えても root を取り違えないよう、未bind時は最古を選べる順にする。
|
|
2737
|
+
return files.sort();
|
|
2738
|
+
}
|
|
2739
|
+
function codexTranscriptSessionId(file) {
|
|
2740
|
+
const size = Math.min(safeStatSize(file), AGENT_EVENT_TAIL_BYTES);
|
|
2741
|
+
if (size === 0)
|
|
2742
|
+
return null;
|
|
2743
|
+
const text = readFileRange(file, 0, size).toString("utf8");
|
|
2744
|
+
for (const line of text.split("\n")) {
|
|
2745
|
+
if (!line.trim())
|
|
2746
|
+
continue;
|
|
2747
|
+
try {
|
|
2748
|
+
const record = JSON.parse(line);
|
|
2749
|
+
if (record?.type === "session_meta" && typeof record?.payload?.id === "string" && record.payload.id) {
|
|
2750
|
+
return record.payload.id;
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
catch {
|
|
2754
|
+
// startup中の未完結行は後のpollで読み直す。
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
return null;
|
|
2758
|
+
}
|
|
2759
|
+
function codexRootTranscript(meta) {
|
|
2760
|
+
if (meta.kind !== "codex" || !meta.codex_home)
|
|
2761
|
+
return null;
|
|
2762
|
+
if (meta.vendor_session_id)
|
|
2763
|
+
return findLatestCodexTranscript(meta.codex_home, meta.vendor_session_id);
|
|
2764
|
+
return listCodexTranscripts(meta.codex_home)[0] ?? null;
|
|
2765
|
+
}
|
|
2766
|
+
function bindCodexTranscriptSession(meta) {
|
|
2767
|
+
const transcript = codexRootTranscript(meta);
|
|
2768
|
+
if (!transcript)
|
|
2769
|
+
return null;
|
|
2770
|
+
const vendorSessionId = codexTranscriptSessionId(transcript);
|
|
2771
|
+
if (vendorSessionId && !meta.vendor_session_id) {
|
|
2772
|
+
meta.vendor_session_id = vendorSessionId;
|
|
2773
|
+
writeAgentMetadata(meta);
|
|
2774
|
+
}
|
|
2775
|
+
return transcript;
|
|
2776
|
+
}
|
|
2777
|
+
function codexCompletionEvent(meta, vendorSessionId, record) {
|
|
2778
|
+
if (record?.type !== "event_msg" ||
|
|
2779
|
+
record?.payload?.type !== "task_complete" ||
|
|
2780
|
+
typeof record?.payload?.turn_id !== "string" ||
|
|
2781
|
+
!record.payload.turn_id)
|
|
2782
|
+
return null;
|
|
2783
|
+
return {
|
|
2784
|
+
type: "agent_done",
|
|
2785
|
+
vendor: "codex",
|
|
2786
|
+
aiterm_session: meta.aiterm_session,
|
|
2787
|
+
launch_id: meta.launch_id,
|
|
2788
|
+
vendor_session_id: vendorSessionId,
|
|
2789
|
+
turn_id: record.payload.turn_id,
|
|
2790
|
+
operation_id: null,
|
|
2791
|
+
reason: "Codex transcript task_complete",
|
|
2792
|
+
done_status: "turn_done",
|
|
2793
|
+
stop_hook_active: false,
|
|
2794
|
+
at: typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString(),
|
|
2795
|
+
};
|
|
2796
|
+
}
|
|
2797
|
+
function latestCodexCompletion(meta) {
|
|
2798
|
+
const transcript = codexRootTranscript(meta);
|
|
2799
|
+
if (!transcript)
|
|
2800
|
+
return null;
|
|
2801
|
+
const vendorSessionId = meta.vendor_session_id ?? codexTranscriptSessionId(transcript);
|
|
2802
|
+
let latest = null;
|
|
2803
|
+
for (const line of readTranscriptLines(transcript)) {
|
|
2804
|
+
if (!line.trim())
|
|
2805
|
+
continue;
|
|
2806
|
+
try {
|
|
2807
|
+
latest = codexCompletionEvent(meta, vendorSessionId, JSON.parse(line)) ?? latest;
|
|
2808
|
+
}
|
|
2809
|
+
catch {
|
|
2810
|
+
// Codexが末尾を書込み中なら、その行は次の観測で完結してから読む。
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
return latest;
|
|
2814
|
+
}
|
|
2815
|
+
function agentCompletionCursor(meta) {
|
|
2816
|
+
if (meta.kind !== "codex")
|
|
2817
|
+
return safeStatSize(meta.event_file);
|
|
2818
|
+
const transcript = bindCodexTranscriptSession(meta);
|
|
2819
|
+
if (meta.completion_route !== "codex_transcript") {
|
|
2820
|
+
meta.completion_route = "codex_transcript";
|
|
2821
|
+
writeAgentMetadata(meta);
|
|
2822
|
+
}
|
|
2823
|
+
return transcript ? safeStatSize(transcript) : 0;
|
|
2824
|
+
}
|
|
2650
2825
|
function transcriptUnavailable() {
|
|
2651
2826
|
throw new AitermError(`transcript がまだありません。ターン完了後に再取得してください。${agentWaitGuide()}`, 2);
|
|
2652
2827
|
}
|
|
@@ -2927,9 +3102,83 @@ export function agentWaitGuide(session) {
|
|
|
2927
3102
|
const cmd = `aiterm-wait --session ${session ?? "<session_id>"} --cursor 0`;
|
|
2928
3103
|
return `完了通知は ${agentWaitLaunchForm(cmd)} で受ける(親はここで待たない・polling 不要)。receipt の outcome=done を確認してから再取得する。`;
|
|
2929
3104
|
}
|
|
3105
|
+
async function observeCodexDone(meta, timeout, requestedCursor) {
|
|
3106
|
+
const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
|
|
3107
|
+
let transcript = codexRootTranscript(meta);
|
|
3108
|
+
const startOffset = requestedCursor ?? (transcript ? safeStatSize(transcript) : 0);
|
|
3109
|
+
let cursor = startOffset;
|
|
3110
|
+
let carry = "";
|
|
3111
|
+
let malformedEvents = 0;
|
|
3112
|
+
let discardLeadingFragment = false;
|
|
3113
|
+
let initializedBoundary = false;
|
|
3114
|
+
const deadline = performance.now() + timeout * 1000;
|
|
3115
|
+
const observation = (outcome, ev = null) => ({
|
|
3116
|
+
schema: "aiterm.agent-wait-result.v1",
|
|
3117
|
+
session_id: meta.aiterm_session,
|
|
3118
|
+
launch_id: meta.launch_id,
|
|
3119
|
+
vendor: "codex",
|
|
3120
|
+
outcome,
|
|
3121
|
+
operation_id: null,
|
|
3122
|
+
vendor_session_id: ev?.vendor_session_id ?? meta.vendor_session_id ?? null,
|
|
3123
|
+
turn_id: ev?.turn_id ?? null,
|
|
3124
|
+
malformed_events: malformedEvents,
|
|
3125
|
+
at: ev?.at ?? null,
|
|
3126
|
+
});
|
|
3127
|
+
for (;;) {
|
|
3128
|
+
if (!fs.existsSync(metadataFile))
|
|
3129
|
+
return observation("closed");
|
|
3130
|
+
transcript ??= codexRootTranscript(meta);
|
|
3131
|
+
if (transcript) {
|
|
3132
|
+
if (!initializedBoundary) {
|
|
3133
|
+
if (cursor > 0) {
|
|
3134
|
+
const previous = readFileRange(transcript, cursor - 1, cursor).toString("utf8");
|
|
3135
|
+
discardLeadingFragment = previous !== "\n";
|
|
3136
|
+
}
|
|
3137
|
+
initializedBoundary = true;
|
|
3138
|
+
}
|
|
3139
|
+
const size = safeStatSize(transcript);
|
|
3140
|
+
if (size < cursor) {
|
|
3141
|
+
throw new AitermError("Codex transcript が完了待機中に短くなりました。該当セッションを閉じて起動し直してください。", 2);
|
|
3142
|
+
}
|
|
3143
|
+
if (size - startOffset > CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES) {
|
|
3144
|
+
throw new AitermError("Codex transcript のturn増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
|
|
3145
|
+
}
|
|
3146
|
+
if (size > cursor) {
|
|
3147
|
+
carry += readFileRange(transcript, cursor, size).toString("utf8");
|
|
3148
|
+
cursor = size;
|
|
3149
|
+
const parts = carry.split("\n");
|
|
3150
|
+
carry = parts.pop() ?? "";
|
|
3151
|
+
if (discardLeadingFragment && parts.length > 0) {
|
|
3152
|
+
parts.shift();
|
|
3153
|
+
discardLeadingFragment = false;
|
|
3154
|
+
}
|
|
3155
|
+
const vendorSessionId = meta.vendor_session_id ?? codexTranscriptSessionId(transcript);
|
|
3156
|
+
for (const line of parts) {
|
|
3157
|
+
if (!line.trim())
|
|
3158
|
+
continue;
|
|
3159
|
+
if (Buffer.byteLength(line, "utf8") > AGENT_EVENT_MAX_BYTES) {
|
|
3160
|
+
malformedEvents++;
|
|
3161
|
+
continue;
|
|
3162
|
+
}
|
|
3163
|
+
try {
|
|
3164
|
+
const done = codexCompletionEvent(meta, vendorSessionId, JSON.parse(line));
|
|
3165
|
+
if (done)
|
|
3166
|
+
return observation("done", done);
|
|
3167
|
+
}
|
|
3168
|
+
catch {
|
|
3169
|
+
malformedEvents++;
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
if (performance.now() >= deadline)
|
|
3175
|
+
return observation(timeout === 0 ? "running" : "timeout");
|
|
3176
|
+
await sleep(AGENT_DONE_POLL_MS);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
2930
3179
|
// 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
|
|
2931
|
-
// event fileの
|
|
2932
|
-
//
|
|
3180
|
+
// Codexはrollout transcript、他vendorはevent fileを増分走査し、vendor_session_idのbind永続化を
|
|
3181
|
+
// 行わない(waiterは観測者であって所有者でない)。
|
|
2933
3182
|
export async function observeAgentDone(name, o = {}) {
|
|
2934
3183
|
const meta = loadAgentMetadata(name);
|
|
2935
3184
|
const operationId = o.operation_id == null ? null : validateOperationId(o.operation_id);
|
|
@@ -2940,6 +3189,9 @@ export async function observeAgentDone(name, o = {}) {
|
|
|
2940
3189
|
throw new AitermError("cursor は0以上の整数byte offsetで指定してください", 2);
|
|
2941
3190
|
}
|
|
2942
3191
|
const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
|
|
3192
|
+
if (meta.kind === "codex" && meta.completion_route === "codex_transcript") {
|
|
3193
|
+
return observeCodexDone(meta, timeout, o.cursor);
|
|
3194
|
+
}
|
|
2943
3195
|
const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
|
|
2944
3196
|
// 境界の優先順: dispatch receipt の event_cursor(起動順序に依存しない)→ operation相関
|
|
2945
3197
|
// (operation_idの一意性で先頭から全走査できる)→ waiter起動時EOF(waiter先行起動が前提)。
|
|
@@ -3203,7 +3455,7 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
|
|
|
3203
3455
|
submit_residue: null,
|
|
3204
3456
|
};
|
|
3205
3457
|
}
|
|
3206
|
-
const startOffset =
|
|
3458
|
+
const startOffset = agentCompletionCursor(meta);
|
|
3207
3459
|
try {
|
|
3208
3460
|
if (meta.kind === "claude") {
|
|
3209
3461
|
prepareSendText(text, { raw: false, force: true });
|
|
@@ -3230,7 +3482,7 @@ export function isAgentSession(name) {
|
|
|
3230
3482
|
return tryLoadAgentMetadata(name) !== null;
|
|
3231
3483
|
}
|
|
3232
3484
|
// v0.16.0: 親をブロックする wait 経路は廃止した。send は ready gate と submit 分離を内蔵した
|
|
3233
|
-
// dispatch として即返り、event_cursor(送信直前の
|
|
3485
|
+
// dispatch として即返り、event_cursor(送信直前のvendor完了正本境界)を receipt で返す。
|
|
3234
3486
|
// 完了通知は aiterm-wait(--cursor で境界を渡す)、回収は pty_read / claude_turn recover が担う。
|
|
3235
3487
|
export async function dispatchAgentTurn(name, text, o = {}) {
|
|
3236
3488
|
assertSessionName(name);
|
|
@@ -3241,14 +3493,16 @@ export async function dispatchAgentTurn(name, text, o = {}) {
|
|
|
3241
3493
|
throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
|
|
3242
3494
|
}
|
|
3243
3495
|
bindCompletedInitialPrompt(meta);
|
|
3244
|
-
|
|
3496
|
+
// Codexはbind済みのfollow-upでも毎回idleを確認してからtranscript境界を切る。同じcursorへ
|
|
3497
|
+
// 複数turnを帰属させる余地を作らない。他vendorの既存dispatch条件は変えない。
|
|
3498
|
+
if (meta.kind === "codex" || !meta.vendor_session_id) {
|
|
3245
3499
|
const ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
3246
3500
|
if (!ready.ready) {
|
|
3247
3501
|
throw new AitermError(`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。文字列は送信していません。` +
|
|
3248
3502
|
"少し後で pty_read(screen:true) を確認し、TUI が起動済みなら再度 pty_send してください。", 2);
|
|
3249
3503
|
}
|
|
3250
3504
|
}
|
|
3251
|
-
const startOffset =
|
|
3505
|
+
const startOffset = agentCompletionCursor(meta);
|
|
3252
3506
|
if (meta.kind === "claude") {
|
|
3253
3507
|
// durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
|
|
3254
3508
|
// receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
|
|
@@ -3445,6 +3699,8 @@ function buildAgentCmd(kind, bin, model, effort, prompt, meta = null) {
|
|
|
3445
3699
|
if (kind === "claude") {
|
|
3446
3700
|
if (meta?.kind === "claude") {
|
|
3447
3701
|
parts.push("--setting-sources", shq(""), "--settings", shq(meta.claude_settings ?? ""));
|
|
3702
|
+
if (meta.claude_mcp_config)
|
|
3703
|
+
parts.push("--mcp-config", shq(meta.claude_mcp_config));
|
|
3448
3704
|
}
|
|
3449
3705
|
if (model)
|
|
3450
3706
|
parts.push("--model", shq(model));
|
|
@@ -3452,8 +3708,6 @@ function buildAgentCmd(kind, bin, model, effort, prompt, meta = null) {
|
|
|
3452
3708
|
parts.push("--effort", shq(effort));
|
|
3453
3709
|
}
|
|
3454
3710
|
else if (kind === "codex") {
|
|
3455
|
-
if (meta?.kind === "codex")
|
|
3456
|
-
parts.push("--dangerously-bypass-hook-trust");
|
|
3457
3711
|
// `codex --help` で確認した実在フラグ。read-only 宣言だけはCLI sandboxへ落とし、
|
|
3458
3712
|
// launcher自身が実効能力壁を作る。パス説明はCodex CLIに同等のallowlist引数がないため宣言のまま残す。
|
|
3459
3713
|
if (meta?.kind === "codex" && meta.write_scope === "read-only")
|
|
@@ -3554,7 +3808,7 @@ function buildAgentLaunchNote(kind, model, effort, meta) {
|
|
|
3554
3808
|
(effectiveEffort === "ultra"
|
|
3555
3809
|
? "⚠ effort=ultra は max 推論+proactive 自動委譲 ON(子エージェント自動生成・使用量急増に注意)。"
|
|
3556
3810
|
: "");
|
|
3557
|
-
const summary = meta?.kind === "codex" && meta.codex_home ? managedCodexConfigSummary(configPath
|
|
3811
|
+
const summary = meta?.kind === "codex" && meta.codex_home ? managedCodexConfigSummary(configPath) : "";
|
|
3558
3812
|
return (summary ? `${launch}\n${summary}\n` : launch) + writeScopeNote;
|
|
3559
3813
|
}
|
|
3560
3814
|
function claudeLaunchRequestDigest({ sessionName, model, effort, cwd, agentDone, }) {
|
package/dist/index.js
CHANGED
|
@@ -487,13 +487,13 @@ function registerAgentTool(toolName, kind, desc) {
|
|
|
487
487
|
wait_command: eventCursor === null ? null : `aiterm-wait --session ${sid} --cursor ${eventCursor}`,
|
|
488
488
|
submit_residue: submitResidue,
|
|
489
489
|
...(supportsWriteScope && write_scope !== undefined
|
|
490
|
-
? {
|
|
491
|
-
: supportsWriteScope ? {
|
|
490
|
+
? {
|
|
492
491
|
write_scope,
|
|
493
492
|
write_scope_enforcement: kind === "codex" && write_scope === "read-only"
|
|
494
493
|
? "enforced_read_only"
|
|
495
494
|
: "declaration_only_unsupported",
|
|
496
|
-
}
|
|
495
|
+
}
|
|
496
|
+
: {}),
|
|
497
497
|
};
|
|
498
498
|
return {
|
|
499
499
|
content: [{ type: "text", text: `session_id: ${sid}\n${hint}` }],
|
|
@@ -511,6 +511,9 @@ registerAgentTool("claude_agent", "claude", "【Claude Code (Anthropic)】の対
|
|
|
511
511
|
agentCompletionDesc +
|
|
512
512
|
"Claude の durable turn は claude_turn でも回収できる。");
|
|
513
513
|
registerAgentTool("codex_agent", "codex", "【Codex (OpenAI)】の対話エージェント TUI を永続端末に起動する。実装・レビュー・調査を対話で回す。" +
|
|
514
|
+
"委譲契約を使う完全な呼び出し例: " +
|
|
515
|
+
'`codex_agent({"prompt":"<依頼>","model":"gpt-5.6-sol","reasoning_effort":"high",' +
|
|
516
|
+
'"cwd":"/absolute/path/to/repo","write_scope":"read-only"})`。' +
|
|
514
517
|
"turn は pty_send で送る(自動で非ブロック dispatch になる)。" +
|
|
515
518
|
agentCompletionDesc +
|
|
516
519
|
"model / reasoning_effort を引数で指定可" +
|
|
@@ -281,7 +281,7 @@ function validateState(value, maxRecords = 256) {
|
|
|
281
281
|
records: typed.map((record) => projectRecord(record)),
|
|
282
282
|
};
|
|
283
283
|
}
|
|
284
|
-
function processStartIdentity(pid, platform) {
|
|
284
|
+
function processStartIdentity(pid, platform, timeoutMs = 1000) {
|
|
285
285
|
if (!Number.isInteger(pid) || pid < 1)
|
|
286
286
|
return null;
|
|
287
287
|
if (platform === "linux") {
|
|
@@ -298,7 +298,7 @@ function processStartIdentity(pid, platform) {
|
|
|
298
298
|
if (platform === "win32") {
|
|
299
299
|
const script = "$p=Get-Process -Id $env:AITERMMCP_PROCESS_ID -ErrorAction Stop; $p.StartTime.ToUniversalTime().Ticks";
|
|
300
300
|
const result = spawnSync("powershell.exe", ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], {
|
|
301
|
-
encoding: "utf8", timeout:
|
|
301
|
+
encoding: "utf8", timeout: timeoutMs, maxBuffer: 4096, windowsHide: true,
|
|
302
302
|
env: { ...process.env, AITERMMCP_PROCESS_ID: String(pid) },
|
|
303
303
|
});
|
|
304
304
|
const value = result.status === 0 ? (result.stdout ?? "").trim() : "";
|
|
@@ -503,7 +503,8 @@ export class RuntimeErrorStore {
|
|
|
503
503
|
|| (info.mode & 0o777) !== 0o700)
|
|
504
504
|
throw new Error("runtime error lock queue のowner/modeが不正です");
|
|
505
505
|
}
|
|
506
|
-
const
|
|
506
|
+
const identityTimeoutMs = this.platform === "win32" ? this.windowsAclTimeoutMs : 1000;
|
|
507
|
+
const startId = processStartIdentity(process.pid, this.platform, identityTimeoutMs);
|
|
507
508
|
if (!startId)
|
|
508
509
|
throw new Error("process start identity を取得できません");
|
|
509
510
|
const owner = { pid: process.pid, start_id: startId, token: randomBytes(16).toString("hex") };
|
|
@@ -548,7 +549,7 @@ export class RuntimeErrorStore {
|
|
|
548
549
|
}
|
|
549
550
|
if (name !== `choosing-${current.token}.json`)
|
|
550
551
|
throw new Error("runtime error choosing entry が不正です");
|
|
551
|
-
const identity = processStartIdentity(current.pid, this.platform);
|
|
552
|
+
const identity = processStartIdentity(current.pid, this.platform, identityTimeoutMs);
|
|
552
553
|
const live = identity === current.start_id || (!identity && processExists(current.pid));
|
|
553
554
|
if (live)
|
|
554
555
|
hasLiveChoosing = true;
|
|
@@ -583,7 +584,7 @@ export class RuntimeErrorStore {
|
|
|
583
584
|
}
|
|
584
585
|
if (!name.endsWith(`-${current.token}.ticket`))
|
|
585
586
|
throw new Error("runtime error lock ticket が不正です");
|
|
586
|
-
const identity = processStartIdentity(current.pid, this.platform);
|
|
587
|
+
const identity = processStartIdentity(current.pid, this.platform, identityTimeoutMs);
|
|
587
588
|
const live = identity === current.start_id || (!identity && processExists(current.pid));
|
|
588
589
|
if (!live) {
|
|
589
590
|
try {
|
|
@@ -824,13 +825,17 @@ function forceKill(child) {
|
|
|
824
825
|
catch { /* already exited */ }
|
|
825
826
|
return;
|
|
826
827
|
}
|
|
828
|
+
// taskkill 自体の起動が混雑した Windows runner で遅れても、deadline を越えた
|
|
829
|
+
// worker 本体の副作用を許さない。まず Node のハンドルから即時停止し、その後に
|
|
830
|
+
// taskkill /T で worker が残した子孫だけを回収する。
|
|
831
|
+
try {
|
|
832
|
+
child.kill("SIGKILL");
|
|
833
|
+
}
|
|
834
|
+
catch { /* already exited */ }
|
|
827
835
|
const killer = spawn("taskkill.exe", ["/pid", String(child.pid), "/T", "/F"], {
|
|
828
836
|
stdio: "ignore", windowsHide: true,
|
|
829
837
|
});
|
|
830
|
-
killer.once("error", () => {
|
|
831
|
-
child.kill("SIGKILL");
|
|
832
|
-
}
|
|
833
|
-
catch { /* already exited */ } });
|
|
838
|
+
killer.once("error", () => { });
|
|
834
839
|
killer.unref();
|
|
835
840
|
}
|
|
836
841
|
function processExists(pid) {
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.4",
|
|
4
4
|
"mcpName": "io.github.kitepon-rgb/aiterm-mcp",
|
|
5
5
|
"description": "Persistent tmux terminal MCP that lets Claude Code drive Codex CLI's interactive TUI, including slash commands and $imagegen. Also runs durable PTY sessions for SSH, containers, REPLs, and coding agents.",
|
|
6
6
|
"keywords": [
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"node": ">=18"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
|
-
"build": "tsc",
|
|
55
|
+
"build": "node scripts/clean-build.mjs && tsc",
|
|
56
56
|
"mcpb:build": "npm run build && node scripts/build-mcpb.mjs && npm ci --omit=dev --ignore-scripts --no-audit --no-fund --prefix dist/mcpb-stage/server && npx --yes @anthropic-ai/mcpb@2.1.2 validate dist/mcpb-stage/manifest.json && npx --yes @anthropic-ai/mcpb@2.1.2 pack dist/mcpb-stage dist/aiterm-mcp.mcpb",
|
|
57
57
|
"verify:release-commit": "node scripts/verify-release-commit.mjs",
|
|
58
58
|
"prepublishOnly": "npm run verify:release-commit && npm run build",
|
package/dist/codex-stop-hook.js
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
|
-
import * as os from "node:os";
|
|
4
|
-
import * as path from "node:path";
|
|
5
|
-
const LAUNCH_ID_RE = /^[0-9a-f]{32}$/;
|
|
6
|
-
const SESSION_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7
|
-
const MAX_STDIN_BYTES = 1024 * 1024;
|
|
8
|
-
function fail(message) {
|
|
9
|
-
process.stderr.write(`aiterm codex-stop-hook: ${message}\n`);
|
|
10
|
-
process.stdout.write(JSON.stringify({ continue: false }) + "\n");
|
|
11
|
-
process.exit(0);
|
|
12
|
-
}
|
|
13
|
-
function noop() {
|
|
14
|
-
process.stdout.write(JSON.stringify({ continue: false }) + "\n");
|
|
15
|
-
process.exit(0);
|
|
16
|
-
}
|
|
17
|
-
function hasAitermEnv() {
|
|
18
|
-
return !!(process.env.AITERM_AGENT_KIND ||
|
|
19
|
-
process.env.AITERM_SESSION_ID ||
|
|
20
|
-
process.env.AITERM_AGENT_SESSION_ID ||
|
|
21
|
-
process.env.AITERM_AGENT_LAUNCH_ID);
|
|
22
|
-
}
|
|
23
|
-
function uid() {
|
|
24
|
-
if (typeof process.getuid !== "function")
|
|
25
|
-
fail("POSIX getuid が使えません");
|
|
26
|
-
return process.getuid();
|
|
27
|
-
}
|
|
28
|
-
function runtimeStateBase() {
|
|
29
|
-
const xdg = process.env.XDG_RUNTIME_DIR;
|
|
30
|
-
if (xdg) {
|
|
31
|
-
try {
|
|
32
|
-
if (fs.statSync(xdg).isDirectory())
|
|
33
|
-
return xdg;
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
/* XDG_RUNTIME_DIR が壊れている CI/非 login 環境では os.tmpdir() に戻す */
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return os.tmpdir();
|
|
40
|
-
}
|
|
41
|
-
function secureAgentsDir() {
|
|
42
|
-
const root = path.join(runtimeStateBase(), `aiterm-mcp-${uid()}`);
|
|
43
|
-
const agents = path.join(root, "agents");
|
|
44
|
-
const rst = fs.lstatSync(root);
|
|
45
|
-
if (!rst.isDirectory() || rst.isSymbolicLink() || rst.uid !== uid() || (rst.mode & 0o077) !== 0) {
|
|
46
|
-
fail(`agent state root が安全ではありません: ${root}`);
|
|
47
|
-
}
|
|
48
|
-
const ast = fs.lstatSync(agents);
|
|
49
|
-
if (!ast.isDirectory() || ast.isSymbolicLink() || ast.uid !== uid() || (ast.mode & 0o077) !== 0) {
|
|
50
|
-
fail(`agent state dir が安全ではありません: ${agents}`);
|
|
51
|
-
}
|
|
52
|
-
return agents;
|
|
53
|
-
}
|
|
54
|
-
function str(v) {
|
|
55
|
-
return typeof v === "string" ? v : null;
|
|
56
|
-
}
|
|
57
|
-
async function readStdin() {
|
|
58
|
-
const chunks = [];
|
|
59
|
-
let total = 0;
|
|
60
|
-
for await (const chunk of process.stdin) {
|
|
61
|
-
const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
62
|
-
total += b.length;
|
|
63
|
-
if (total > MAX_STDIN_BYTES)
|
|
64
|
-
fail("payload が大きすぎます");
|
|
65
|
-
chunks.push(b);
|
|
66
|
-
}
|
|
67
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
68
|
-
}
|
|
69
|
-
function appendEvent(file, event) {
|
|
70
|
-
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
71
|
-
const line = JSON.stringify(event) + "\n";
|
|
72
|
-
if (Buffer.byteLength(line, "utf8") > 64 * 1024)
|
|
73
|
-
fail("event line が大きすぎます");
|
|
74
|
-
const fd = fs.openSync(file, fs.constants.O_CREAT | fs.constants.O_APPEND | fs.constants.O_WRONLY | nofollow, 0o600);
|
|
75
|
-
try {
|
|
76
|
-
const st = fs.fstatSync(fd);
|
|
77
|
-
if (!st.isFile() || st.uid !== uid() || st.nlink !== 1 || (st.mode & 0o077) !== 0) {
|
|
78
|
-
fail(`event file が安全ではありません: ${file}`);
|
|
79
|
-
}
|
|
80
|
-
const written = fs.writeSync(fd, line, undefined, "utf8");
|
|
81
|
-
if (written < Buffer.byteLength(line, "utf8")) {
|
|
82
|
-
fs.ftruncateSync(fd, st.size);
|
|
83
|
-
fail(`event file への書込みが途中で終了しました: ${file}`);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
finally {
|
|
87
|
-
fs.closeSync(fd);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
async function main() {
|
|
91
|
-
if (!hasAitermEnv())
|
|
92
|
-
noop();
|
|
93
|
-
const kind = process.env.AITERM_AGENT_KIND;
|
|
94
|
-
const session = process.env.AITERM_SESSION_ID || process.env.AITERM_AGENT_SESSION_ID || "";
|
|
95
|
-
const launchId = process.env.AITERM_AGENT_LAUNCH_ID || "";
|
|
96
|
-
if (kind !== "codex")
|
|
97
|
-
fail(`AITERM_AGENT_KIND が codex ではありません: ${kind ?? ""}`);
|
|
98
|
-
if (!SESSION_RE.test(session))
|
|
99
|
-
fail(`session id が不正です: ${session}`);
|
|
100
|
-
if (!LAUNCH_ID_RE.test(launchId))
|
|
101
|
-
fail(`launch id が不正です: ${launchId}`);
|
|
102
|
-
let payload = {};
|
|
103
|
-
const input = await readStdin();
|
|
104
|
-
if (input.trim()) {
|
|
105
|
-
try {
|
|
106
|
-
payload = JSON.parse(input);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
payload = {};
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const agents = secureAgentsDir();
|
|
113
|
-
const eventFile = path.join(agents, `${session}.${launchId}.events.jsonl`);
|
|
114
|
-
appendEvent(eventFile, {
|
|
115
|
-
type: "agent_done",
|
|
116
|
-
vendor: "codex",
|
|
117
|
-
aiterm_session: session,
|
|
118
|
-
launch_id: launchId,
|
|
119
|
-
vendor_session_id: str(payload.session_id),
|
|
120
|
-
turn_id: str(payload.turn_id),
|
|
121
|
-
reason: str(payload.hook_event_name) ?? "Stop",
|
|
122
|
-
done_status: "turn_done",
|
|
123
|
-
stop_hook_active: !!payload.stop_hook_active,
|
|
124
|
-
at: new Date().toISOString(),
|
|
125
|
-
});
|
|
126
|
-
process.stdout.write(JSON.stringify({ continue: false }) + "\n");
|
|
127
|
-
}
|
|
128
|
-
main().catch((e) => fail(e instanceof Error ? e.message : String(e)));
|