aiterm-mcp 0.35.0 → 0.36.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,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.36.0] - 2026-09-13
11
+
12
+ ### 追加
13
+
14
+ - macOSのCodex Desktop向けにSteerの選択導入を追加。`aiterm-setup`の対話選択または`--codex-steer enable`で、同じ公式App Serverへ回答を送り、実行中は同じターンへ反映、終了後は同じタスクを再開する。
15
+ - 中継・POSIX起動処理をnpm packageへ同梱。公式App Serverの再ビルド、別配布、Pythonの追加導入は不要。公式バイナリのPID・Desktopとの親子関係・署名・通常環境を維持する。
16
+ - `--codex-steer status|disable`で実効状態の確認と元の設定への復元を行う。ログイン時の設定は専用LaunchAgentで維持する。初回は`restart_required`と終了コード3を返し、Codexの完全再起動を必要とする。
17
+ - Steerを選択した環境で接続できない時は明示エラーとし、queueへ自動退避しない。送信結果不明の回答は従来どおり保存し、自動再送しない。配送record schemaは維持し、Steer受付済みの`queued_submission_id`はnullとなる。
18
+ - Steerを持たない旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行する。Windows・LinuxのSteer選択は未対応を明示し、単品導入と通常のqueue配送は維持する。
19
+
20
+ ### 修正
21
+
22
+ - 公開コマンドが配布物の検査前にbuildを行い、clean cloneからの公開で未生成の`dist/`を参照して失敗する問題を修正した。
23
+ - 同一エラーの再発時に`product_version`を更新し、snapshotに最終実発生時の版を公開する。
24
+ - runtime storeをv2へ移行する。旧v1の単発記録は版を保持し、複数回の旧集約は最終発生版を復元できないため`unknown`にする。読取りで状態JSONを書き戻さず、次のロック内更新でv2を保存する。
25
+ - v2を保存したstoreは旧writerで開かない。consumerを先に更新し、旧プロセスを終了してから新writerへ切り替える。旧版へ戻す場合は製品のバックアップを使い、v2をv1に偽装しない。
26
+
27
+ ## [0.35.1] - 2026-09-10
28
+
29
+ ### Fixed
30
+
31
+ - Claudeの起動時に、指定された作業ディレクトリの実体パスを記録する。macOSの`/var`やリンク経由のcwdでも、会話記録を使うAPIエラー検出が正しい保存場所を参照する。
32
+
10
33
  ## [0.35.0] - 2026-09-10
11
34
 
12
35
  ### Added
@@ -1634,7 +1657,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1634
1657
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1635
1658
  provenance.
1636
1659
 
1637
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.0...HEAD
1660
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.36.0...HEAD
1661
+ [0.36.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.1...v0.36.0
1662
+ [0.35.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.35.0...v0.35.1
1638
1663
  [0.35.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.34.0...v0.35.0
1639
1664
  [0.34.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.33.1...v0.34.0
1640
1665
  [0.33.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.33.0...v0.33.1
package/README.ja.md CHANGED
@@ -40,11 +40,31 @@ WindowsはwingetでPowerShell 7・Git for Windows・psmux、macOSはHomebrewでt
40
40
  Ubuntu/Debianはsudoとaptでtmuxを準備する。必要な公式package managerと実行権限は事前に必要。
41
41
  他のLinuxでも既存tmuxを利用できるが、自動導入は`unsupported`で停止する。
42
42
  既存設定の他サーバーを保持し、JSON設定は変更前の`.aiterm-backup`を残す。
43
- 結果の`status`は`ready`/`unsupported`/`failed`。未検出のAIは`not_detected`とし、全AI未検出は成功にしない。
43
+ 結果の`status`は`ready`/`unsupported`/`failed`/`restart_required`。未検出のAIは`not_detected`とし、全AI未検出は成功にしない。
44
44
  登録先はglobal packageのNodeとMCP入口の絶対パスで、npm一時cacheやsource checkoutは登録しない。
45
45
  更新後も同じ入口を実行し、MCP clientを再起動する。npm install自体はユーザー設定を変更しない。
46
46
  公開JSONは`schema: "aiterm.setup-result.v1"`、全体の`status`、端末の`backend`、
47
- AI別の`integrations`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、それ以外は2となる。
47
+ AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、再起動待ちは3、それ以外は2となる。
48
+
49
+
50
+ ### Codex DesktopへSteerを有効にする(macOS)
51
+
52
+ 対話実行の`aiterm-setup`で「Aiterm単品」と「Steer付き」を選べます。無人導入では明示します。
53
+
54
+ ```bash
55
+ aiterm-setup --json --codex-steer enable
56
+ ```
57
+
58
+ 公式Codex Desktopに同梱されたApp Serverを使い、実行中の親には同じターンへ回答を送り、
59
+ 終了後に届いた回答でも同じタスクを自動再開します。App Serverの改造・再ビルド・別配布は不要です。
60
+ Steerの初回設定後は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
61
+ `aiterm-setup --codex-steer status`で`ready`を確認してください。接続できない時にキューへ自動退避しません。
62
+
63
+ 選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存し、ユーザーLaunchAgentがログイン時にも
64
+ 起動設定を適用します。更新時の`aiterm-setup --json`は選択を維持して中継を更新します。
65
+ 解除・旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
66
+ SteerはmacOSのCodex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
67
+ Windows・LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
48
68
 
49
69
  cloneもビルドも不要。どのクライアントでも公開パッケージを次のコマンドで起動する:
50
70
 
@@ -171,7 +191,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
171
191
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
172
192
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
173
193
 
174
- **状態:** 開発継続中 · 現行公開版 **v0.35.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
194
+ **状態:** 開発継続中 · 現行公開版 **v0.36.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
175
195
 
176
196
  ### 更新と巻き戻し
177
197
 
@@ -205,7 +225,7 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
205
225
 
206
226
  同じ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
227
 
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親は公式受信キュー、Claude Code親は公式非同期hookで回答本文を自動受信する。それ以外の親は`aiterm-wait`を親のターンを塞がない別processで受ける。Cursorのsubmitキーはadapterが現行CLIのextended keyboard protocolへ変換する。送信textがCursorのcomposerへ残った場合は成功receiptを返さず失敗する。
228
+ `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親は選択に応じて公式Steerまたは受信キュー、Claude Code親は公式非同期hookで回答本文を自動受信する。それ以外の親は`aiterm-wait`を親のターンを塞がない別processで受ける。Cursorのsubmitキーはadapterが現行CLIのextended keyboard protocolへ変換する。送信textがCursorのcomposerへ残った場合は成功receiptを返さず失敗する。
209
229
 
210
230
  `agent_launch`・`pty_send`(agent dispatch)・`agent_steer`は任意の`image`(画像ファイルの絶対パスの配列。png/jpg/jpeg/gif/webp)を受ける。aitermが本文末尾へ添付行を付け、どのharnessも自分のfile読取toolでそのpathを画像として開く。呼出し側はharness別の添付手順を覚えない。不正なpathは送信前に拒否する。
211
231
 
@@ -518,9 +538,9 @@ SSH先がPowerShellの場合、`mark:true`は現在の標準`PS ...>`プロン
518
538
 
519
539
  Codex/Claude Codeから子を起動・通常dispatchした後は、別作業へ進むか親のturnを終了するだけでよい。Aitermが完了を観測し、加工前の回答を保存して親へ届ける。waiter、`pty_read`による回答回収、子への送信指示は不要。子は全対応harnessから選べる。
520
540
 
521
- 自動配送時はreceiptに`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はCodexのキュー受付またはClaudeのhookへの本文出力を示し、modelの読了ではない。MCP再接続後は未送信の記録を再開し、送信中に接続が切れて結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
541
+ 自動配送時はreceiptに`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はCodexの公式受信口での受付またはClaudeのhookへの本文出力を示し、modelの読了ではない。MCP再接続後は未送信の記録を再開し、送信中に接続が切れて結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
522
542
 
523
- CodexにはMCPの`_meta.threadId`と公式`thread/queue` APIが必要で、Codex CLI 0.154.0で確認している。`aiterm-setup`はインストールされた公式queue入口を確認し、各dispatchでは実際の親threadの受入可否を確認する。Codexのnative sub-agentは外部からのqueue入力を拒否するため、自動配送の親としては未対応。
543
+ 単品導入のCodexにはMCPの`_meta.threadId`と公式`thread/queue` APIが必要で、Codex CLI 0.154.0で確認している。`aiterm-setup`はインストールされた公式queue入口を確認し、各dispatchでは実際の親threadの受入可否を確認する。Codexのnative sub-agentは外部からのqueue入力を拒否するため、自動配送の親としては未対応。
524
544
 
525
545
  Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が専用の`PreToolUse`、`PostToolUse`、`SessionEnd`を登録するため、Channelsの起動flagは不要。公式`asyncRewake` hookだけが裏で待ち、親はその間も次のturnへ進める。回答は`Stop hook feedback`として届く。hookのexit 2は親の再開信号であり、子の成功・失敗は本文の`outcome`で区別する。
526
546
 
@@ -528,6 +548,8 @@ Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が
528
548
 
529
549
  hookを持たない旧版へ戻す時は、install前に`aiterm-setup --remove-claude-parent-hooks`を実行する。Aiterm専用hookだけを解除し、他製品のhookと設定は保持する。
530
550
 
551
+ Claudeをリンク経由の`cwd`から起動した場合も、実体パスに対応する会話記録を参照する。
552
+
531
553
 
532
554
  ### トークン削減
533
555
 
package/README.md CHANGED
@@ -40,11 +40,31 @@ WindowsはwingetでPowerShell 7・Git for Windows・psmux、macOSはHomebrewでt
40
40
  Ubuntu/Debianはsudoとaptでtmuxを準備する。必要な公式package managerと実行権限は事前に必要。
41
41
  他のLinuxでも既存tmuxを利用できるが、自動導入は`unsupported`で停止する。
42
42
  既存設定の他サーバーを保持し、JSON設定は変更前の`.aiterm-backup`を残す。
43
- 結果の`status`は`ready`/`unsupported`/`failed`。未検出のAIは`not_detected`とし、全AI未検出は成功にしない。
43
+ 結果の`status`は`ready`/`unsupported`/`failed`/`restart_required`。未検出のAIは`not_detected`とし、全AI未検出は成功にしない。
44
44
  登録先はglobal packageのNodeとMCP入口の絶対パスで、npm一時cacheやsource checkoutは登録しない。
45
45
  更新後も同じ入口を実行し、MCP clientを再起動する。npm install自体はユーザー設定を変更しない。
46
46
  公開JSONは`schema: "aiterm.setup-result.v1"`、全体の`status`、端末の`backend`、
47
- AI別の`integrations`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、それ以外は2となる。
47
+ AI別の`integrations`と選択機能の`codex_steer`を持つ。失敗時は`reason_code`を付け、終了コードはreadyなら0、再起動待ちは3、それ以外は2となる。
48
+
49
+
50
+ ### Codex DesktopへSteerを有効にする(macOS)
51
+
52
+ 対話実行の`aiterm-setup`で「Aiterm単品」と「Steer付き」を選べます。無人導入では明示します。
53
+
54
+ ```bash
55
+ aiterm-setup --json --codex-steer enable
56
+ ```
57
+
58
+ 公式Codex Desktopに同梱されたApp Serverを使い、実行中の親には同じターンへ回答を送り、
59
+ 終了後に届いた回答でも同じタスクを自動再開します。App Serverの改造・再ビルド・別配布は不要です。
60
+ Steerの初回設定後は`restart_required`(終了コード3)を返します。Codexを完全終了して再起動し、
61
+ `aiterm-setup --codex-steer status`で`ready`を確認してください。接続できない時にキューへ自動退避しません。
62
+
63
+ 選択と復元情報は`~/.config/aiterm-mcp/codex-relay/`へ保存し、ユーザーLaunchAgentがログイン時にも
64
+ 起動設定を適用します。更新時の`aiterm-setup --json`は選択を維持して中継を更新します。
65
+ 解除・旧版への巻き戻し前は`aiterm-setup --codex-steer disable`を実行してCodexを再起動してください。
66
+ SteerはmacOSのCodex Desktopと同梱CLI 0.154以上に対応します。Steer有効時の宛先は中継で起動したDesktopに限ります。
67
+ Windows・LinuxのSteer付き導入は理由付き`unsupported`を返します。Aiterm単品は従来どおり利用できます。
48
68
 
49
69
  No clone or build is required. Each client launches the published package with:
50
70
 
@@ -185,7 +205,7 @@ collection is off by default and performs no network I/O. It ships via
185
205
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
186
206
  Release re-registers the Official MCP Registry entry.
187
207
 
188
- **Status:** actively maintained · current public release **v0.35.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).
208
+ **Status:** actively maintained · current public release **v0.36.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).
189
209
 
190
210
  ### Update and rollback
191
211
 
@@ -225,7 +245,7 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
225
245
 
226
246
  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`.
227
247
 
228
- 起動結果には正規`harness`を含む`aiterm.agent-launch-result.v1`が付き、旧`provider`は互換fieldとして残る。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`にも載る。Codexは通常rollout、Grokは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcriptの`turn_ended`を完了正本に使う。agentへの送信は非ブロックdispatchで、harnessごとの完了境界を表す整数`event_cursor`を返す。Codex親は公式queue、Claude Code親は公式非同期hookで本文を自動受信する。他の親は[`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait)を使う。CursorのsubmitはadapterがCLIのextended keyboard protocolへ変換し、送信本文がcomposerへ残る場合は明示errorにする。
248
+ 起動結果には正規`harness`を含む`aiterm.agent-launch-result.v1`が付き、旧`provider`は互換fieldとして残る。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`にも載る。Codexは通常rollout、Grokは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcriptの`turn_ended`を完了正本に使う。agentへの送信は非ブロックdispatchで、harnessごとの完了境界を表す整数`event_cursor`を返す。Codex親は選択に応じて公式Steerまたはqueue、Claude Code親は公式非同期hookで本文を自動受信する。他の親は[`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait)を使う。CursorのsubmitはadapterがCLIのextended keyboard protocolへ変換し、送信本文がcomposerへ残る場合は明示errorにする。
229
249
 
230
250
  `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.
231
251
 
@@ -513,6 +533,8 @@ continue to use `claude_approval`.
513
533
 
514
534
  ### Local runtime error snapshot
515
535
 
536
+ snapshotの`product_version`は各recordの最終実発生時の版を表す。store v2は旧v1を読み取り、単発記録の版を保持し、複数回の旧集約の版は`unknown`にする。読取りでは状態JSONを書き戻さず、次のロック内更新でv2を保存する。consumerを先に更新し、旧writerの終了後に新writerを使う。v2保存後の旧版への切替は、製品のバックアップ復元を伴う。
537
+
516
538
  `aiterm-runtime-errors snapshot` exposes a machine-readable, product-owned local snapshot for the dotagents factory adapter. Collection is fail-closed unless the canonical dotagents factory-reporter config is schema-exact, its host profile matches the executing OS, and it contains the JSON boolean `collection.enabled: true`; reporting fields are schema-validated but endpoints and credential files are never contacted, and the store performs no network I/O. The only accepted observations are three fixed codes owned by the core boundary (PTY dependency, persistence, and optional vendor launcher). Stored data is limited to fixed templates and aggregate metadata (SHA-256 fingerprint, count, first/last seen, status, and monotonic sequence); exceptions, stderr/stdout, stacks, prompts, terminal/transcript/event bodies, paths, and arbitrary context cannot enter the API. Persisted JSON is revalidated with exact top/record fields and a recomputed fingerprint before explicit DTO projection.
517
539
 
518
540
  Consumer flow is `aiterm-runtime-errors snapshot`, then `aiterm-runtime-errors ack --cursor N` after durable ingestion. Operators can use `resolve|reopen --fingerprint SHA256`. MCP collection and diagnostic reads run in timeout-bounded child processes, so a FIFO or stalled filesystem cannot block terminal work; child failure emits only the fixed store diagnostic. Store mutation uses a bounded bakery ticket queue: every waiter owns a never-reused ticket containing PID, process-start identity, and an owner token, so dead owners are removed by unique filename without fixed-path reclaim ABA. The queue deadline measures lack of progress by the same head owner, not total wait behind healthy predecessors; normal polling uses the native process-liveness check and validates process-start identity only when a blocker stalls. Worker deadlines use forced termination so a SIGTERM-ignoring child cannot mutate state after timeout. POSIX state is atomically replaced under `$XDG_STATE_HOME/aiterm-mcp/` (default `~/.local/state/aiterm-mcp/`) with owner/mode rechecked on every read. Windows native uses `%LOCALAPPDATA%\aiterm-mcp\`; each DACL is rebuilt and read back as one non-inherited FullControl ACE for the current SID. Windows path/DACL/timeout behavior is covered by pure tests in this change; no new Windows integration success is claimed.
@@ -551,9 +573,9 @@ For PowerShell over SSH, `mark:true` recognizes the current standard `PS ...>` p
551
573
 
552
574
  **Codex/Claude Code親には子の回答本文が自動で届く。** 子を起動・dispatchした後は、別作業へ進むか親のturnを終える。Aitermが完了を観測し、加工前の本文を保存して親へ渡す。waiter、`pty_read`による回答回収、子への返送指示は不要。子は全対応harnessから選べる。
553
575
 
554
- 自動配送のreceiptには`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はCodexのキュー受付またはClaudeのhookへの本文出力を示し、modelの読了ではない。MCP再接続後は未送信の記録を再開し、出力中断で結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
576
+ 自動配送のreceiptには`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はCodexの公式受信口での受付またはClaudeのhookへの本文出力を示し、modelの読了ではない。MCP再接続後は未送信の記録を再開し、出力中断で結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
555
577
 
556
- 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.
578
+ For queue delivery, 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. This queue route applies when Desktop Steer is not enabled.
557
579
 
558
580
  Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が専用の`PreToolUse`、`PostToolUse`、`SessionEnd`を登録するため、Channelsの起動flagは不要。公式`asyncRewake` hookだけが裏で待ち、親はその間も次のturnへ進める。回答は`Stop hook feedback`として届く。hookのexit 2は親の再開信号であり、子の成功・失敗は本文の`outcome`で区別する。
559
581
 
@@ -561,6 +583,8 @@ Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が
561
583
 
562
584
  hookを持たない旧版へ戻す時は、install前に`aiterm-setup --remove-claude-parent-hooks`を実行する。Aiterm専用hookだけを解除し、他製品のhookと設定は保持する。
563
585
 
586
+ Claudeをリンク経由の`cwd`から起動した場合も、実体パスに対応する会話記録を参照する。
587
+
564
588
  **For other parent hosts**, dispatch and start the receipt's waiter in a separate process:
565
589
 
566
590
  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.
@@ -0,0 +1,10 @@
1
+ import { AitermError } from "./errors.js";
2
+ export class CodexDeliveryError extends AitermError {
3
+ delivery_code;
4
+ outcome_unknown;
5
+ constructor(delivery_code, message, outcome_unknown = false) {
6
+ super(`${delivery_code}: ${message}`, 2);
7
+ this.delivery_code = delivery_code;
8
+ this.outcome_unknown = outcome_unknown;
9
+ }
10
+ }
@@ -1,19 +1,13 @@
1
- // Codex親の公式受信キューへの接続。親threadのload/resumeやDesktop固有通信は行わない。
1
+ // Codex親の配送。Steerを選択した環境は同じ公式App Server、それ以外は公式queueを使う。
2
2
  import { spawn } from "node:child_process";
3
3
  import { createInterface } from "node:readline";
4
4
  import * as path from "node:path";
5
5
  import { resolveAgentBin } from "./agent-resolver.js";
6
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
- }
7
+ import { CodexDeliveryError } from "./codex-delivery-error.js";
8
+ import { readRelayConfig, parentRelaySocket } from "./codex-relay-config.js";
9
+ import { withCodexRelay, verifyLoadedParent } from "./codex-relay-client.js";
10
+ export { CodexDeliveryError } from "./codex-delivery-error.js";
17
11
  /** modelの引数ではなく、CodexがMCP要求へ付けるmetadataだけを宛先にする。 */
18
12
  export function codexParentFromRequest(clientName, metadata) {
19
13
  if (clientName !== "codex-mcp-client")
@@ -24,6 +18,12 @@ export function codexParentFromRequest(clientName, metadata) {
24
18
  }
25
19
  return { thread_id: threadId, codex_home: path.resolve(realCodexHome()) };
26
20
  }
21
+ function relaySocket(runtime) {
22
+ if (runtime)
23
+ return runtime.socket_path ?? null;
24
+ const config = readRelayConfig();
25
+ return config?.enabled ? parentRelaySocket(config) : null;
26
+ }
27
27
  async function withCodexReceiver(parent, action, runtime = {}) {
28
28
  const executable = runtime.executable ?? resolveAgentBin("codex");
29
29
  if (!executable)
@@ -108,6 +108,11 @@ async function withCodexReceiver(parent, action, runtime = {}) {
108
108
  }
109
109
  /** 子へ送る前に、同じstoreの宛先と公式キューの対応を確認する。本文は保存・表示しない。 */
110
110
  export async function verifyCodexParent(parent, runtime) {
111
+ const socket = relaySocket(runtime);
112
+ if (socket) {
113
+ await withCodexRelay(socket, request => verifyLoadedParent(request, parent.thread_id), runtime?.timeout_ms);
114
+ return;
115
+ }
111
116
  await withCodexReceiver(parent, async (request) => {
112
117
  const response = await request("thread/read", { threadId: parent.thread_id, includeTurns: false });
113
118
  if (response?.thread?.id !== parent.thread_id) {
@@ -121,6 +126,19 @@ export async function verifyCodexParent(parent, runtime) {
121
126
  }, runtime);
122
127
  }
123
128
  export async function submitCodexParentAnswer(parent, deliveryId, text, runtime) {
129
+ const socket = relaySocket(runtime);
130
+ if (socket) {
131
+ return withCodexRelay(socket, async (request) => {
132
+ await verifyLoadedParent(request, parent.thread_id);
133
+ // 公式の同一処理内で、実行中はSteer、終了済みなら開始する。本文は一度だけ送る。
134
+ const result = await request("turn/start", { threadId: parent.thread_id,
135
+ input: [{ type: "text", text, text_elements: [] }], clientUserMessageId: deliveryId });
136
+ if (typeof result?.turn?.id !== "string" || !result.turn.id) {
137
+ throw new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "配送先turnの受付IDを確認できません", true);
138
+ }
139
+ return { queued_submission_id: null };
140
+ }, runtime?.timeout_ms);
141
+ }
124
142
  return withCodexReceiver(parent, async (request) => {
125
143
  const result = await request("thread/queue/add", {
126
144
  threadId: parent.thread_id,
@@ -0,0 +1,110 @@
1
+ import { createConnection } from "node:net";
2
+ import WebSocket from "ws";
3
+ import { CodexDeliveryError } from "./codex-delivery-error.js";
4
+ import { verifyRelaySocket } from "./codex-relay-config.js";
5
+ /** 公式受付へ追加接続する。Desktopへの承認要求や通知には応答しない。 */
6
+ export async function withCodexRelay(socketPath, action, timeout = 15_000) {
7
+ try {
8
+ verifyRelaySocket(socketPath);
9
+ }
10
+ catch (error) {
11
+ if (error instanceof CodexDeliveryError)
12
+ throw error;
13
+ throw new CodexDeliveryError("CODEX_RELAY_UNAVAILABLE", "公式App Serverのsocketがありません。Codexを再起動してください");
14
+ }
15
+ const socket = new WebSocket("ws://localhost/rpc", {
16
+ createConnection: () => createConnection(socketPath), handshakeTimeout: timeout, perMessageDeflate: false,
17
+ });
18
+ let sequence = 0;
19
+ let failure = null;
20
+ const pending = new Map();
21
+ const fail = (message) => {
22
+ failure = message;
23
+ for (const item of pending.values()) {
24
+ clearTimeout(item.timer);
25
+ item.reject(new CodexDeliveryError("CODEX_RELAY_TRANSPORT_FAILED", message, item.writing));
26
+ }
27
+ pending.clear();
28
+ };
29
+ socket.on("error", () => fail("公式App Serverとの通信が失敗しました"));
30
+ socket.on("close", () => fail("公式App Serverとの接続が終了しました"));
31
+ socket.on("message", (data, binary) => {
32
+ let value;
33
+ try {
34
+ if (binary)
35
+ throw new Error();
36
+ value = JSON.parse(data.toString());
37
+ }
38
+ catch {
39
+ fail("公式App ServerのJSON応答を読めません");
40
+ return;
41
+ }
42
+ if (value?.method !== undefined)
43
+ return;
44
+ const item = pending.get(value?.id);
45
+ if (!item)
46
+ return;
47
+ clearTimeout(item.timer);
48
+ pending.delete(value.id);
49
+ if (value.error)
50
+ item.reject(new CodexDeliveryError("CODEX_RECEIVER_REJECTED", typeof value.error.message === "string" ? value.error.message : "公式受信口が要求を拒否しました"));
51
+ else if ("result" in value)
52
+ item.resolve(value.result);
53
+ else
54
+ item.reject(new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "公式受信口の応答にresultがありません", item.writing));
55
+ });
56
+ const request = (method, params) => new Promise((resolve, reject) => {
57
+ if (failure || socket.readyState !== WebSocket.OPEN) {
58
+ reject(new CodexDeliveryError("CODEX_RELAY_UNAVAILABLE", failure ?? "接続が開いていません"));
59
+ return;
60
+ }
61
+ const id = ++sequence;
62
+ const writing = method === "turn/start";
63
+ const timer = setTimeout(() => {
64
+ pending.delete(id);
65
+ reject(new CodexDeliveryError("CODEX_RECEIVER_TIMEOUT", `${method}の応答を確認できません`, writing));
66
+ }, timeout);
67
+ pending.set(id, { resolve, reject, timer, writing });
68
+ socket.send(JSON.stringify({ id, method, params }), error => { if (error)
69
+ fail("公式App Serverへの送信に失敗しました"); });
70
+ });
71
+ try {
72
+ await new Promise((resolve, reject) => {
73
+ socket.once("open", resolve);
74
+ socket.once("error", () => reject(new CodexDeliveryError("CODEX_RELAY_UNAVAILABLE", "公式App Serverへ接続できません")));
75
+ socket.once("close", () => reject(new CodexDeliveryError("CODEX_RELAY_UNAVAILABLE", "公式App Serverが接続を閉じました")));
76
+ });
77
+ await request("initialize", { clientInfo: { name: "aiterm_parent_delivery", version: "1" } });
78
+ socket.send(JSON.stringify({ method: "initialized" }));
79
+ return await action(request);
80
+ }
81
+ finally {
82
+ for (const item of pending.values())
83
+ clearTimeout(item.timer);
84
+ pending.clear();
85
+ socket.terminate();
86
+ }
87
+ }
88
+ export async function verifyLoadedParent(request, threadId) {
89
+ let cursor = null;
90
+ do {
91
+ const result = await request("thread/loaded/list", { limit: 100, ...(cursor ? { cursor } : {}) });
92
+ if (!Array.isArray(result?.data))
93
+ throw new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "実行中taskの一覧を確認できません");
94
+ if (result.data.includes(threadId)) {
95
+ const read = await request("thread/read", { threadId, includeTurns: false });
96
+ if (read?.thread?.id !== threadId)
97
+ throw new CodexDeliveryError("CODEX_PARENT_UNAVAILABLE", "親taskの識別が一致しません");
98
+ const subagent = read.thread.source?.subAgent;
99
+ if (subagent && typeof subagent === "object" && "thread_spawn" in subagent) {
100
+ throw new CodexDeliveryError("CODEX_PARENT_UNSUPPORTED", "Codexのnative sub-agentは外部processからの直接入力を受け付けません");
101
+ }
102
+ return;
103
+ }
104
+ const next = result.nextCursor ?? null;
105
+ if (next !== null && (typeof next !== "string" || next === cursor))
106
+ throw new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "task一覧の続きが不正です");
107
+ cursor = next;
108
+ } while (cursor);
109
+ throw new CodexDeliveryError("CODEX_PARENT_UNAVAILABLE", "同じ公式App Serverに親taskがありません。別processでのresumeやqueueへの退避は行いません");
110
+ }
@@ -0,0 +1,66 @@
1
+ // Aitermの選択設定と、同じ親processが所有する公式socketの識別。
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { z } from "zod";
6
+ import { readRuntimeProcesses } from "./process-runtime.js";
7
+ import { CodexDeliveryError } from "./codex-delivery-error.js";
8
+ export const relayConfigSchema = z.object({
9
+ schema: z.literal("aiterm.codex-relay.v1"),
10
+ enabled: z.boolean(),
11
+ binary: z.string(), node: z.string(), launcher: z.string(), socket_root: z.string(),
12
+ previous_cli_path: z.string().nullable(),
13
+ }).strict();
14
+ export function relayConfigDirectory(home = process.env.HOME ?? homedir()) {
15
+ return path.join(home, ".config", "aiterm-mcp", "codex-relay");
16
+ }
17
+ export function readRelayConfig(directory = relayConfigDirectory()) {
18
+ const file = path.join(directory, "config.json");
19
+ let text;
20
+ try {
21
+ text = fs.readFileSync(file, "utf8");
22
+ }
23
+ catch (error) {
24
+ if (error.code === "ENOENT")
25
+ return null;
26
+ throw error;
27
+ }
28
+ try {
29
+ return relayConfigSchema.parse(JSON.parse(text));
30
+ }
31
+ catch {
32
+ throw new CodexDeliveryError("CODEX_RELAY_CONFIG_INVALID", "Steerの設定を読めません。aiterm-setup --codex-steer statusで確認してください");
33
+ }
34
+ }
35
+ export function prepareRelayDirectory(directory) {
36
+ if (!path.isAbsolute(directory))
37
+ throw new CodexDeliveryError("CODEX_RELAY_PATH_INVALID", "socketディレクトリが絶対pathではありません");
38
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
39
+ const stat = fs.lstatSync(directory);
40
+ if (!stat.isDirectory() || stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o700) {
41
+ throw new CodexDeliveryError("CODEX_RELAY_PATH_INVALID", "socketディレクトリは本人所有の0700である必要があります");
42
+ }
43
+ }
44
+ export function verifyRelaySocket(socket) {
45
+ const directory = fs.lstatSync(path.dirname(socket));
46
+ const stat = fs.lstatSync(socket);
47
+ if (!directory.isDirectory() || directory.uid !== process.getuid?.() || (directory.mode & 0o777) !== 0o700
48
+ || !stat.isSocket() || stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o600) {
49
+ throw new CodexDeliveryError("CODEX_RELAY_PATH_INVALID", "本人所有の公式socketを確認できません");
50
+ }
51
+ }
52
+ export function parentRelaySocket(config, processes = readRuntimeProcesses(), pid = process.pid) {
53
+ const rows = new Map(processes.map(row => [row.pid, row]));
54
+ const seen = new Set();
55
+ let current = rows.get(pid)?.parent_pid;
56
+ while (current && !seen.has(current)) {
57
+ seen.add(current);
58
+ const socket = path.join(config.socket_root, `${current}.sock`);
59
+ if (fs.existsSync(socket)) {
60
+ verifyRelaySocket(socket);
61
+ return socket;
62
+ }
63
+ current = rows.get(current)?.parent_pid;
64
+ }
65
+ throw new CodexDeliveryError("CODEX_STEER_RESTART_REQUIRED", "Steerを有効にしたCodex Desktopの親接続がありません。aiterm-setupの結果を確認し、Codexを再起動してください");
66
+ }
@@ -0,0 +1,63 @@
1
+ // POSIXのexecで公式CLIのPIDとDesktopからの親子関係を維持する。
2
+ const quote = (text) => `'${text.replace(/'/g, `'"'"'`)}'`;
3
+ export function codexRelayLauncher(options) {
4
+ return `#!/bin/sh
5
+ binary=${quote(options.binary)}
6
+ node=${quote(options.node)}
7
+ relay=${quote(options.relay)}
8
+ root=${quote(options.socket_root)}
9
+ mode=root
10
+ value=no
11
+ for arg do
12
+ if [ "$value" = yes ]; then value=no; continue; fi
13
+ case "$arg" in
14
+ -c|--config|--enable|--disable|--listen) value=yes; continue ;;
15
+ esac
16
+ if [ "$mode" = root ]; then
17
+ case "$arg" in
18
+ --config=*|--enable=*|--disable=*|-c?*) ;;
19
+ app-server) mode=server ;;
20
+ *) mode=other; break ;;
21
+ esac
22
+ else
23
+ case "$arg" in
24
+ proxy|start|stop|status|generate-ts|generate-json-schema|--help|-h|--version|-V) mode=other; break ;;
25
+ esac
26
+ fi
27
+ done
28
+ if [ "$mode" != server ]; then exec "$binary" "$@"; fi
29
+
30
+ # 引数を一巡させ、stdio指定だけを公式Unix受付へ変更する。
31
+ remaining=$#
32
+ value=no
33
+ while [ "$remaining" -gt 0 ]; do
34
+ arg=$1
35
+ shift
36
+ remaining=$((remaining - 1))
37
+ if [ "$value" = yes ]; then
38
+ set -- "$@" "$arg"
39
+ value=no
40
+ continue
41
+ fi
42
+ case "$arg" in
43
+ -c|--config|--enable|--disable) value=yes; set -- "$@" "$arg" ;;
44
+ --stdio) ;;
45
+ --listen)
46
+ if [ "$remaining" -eq 0 ] || [ "$1" != stdio:// ]; then
47
+ echo 'aiterm-relay: stdio以外の接続指定は変更できません' >&2
48
+ exit 2
49
+ fi
50
+ shift
51
+ remaining=$((remaining - 1)) ;;
52
+ --listen=stdio://) ;;
53
+ --listen=*) echo 'aiterm-relay: stdio以外の接続指定は変更できません' >&2; exit 2 ;;
54
+ *) set -- "$@" "$arg" ;;
55
+ esac
56
+ done
57
+ "$node" "$relay" --prepare "$root" || exit $?
58
+ socket="$root/$$.sock"
59
+ exec 3<&0
60
+ "$node" "$relay" "$socket" "$$" <&3 3<&- &
61
+ exec "$binary" "$@" --listen "unix://$socket" </dev/null >/dev/null 3<&-
62
+ `;
63
+ }
@@ -0,0 +1,47 @@
1
+ // ユーザーのログイン時にもCodexの起動設定を適用する、Aiterm専用LaunchAgent。
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { spawnSync } from "node:child_process";
6
+ import { SetupError } from "./setup-platform.js";
7
+ const label = "dev.kitepon.aiterm-codex-relay";
8
+ const xml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
9
+ export function relayLoginPlist(launcher) {
10
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array><string>/bin/launchctl</string><string>setenv</string><string>CODEX_CLI_PATH</string><string>${xml(launcher)}</string></array><key>RunAtLoad</key><true/></dict></plist>\n`;
11
+ }
12
+ function context(options) {
13
+ const directory = options.directory ?? path.join(process.env.HOME ?? homedir(), "Library", "LaunchAgents");
14
+ const uid = options.uid ?? process.getuid?.();
15
+ if (uid === undefined)
16
+ throw new SetupError("codex_steer_platform_unsupported", "LaunchAgentはmacOSだけで使用します");
17
+ const run = options.run ?? ((args) => {
18
+ const result = spawnSync("/bin/launchctl", args, { encoding: "utf8", timeout: 10_000 });
19
+ if (result.error)
20
+ throw new SetupError("codex_steer_login_failed", "ログイン時の起動設定を実行できません");
21
+ return result.status ?? -1;
22
+ });
23
+ return { directory, file: path.join(directory, `${label}.plist`), domain: `gui/${uid}`, service: `gui/${uid}/${label}`, run };
24
+ }
25
+ export function installRelayLogin(launcher, options = {}) {
26
+ const { directory, file, domain, service, run } = context(options);
27
+ const contents = relayLoginPlist(launcher);
28
+ if (fs.existsSync(file) && (fs.lstatSync(file).isSymbolicLink() || fs.readFileSync(file, "utf8") !== contents)) {
29
+ throw new SetupError("codex_steer_login_conflict", "同名のLaunchAgentが変更されています。所有外の設定は上書きしません");
30
+ }
31
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
32
+ if (!fs.existsSync(file))
33
+ fs.writeFileSync(file, contents, { mode: 0o600, flag: "wx" });
34
+ if (run(["print", service]) !== 0 && run(["bootstrap", domain, file]) !== 0)
35
+ throw new SetupError("codex_steer_login_failed", "ログイン時のSteer設定を登録できません");
36
+ }
37
+ export function removeRelayLogin(launcher, options = {}) {
38
+ const { file, service, run } = context(options);
39
+ if (!fs.existsSync(file))
40
+ return;
41
+ if (fs.lstatSync(file).isSymbolicLink() || fs.readFileSync(file, "utf8") !== relayLoginPlist(launcher)) {
42
+ throw new SetupError("codex_steer_login_conflict", "変更されたLaunchAgentは削除しません");
43
+ }
44
+ if (run(["print", service]) === 0 && run(["bootout", service]) !== 0)
45
+ throw new SetupError("codex_steer_login_failed", "ログイン時のSteer設定を解除できません");
46
+ fs.unlinkSync(file);
47
+ }
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ // DesktopのJSONLと公式App ServerのUnix WebSocketの中継。RPCの内容は変更しない。
3
+ import { createConnection } from "node:net";
4
+ import { createInterface } from "node:readline";
5
+ import { setTimeout as delay } from "node:timers/promises";
6
+ import { once } from "node:events";
7
+ import { fileURLToPath } from "node:url";
8
+ import WebSocket from "ws";
9
+ import { prepareRelayDirectory } from "./codex-relay-config.js";
10
+ export async function runCodexStdioRelay(socketPath, serverPid) {
11
+ let socket;
12
+ let ending = false;
13
+ let stopping;
14
+ function stopServer() {
15
+ return stopping ??= (async () => {
16
+ // 公式Unix受付は1回目でturn完了待ち、2回目で終了する。
17
+ for (let i = 0; i < 2; i++) {
18
+ if (process.ppid !== serverPid)
19
+ return;
20
+ try {
21
+ process.kill(serverPid, "SIGTERM");
22
+ }
23
+ catch (error) {
24
+ if (error.code === "ESRCH")
25
+ return;
26
+ throw error;
27
+ }
28
+ if (i === 0)
29
+ await delay(100);
30
+ }
31
+ })();
32
+ }
33
+ async function connectDuringStartup() {
34
+ const deadline = Date.now() + 15_000;
35
+ while (true) {
36
+ if (process.ppid !== serverPid)
37
+ throw new Error("公式App Serverが起動中に終了しました");
38
+ const candidate = new WebSocket("ws://localhost/rpc", {
39
+ createConnection: () => createConnection(socketPath), handshakeTimeout: 10_000, perMessageDeflate: false,
40
+ });
41
+ try {
42
+ await once(candidate, "open");
43
+ return candidate;
44
+ }
45
+ catch (error) {
46
+ candidate.on("error", () => { });
47
+ candidate.terminate();
48
+ // exec直後のsocket作成だけを待つ。送信済みRPCは再送しない。
49
+ if (!["ENOENT", "ECONNREFUSED"].includes(error.code ?? "") || Date.now() >= deadline)
50
+ throw error;
51
+ await delay(25);
52
+ }
53
+ }
54
+ }
55
+ try {
56
+ socket = await connectDuringStartup();
57
+ const connected = socket;
58
+ connected.on("message", (data, binary) => {
59
+ if (binary) {
60
+ process.stderr.write("aiterm-relay: 予期しないバイナリ応答\n");
61
+ process.exitCode = 1;
62
+ void stopServer();
63
+ connected.terminate();
64
+ return;
65
+ }
66
+ if (!process.stdout.write(`${data.toString()}\n`))
67
+ connected.pause();
68
+ });
69
+ process.stdout.on("drain", () => connected.resume());
70
+ connected.on("error", () => {
71
+ process.stderr.write("aiterm-relay: 通信に失敗しました\n");
72
+ process.exitCode = 1;
73
+ void stopServer();
74
+ });
75
+ connected.on("close", () => {
76
+ if (!ending) {
77
+ process.stderr.write("aiterm-relay: 公式App Serverとの接続が終了しました\n");
78
+ process.exitCode = 1;
79
+ }
80
+ process.stdin.destroy();
81
+ });
82
+ process.stdout.on("error", () => { ending = true; void stopServer(); connected.terminate(); process.stdin.destroy(); });
83
+ const lines = createInterface({ input: process.stdin, crlfDelay: Infinity });
84
+ for await (const line of lines) {
85
+ await new Promise((resolve, reject) => connected.send(line, error => error ? reject(error) : resolve()));
86
+ }
87
+ ending = true;
88
+ await stopServer();
89
+ connected.terminate();
90
+ }
91
+ catch (error) {
92
+ process.stderr.write(`aiterm-relay: 中継失敗: ${error instanceof Error ? error.message : String(error)}\n`);
93
+ process.exitCode = 1;
94
+ await stopServer();
95
+ socket?.terminate();
96
+ process.stdin.destroy();
97
+ }
98
+ }
99
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
100
+ const [socket, pid] = process.argv.slice(2);
101
+ if (socket === "--prepare") {
102
+ try {
103
+ if (process.platform === "win32")
104
+ throw new Error("WindowsのAF_UNIX中継は未対応です");
105
+ if (!pid || Buffer.byteLength(`${pid}/9999999999.sock`) >= 104)
106
+ throw new Error("socketのpathが長すぎます");
107
+ prepareRelayDirectory(pid);
108
+ }
109
+ catch (error) {
110
+ process.stderr.write(`aiterm-relay: ${error instanceof Error ? error.message : String(error)}\n`);
111
+ process.exitCode = 2;
112
+ }
113
+ }
114
+ else if (!socket || !/^\d+$/.test(pid ?? "") || Number(pid) <= 1 || Number(pid) !== process.ppid) {
115
+ process.stderr.write("aiterm-relay: 親processの指定が不正です\n");
116
+ process.exitCode = 2;
117
+ }
118
+ else
119
+ await runCodexStdioRelay(socket, Number(pid));
120
+ }
@@ -224,6 +224,8 @@ export function claudeStartupAction(screen, trustProject) {
224
224
  // submit座礁観測のcomposer領域マーカー(ready判定と同じ記号を行頭基準で探す)。
225
225
  export const CLAUDE_COMPOSER_MARKER_RE = /^\s*❯/;
226
226
  export function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId, launchRequestDigest, lineageContext, model, effort) {
227
+ // Claude Codeはリンクを解決したcwdでproject slugを作る。起動時に固定し、記録の監視時に再解決しない。
228
+ const transcriptCwd = cwd === null ? null : fs.realpathSync(cwd);
227
229
  const launchId = randomBytes(16).toString("hex");
228
230
  const eventFile = agentEventPath(name, launchId);
229
231
  const resultFile = agentClaudeResultPath(name, launchId);
@@ -236,7 +238,7 @@ export function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperat
236
238
  launch_id: launchId,
237
239
  event_file: eventFile,
238
240
  created_at: new Date().toISOString(),
239
- cwd,
241
+ cwd: transcriptCwd,
240
242
  vendor_session_id: randomUUID(),
241
243
  initial_prompt: initialPrompt,
242
244
  launch_operation_id: launchOperationId,
@@ -235,7 +235,7 @@ export class ParentDeliveryManager {
235
235
  }
236
236
  record.state = "ready";
237
237
  this.save(job);
238
- // 次の依頼へ進める時点は本文保存まで。親のキュー受付を待たせない。
238
+ // 次の依頼へ進める時点は本文保存まで。親の受付を待たせない。
239
239
  job.delivery = this.deliver(job).catch((error) => this.reportServiceError(error));
240
240
  })();
241
241
  return job.capture;
@@ -368,7 +368,7 @@ export class ParentDeliveryManager {
368
368
  this.jobs.set(record.delivery_id, job);
369
369
  if (record.state === "sending") {
370
370
  record.state = "unknown";
371
- record.error = "PARENT_DELIVERY_INTERRUPTED: キュー送信中にMCP processが終了しました。自動再送はしていません";
371
+ record.error = "PARENT_DELIVERY_INTERRUPTED: 回答送信中にMCP processが終了しました。自動再送はしていません";
372
372
  this.finish(job);
373
373
  }
374
374
  else if (record.state === "waiting" || record.state === "ready") {
@@ -7,7 +7,7 @@ import { defaultRuntimeErrorPaths, windowsPrivateDaclCommand, expectedHostProfil
7
7
  export { defaultRuntimeErrorPaths, windowsPrivateDaclCommand, windowsPrivateDaclVerifyCommand } from "./runtime-error-os.js";
8
8
  import { fileURLToPath } from "node:url";
9
9
  const pkg = createRequire(import.meta.url)("../package.json");
10
- const STORE_SCHEMA = "aiterm-mcp.runtime-errors.v1";
10
+ const STORE_SCHEMA = "aiterm-mcp.runtime-errors.v2";
11
11
  const STATE_SCHEMA = "1.0";
12
12
  const MAX_CONFIG_BYTES = 16 * 1024;
13
13
  const MAX_STORE_BYTES = 1024 * 1024;
@@ -241,9 +241,17 @@ export class RuntimeErrorStore {
241
241
  return EMPTY_STATE();
242
242
  throw error;
243
243
  }
244
- const state = validateState(JSON.parse(text), this.maxRecords);
244
+ const value = JSON.parse(text);
245
+ const legacy = isObject(value) && value.schema_version === "aiterm-mcp.runtime-errors.v1";
246
+ const state = validateState(legacy ? { ...value, schema_version: STORE_SCHEMA } : value, this.maxRecords);
245
247
  if (!state)
246
248
  throw new Error("runtime error store schema が不正です");
249
+ // 旧集約は初回版だけを保持した。複数回発生した記録の最終発生版は復元できない。
250
+ if (legacy)
251
+ for (const record of state.records) {
252
+ if (record.occurrence_count > 1)
253
+ record.product_version = "unknown";
254
+ }
247
255
  return state;
248
256
  }
249
257
  applyWindowsDacl(target, kind) {
@@ -542,6 +550,7 @@ export class RuntimeErrorStore {
542
550
  const seen = this.now().toISOString();
543
551
  const sequence = state.cursor + 1;
544
552
  if (record) {
553
+ record.product_version = this.productVersion;
545
554
  record.occurrence_count += 1;
546
555
  record.last_seen = seen;
547
556
  record.status = "open";
package/dist/setup-cli.js CHANGED
@@ -3,6 +3,9 @@ import { runSetup } from "./setup.js";
3
3
  import { removeClaudeParentHooks } from "./setup-integrations.js";
4
4
  import { homedir } from "node:os";
5
5
  import { join } from "node:path";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { configureCodexSteer } from "./setup-codex-relay.js";
8
+ import { readRelayConfig } from "./codex-relay-config.js";
6
9
  const args = process.argv.slice(2);
7
10
  if (args.length === 1 && args[0] === "--remove-claude-parent-hooks") {
8
11
  try {
@@ -15,14 +18,42 @@ if (args.length === 1 && args[0] === "--remove-claude-parent-hooks") {
15
18
  }
16
19
  }
17
20
  else if (args.length === 1 && ["--help", "-h"].includes(args[0])) {
18
- process.stdout.write("使い方: aiterm-setup [--json | --remove-claude-parent-hooks]\n製品の依存準備、検出したAIへの登録、MCPと端末の実動作確認を行います。結果はJSONで返します。旧版へ戻す前の専用hook解除も、この入口で行います。\n");
19
- }
20
- else if (args.length > 1 || (args.length === 1 && args[0] !== "--json")) {
21
- process.stderr.write("使い方: aiterm-setup [--json]\n");
22
- process.exitCode = 2;
21
+ process.stdout.write("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]\n依存準備、AIへの登録、MCPと端末の実動作確認を行います。対話実行ではAiterm単品かCodex Desktop Steer付きかを選べます。SteerはmacOS対応で、初回はCodexの再起動が必要です。disableは元の起動設定へ戻し、statusは実際の接続を確認します。--jsonは対話せず、Steerの選択を維持します。\n旧版へ戻す前のClaude専用hook解除: --remove-claude-parent-hooks\n");
23
22
  }
24
23
  else {
25
- const result = await runSetup();
26
- process.stdout.write(`${JSON.stringify(result)}\n`);
27
- process.exitCode = result.status === "ready" ? 0 : 2;
24
+ try {
25
+ let action;
26
+ let json = false;
27
+ for (let i = 0; i < args.length; i++) {
28
+ if (args[i] === "--json" && !json)
29
+ json = true;
30
+ else if (args[i] === "--codex-steer" && !action && ["enable", "disable", "status"].includes(args[i + 1]))
31
+ action = args[++i];
32
+ else
33
+ throw new Error("使い方: aiterm-setup [--json] [--codex-steer enable|disable|status]");
34
+ }
35
+ const onlySteer = action === "disable" || action === "status";
36
+ if (!action && !json && process.stdin.isTTY && process.stderr.isTTY && process.platform === "darwin") {
37
+ const enabled = readRelayConfig()?.enabled ?? false;
38
+ const prompt = createInterface({ input: process.stdin, output: process.stderr });
39
+ try {
40
+ const answer = (await prompt.question(`導入方法: 1=Aiterm単品(公式キュー配送)、2=Codex DesktopへSteerと終了後再開を有効化 [${enabled ? "2" : "1"}]: `)).trim();
41
+ if (answer && !["1", "2"].includes(answer))
42
+ throw new Error("導入方法は1または2で指定してください");
43
+ action = (answer || (enabled ? "2" : "1")) === "2" ? "enable" : "disable";
44
+ }
45
+ finally {
46
+ prompt.close();
47
+ }
48
+ }
49
+ const result = onlySteer
50
+ ? { schema: "aiterm.codex-steer-result.v1", ...await configureCodexSteer(action) }
51
+ : await runSetup({ codex_steer: action });
52
+ process.stdout.write(`${JSON.stringify(result)}\n`);
53
+ process.exitCode = ["ready", "disabled"].includes(result.status) ? 0 : result.status === "restart_required" ? 3 : 2;
54
+ }
55
+ catch (error) {
56
+ process.stderr.write(`aiterm-setup: ${error instanceof Error ? error.message : String(error)}\n`);
57
+ process.exitCode = 2;
58
+ }
28
59
  }
@@ -0,0 +1,184 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { spawn, spawnSync } from "node:child_process";
5
+ import { createInterface } from "node:readline";
6
+ import { fileURLToPath } from "node:url";
7
+ import { randomUUID } from "node:crypto";
8
+ import { SetupError } from "./setup-platform.js";
9
+ import { CodexDeliveryError } from "./codex-delivery-error.js";
10
+ import { readRelayConfig, relayConfigDirectory, prepareRelayDirectory } from "./codex-relay-config.js";
11
+ import { codexRelayLauncher } from "./codex-relay-launcher.js";
12
+ import { withCodexRelay } from "./codex-relay-client.js";
13
+ import { readRuntimeProcesses } from "./process-runtime.js";
14
+ import { installRelayLogin, removeRelayLogin } from "./codex-relay-login.js";
15
+ function command(executable, args) {
16
+ const result = spawnSync(executable, args, { encoding: "utf8", timeout: 15_000 });
17
+ if (result.error || result.status !== 0)
18
+ throw new SetupError("codex_steer_setup_failed", `${path.basename(executable)}を実行できません`);
19
+ return result.stdout.trim();
20
+ }
21
+ function getGui(key) {
22
+ const result = spawnSync("/bin/launchctl", ["getenv", key], { encoding: "utf8", timeout: 5_000 });
23
+ if (result.status === 1 && !result.stderr.trim())
24
+ return null;
25
+ if (result.error || result.status !== 0)
26
+ throw new SetupError("codex_steer_environment_unavailable", "GUIの起動設定を確認できません");
27
+ return result.stdout.trim() || null;
28
+ }
29
+ function findDesktopBinary() {
30
+ const search = spawnSync("/usr/bin/mdfind", ["kMDItemCFBundleIdentifier == 'com.openai.codex'"], { encoding: "utf8", timeout: 10_000 });
31
+ const candidates = [...new Set(["/Applications/Codex.app", "/Applications/ChatGPT.app", ...(search.status === 0 ? search.stdout.trim().split("\n") : [])])];
32
+ const found = candidates.filter(app => {
33
+ if (!app || !fs.existsSync(path.join(app, "Contents", "Resources", "codex")))
34
+ return false;
35
+ const result = spawnSync("/usr/libexec/PlistBuddy", ["-c", "Print:CFBundleIdentifier", path.join(app, "Contents", "Info.plist")], { encoding: "utf8", timeout: 5_000 });
36
+ return result.status === 0 && result.stdout.trim() === "com.openai.codex";
37
+ });
38
+ if (found.length !== 1)
39
+ throw new SetupError("codex_desktop_not_identified", "Codex Desktopのインストール先を一つに特定できません");
40
+ const binary = path.join(found[0], "Contents", "Resources", "codex");
41
+ command("/usr/bin/codesign", ["--verify", "--strict", binary]);
42
+ const version = command(binary, ["--version"]);
43
+ const match = /codex-cli (\d+)\.(\d+)\.(\d+)/.exec(version);
44
+ if (!match || (Number(match[1]) === 0 && Number(match[2]) < 154)) {
45
+ throw new SetupError("codex_version_unsupported", "SteerにはCodex CLI 0.154以上を同梱したCodex Desktopが必要です");
46
+ }
47
+ return binary;
48
+ }
49
+ /** 模擬HOMEで起動・initialize・終了を確認する。利用者の認証やtaskは使わない。 */
50
+ export async function verifyRelayLauncher(launcher) {
51
+ const temporary = fs.mkdtempSync(path.join(tmpdir(), "aiterm-relay-setup-"));
52
+ fs.mkdirSync(path.join(temporary, ".codex"));
53
+ const child = spawn(launcher, ["-c", 'cli_auth_credentials_store="file"', "-c", 'mcp_oauth_credentials_store="file"', "app-server"], {
54
+ env: { ...process.env, HOME: temporary, CODEX_HOME: path.join(temporary, ".codex") }, stdio: ["pipe", "pipe", "ignore"],
55
+ });
56
+ const reader = createInterface({ input: child.stdout });
57
+ const exited = new Promise(resolve => child.once("close", resolve));
58
+ try {
59
+ await new Promise((resolve, reject) => {
60
+ const timer = setTimeout(() => reject(new SetupError("codex_relay_probe_failed", "中継経由の公式受付を確認できません")), 20_000);
61
+ const fail = () => { clearTimeout(timer); reject(new SetupError("codex_relay_probe_failed", "中継経由の公式起動に失敗しました")); };
62
+ child.once("error", fail);
63
+ child.once("close", fail);
64
+ child.stdin.once("error", fail);
65
+ reader.on("line", line => {
66
+ try {
67
+ const value = JSON.parse(line);
68
+ if (value.id !== 1 || value.method)
69
+ return;
70
+ clearTimeout(timer);
71
+ if (!value.result || value.error)
72
+ fail();
73
+ else
74
+ resolve();
75
+ }
76
+ catch {
77
+ fail();
78
+ }
79
+ });
80
+ child.stdin.write(JSON.stringify({ id: 1, method: "initialize", params: { clientInfo: { name: "aiterm_setup", version: "1" } } }) + "\n");
81
+ });
82
+ }
83
+ finally {
84
+ child.stdin.end();
85
+ const timer = setTimeout(() => child.kill("SIGKILL"), 2_000);
86
+ const code = await exited;
87
+ clearTimeout(timer);
88
+ reader.close();
89
+ fs.rmSync(temporary, { recursive: true, force: true });
90
+ if (code !== 0)
91
+ throw new SetupError("codex_relay_probe_failed", "中継の終了を確認できません");
92
+ }
93
+ }
94
+ export async function liveRelay(config) {
95
+ if (!fs.existsSync(config.socket_root))
96
+ return false;
97
+ const processes = readRuntimeProcesses();
98
+ const rows = new Map(processes.map(row => [row.pid, row]));
99
+ const desktopPrefix = path.join(path.dirname(path.dirname(config.binary)), "MacOS") + path.sep;
100
+ for (const name of fs.readdirSync(config.socket_root).filter(name => /^\d+\.sock$/.test(name))) {
101
+ const server = rows.get(Number(name.slice(0, -5)));
102
+ const desktop = server && rows.get(server.parent_pid);
103
+ if (!server || !(server.command === config.binary || server.command.startsWith(config.binary + " "))
104
+ || !desktop?.command.startsWith(desktopPrefix))
105
+ continue;
106
+ try {
107
+ await withCodexRelay(path.join(config.socket_root, name), async (request) => { await request("thread/loaded/list", { limit: 1 }); }, 1_000);
108
+ return true;
109
+ }
110
+ catch (error) {
111
+ // 終了済みsocketだけをreadyの証拠から外す。権限・protocolエラーはそのまま返す。
112
+ if (!(error instanceof CodexDeliveryError) || error.delivery_code !== "CODEX_RELAY_UNAVAILABLE")
113
+ throw error;
114
+ }
115
+ }
116
+ return false;
117
+ }
118
+ function writeConfig(directory, config) {
119
+ const temporary = path.join(directory, `${randomUUID()}.tmp`);
120
+ fs.writeFileSync(temporary, JSON.stringify(config, null, 2) + "\n", { mode: 0o600, flag: "wx" });
121
+ fs.renameSync(temporary, path.join(directory, "config.json"));
122
+ }
123
+ export async function configureCodexSteer(action = "status", overrides = {}) {
124
+ const runtime = {
125
+ platform: process.platform, directory: relayConfigDirectory(), socket_root: `/tmp/aiterm-codex-${process.getuid?.() ?? 0}`,
126
+ node: process.execPath, relay: fileURLToPath(new URL("./codex-stdio-relay.js", import.meta.url)),
127
+ findBinary: findDesktopBinary, getGui,
128
+ setGui: (key, value) => { command("/bin/launchctl", value === null ? ["unsetenv", key] : ["setenv", key, value]); },
129
+ persist: installRelayLogin, unpersist: removeRelayLogin,
130
+ verify: verifyRelayLauncher, live: liveRelay, ...overrides,
131
+ };
132
+ if (runtime.platform !== "darwin") {
133
+ return action === "enable" ? { status: "unsupported", reason_code: "codex_steer_platform_unsupported" } : { status: "disabled" };
134
+ }
135
+ const previous = readRelayConfig(runtime.directory);
136
+ if (action === "status") {
137
+ if (!previous?.enabled)
138
+ return { status: "disabled" };
139
+ if (runtime.getGui("CODEX_CLI_PATH") !== previous.launcher)
140
+ return { status: "failed", reason_code: "codex_steer_configuration_changed" };
141
+ return await runtime.live(previous) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
142
+ }
143
+ if (action === "disable") {
144
+ if (!previous?.enabled)
145
+ return { status: "disabled" };
146
+ if (![previous.launcher, previous.previous_cli_path].includes(runtime.getGui("CODEX_CLI_PATH")))
147
+ throw new SetupError("codex_steer_configuration_changed", "起動設定が他から変更されています。所有外の値は上書きしません");
148
+ runtime.unpersist(previous.launcher);
149
+ runtime.setGui("CODEX_CLI_PATH", previous.previous_cli_path);
150
+ if (runtime.getGui("CODEX_CLI_PATH") !== previous.previous_cli_path)
151
+ throw new SetupError("codex_steer_readback_failed", "元の起動設定を確認できません");
152
+ writeConfig(runtime.directory, { ...previous, enabled: false });
153
+ return { status: "restart_required", reason_code: "codex_restart_required" };
154
+ }
155
+ for (const key of ["CODEX_APP_SERVER_WS_URL", "CODEX_APP_SERVER_USE_LOCAL_DAEMON", "CODEX_APP_SERVER_FORCE_CLI"]) {
156
+ if (runtime.getGui(key))
157
+ throw new SetupError("codex_steer_configuration_conflict", `${key}が設定されています。既存の接続設定は上書きしません`);
158
+ }
159
+ const binary = runtime.findBinary();
160
+ const current = runtime.getGui("CODEX_CLI_PATH");
161
+ if (previous?.enabled && ![previous.launcher, previous.previous_cli_path].includes(current))
162
+ throw new SetupError("codex_steer_configuration_changed", "起動設定が他から変更されています");
163
+ prepareRelayDirectory(runtime.directory);
164
+ const launcher = path.join(runtime.directory, "codex");
165
+ const config = { schema: "aiterm.codex-relay.v1", enabled: true, binary, node: runtime.node,
166
+ launcher, socket_root: runtime.socket_root, previous_cli_path: previous?.enabled ? previous.previous_cli_path : current };
167
+ const candidate = path.join(runtime.directory, `codex-${randomUUID()}`);
168
+ fs.writeFileSync(candidate, codexRelayLauncher({ ...config, relay: runtime.relay }), { mode: 0o700, flag: "wx" });
169
+ try {
170
+ await runtime.verify(candidate);
171
+ fs.renameSync(candidate, launcher);
172
+ }
173
+ finally {
174
+ if (fs.existsSync(candidate))
175
+ fs.unlinkSync(candidate);
176
+ }
177
+ // 起動設定の変更前に復元値を保存する。読戻し不一致を成功扱いしない。
178
+ writeConfig(runtime.directory, config);
179
+ runtime.persist(launcher);
180
+ runtime.setGui("CODEX_CLI_PATH", launcher);
181
+ if (runtime.getGui("CODEX_CLI_PATH") !== launcher)
182
+ throw new SetupError("codex_steer_readback_failed", "Steerの起動設定を確認できません");
183
+ return await runtime.live(config) ? { status: "ready" } : { status: "restart_required", reason_code: "codex_restart_required" };
184
+ }
package/dist/setup.js CHANGED
@@ -8,6 +8,8 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
8
8
  import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { prepareBackend, runSetupCommand, SetupError } from "./setup-platform.js";
10
10
  import { configureIntegrations } from "./setup-integrations.js";
11
+ import { configureCodexSteer } from "./setup-codex-relay.js";
12
+ import { readRelayConfig } from "./codex-relay-config.js";
11
13
  export function globalRegistration(run = runSetupCommand) {
12
14
  const root = run(process.platform === "win32" ? "npm.cmd" : "npm", ["root", "-g"]).trim();
13
15
  if (!isAbsolute(root) || /[\r\n]/u.test(root))
@@ -75,6 +77,18 @@ export async function runSetup(options = {}) {
75
77
  }
76
78
  else
77
79
  result.status = "ready";
80
+ if (result.status === "ready") {
81
+ stage = "codex_steer";
82
+ result.codex_steer = await (options.steer ?? configureCodexSteer)(options.codex_steer ?? (readRelayConfig()?.enabled ? "enable" : "status"));
83
+ if (["failed", "unsupported", "restart_required"].includes(result.codex_steer.status)) {
84
+ result.status = result.codex_steer.status;
85
+ result.reason_code = result.codex_steer.reason_code;
86
+ }
87
+ if (result.codex_steer.status === "restart_required")
88
+ progress("起動設定を保存しました。Codexを完全終了して再起動し、aiterm-setup --codex-steer statusで確認してください");
89
+ if (result.codex_steer.status === "unsupported")
90
+ progress("このOSのCodex Desktop Steerは未対応です。Aiterm単品の導入・利用は可能です");
91
+ }
78
92
  }
79
93
  catch (error) {
80
94
  const code = error instanceof SetupError ? error.code : `${stage}_failed`;
package/docs/DESIGN.md CHANGED
@@ -48,7 +48,7 @@ project/user環境を置換せず、launch相関と完了回収に必要なsta
48
48
  Throughlineの補足記憶はpathを透過搬送するだけで、内容、project束縛、context予算はThroughlineが所有する。
49
49
 
50
50
  agent turnは常に非ブロックdispatchであり、receiptの`event_cursor`がturn境界になる。
51
- Codex親にはAitermのMCP processが完了を観測し、回答本文を公式受信キューへ自動配送する。
51
+ Codex親にはAitermのMCP processが完了を観測し、選択設定に応じて公式Steerまたはqueueへ本文を自動配送する。
52
52
  Claude Code親は公式の非同期hookで本文を受け取り、待機中も新しいturnへ進める。
53
53
  それ以外の親には`wait_process`がplatform nativeな別process起動情報を返す。
54
54
  waiterは純readerで、親のforeground turnを塞がない。
@@ -64,15 +64,30 @@ Cursorのsubmitはadapterがextended keyboard protocolのEnterへ変換し、呼
64
64
 
65
65
  `src/parent-delivery.ts`が依頼の送信前に宛先と完了境界を保存し、完了観測、加工前の回答保存、配送を所有する。
66
66
  宛先はCodexのMCP handshakeと各要求の`_meta.threadId`から取得する。modelが指定したIDや環境変数で代用しない。
67
- `src/codex-parent-receiver.ts`は同じ`CODEX_HOME`の公式app-serverへstdioで接続し、`thread/read`と
68
- `thread/queue/list`で宛先を確認した後、`thread/queue/add`へ本文をJSONで渡す。
69
- 親のload/resume、Desktop固有通信、子への送信指示、別daemonは使わない。
70
- setupは公式`queue`入口を確認し、各dispatchでは実際の親threadの受入可否を確認する。
71
- native sub-agentを親にした外部queue入力はCodex自身が拒否するため、子への送信前に明示errorにする。
72
-
73
- 通常結果は親の実行中turnを中断せず、親がidleになった後に処理される。
67
+ 単品導入の`src/codex-parent-receiver.ts`は同じ`CODEX_HOME`の公式app-serverへstdioで接続し、
68
+ `thread/read`と`thread/queue/list`で宛先を確認してから`thread/queue/add`へ本文をJSONで渡す。
69
+ Steerを選択したmacOS Desktopでは、MCP processの祖先にある公式App Serverの本人所有socketへ接続する。
70
+ `thread/loaded/list`と`thread/read`で同じ親を確認し、公式`turn/start`へ一度だけ送る。
71
+ 公式の`start_or_steer_turn`が実行中なら同じturnへ追加入力し、終了後なら同じtaskの次turnを開始する。
72
+ 状態を読んでから送信方法を選ぶraceや再送は作らない。model・権限のoverrideも渡さない。
73
+ 親のload/resumeは行わず、接続不能・未load・native sub-agentは明示errorにする。
74
+ Steer設定がある場合にqueueへ自動退避することはない。
75
+
76
+ `aiterm-setup`の`--codex-steer enable|disable|status`が選択導入を所有する。
77
+ 設定とPOSIX launcherは`~/.config/aiterm-mcp/codex-relay/`、一時socketは`/tmp/aiterm-codex-<uid>/`へ置く。
78
+ launcherはNodeのstdio中継を起動してから公式CLIへ同じPIDでexecし、Desktopとの親子関係・署名・通常環境を保つ。
79
+ Python・独自App Server・認証情報のコピーは使わない。中継はRPCと承認応答を透過搬送し、追加clientは承認へ応答しない。
80
+ stdio EOFは公式の2段階終了へ伝える。受付開始待ちはexec直後だけに限定し、送信済みRPCは再送しない。
81
+ macOSのユーザーLaunchAgentはログイン時に同じ起動設定を適用する。解除は専用LaunchAgentを停止・削除し、
82
+ 保存した元の`CODEX_CLI_PATH`へ戻す。他から変更された設定は上書きしない。
83
+ `status=ready`は公式binary・Desktopの直接子・同じsocketのRPC応答を確認した場合だけ返す。
84
+ 初回の設定保存後は`restart_required`であり、Desktopの完全終了・再起動を要する。
85
+ Linux/WindowsのSteer選択は`unsupported`とする。通常の単品導入・queue配送は両OSで維持する。
86
+
87
+ 単品導入のqueue配送は親の実行中turnを中断せず、親がidleになった後に処理される。
74
88
  `parent_delivery`が配送IDと状態を返し、自動配送時の`wait_process`/`wait_command`はnullになる。
75
- `pty_observe`の`parent_deliveries`で状態を確認できる。`submitted`はキュー受付済みを示し、modelの読了を意味しない。
89
+ `pty_observe`の`parent_deliveries`で状態を確認できる。`submitted`は公式受信口の受付済みを示し、modelの読了を意味しない。
90
+ Steer配送の`queued_submission_id`はnullである。既存の配送record schemaは維持する。
76
91
  次の依頼へ進める前に前回の回答を保存し、harness所有記録を後の回答と取り違えない。
77
92
 
78
93
  Codexの配送記録と本文はAiterm stateの`parent-deliveries`へ保存する。ownerのPIDと開始識別子で生存を判定し、
@@ -111,6 +126,9 @@ Claude Desktopのチャット、Web、`agent_id`付きの会話(`--agent`で
111
126
  この受信契約の対象に含めない。
112
127
  対応対象は公式command hookとMCP metadataを提供するClaude Codeの対話sessionである。
113
128
 
129
+ Claudeの起動metadataには指定cwdの実体パスを保存する。Claude Codeが実体パスから作るproject slugと
130
+ APIエラー監視の参照先を一致させ、監視中のリンク変更で保存場所を取り違えない。
131
+
114
132
  `trust_project:true`は対象projectの既知のworkspace、hooks、MCP初期同意を起動準備として進める意図である。
115
133
  promptなしでも入力受付とharness生存を確認して`startup.ready`を返す。指定なしのpromptなし起動は
116
134
  従来どおり`startup.not_checked`で返す。初手receiptは未要求・未送信・送信済み未確認・開始確認を分ける。
package/docs/RELEASE.md CHANGED
@@ -35,6 +35,13 @@ Aitermのreleaseはこのrepositoryが所有する。`.github/workflows/product-
35
35
 
36
36
  ## 公開後smoke
37
37
 
38
+ Codex Steerを変更した場合は、公式バイナリを指定した`test/codex-relay-official.test.mjs`で
39
+ 実行中Steer・終了後再開・承認応答・stdio終了を先に確認する。公開packageの
40
+ `aiterm-setup --json --codex-steer enable`で選択導入し、`restart_required`なら人がDesktopを完全再起動する。
41
+ 再起動後の`aiterm-setup --codex-steer status`が`ready`であることと、通常の親からの子の回答配送を確認する。
42
+ socketがあるだけで成功とせず、公式binaryとDesktopの直接の子であることまで確認する。
43
+ Linux/Windowsの単品導入と、未対応のSteer選択が理由付きで停止することもCIで確認する。
44
+
38
45
  setupを変更した場合は、公開packageのglobal install後に`aiterm-setup --json`を実行し、
39
46
  端末実行と検出した各AIの登録結果を確認する。初回と再実行は一時設定領域でも試験し、所有外の設定保持を確かめる。
40
47
  WindowsのGrokパス変更ではスラッシュ区切りcwdで起動し、同じturnの完了通知と回答回収を確認する。
@@ -62,6 +69,10 @@ aiterm-setup --json
62
69
 
63
70
  巻き戻しは既知の正常版を指定する。
64
71
 
72
+ Steerを持たない旧版へ戻す時は、install前に`aiterm-setup --codex-steer disable`を実行する。
73
+ 専用LaunchAgentの解除と元のGUI起動設定の復元を確認し、MCP clientを再起動する。
74
+ 回答record schemaは従来と共通であり、Steer送信済みrecordの`queued_submission_id`はnullとなる。
75
+
65
76
  Claude親配送hookのない旧版へ戻す場合は、旧版のinstall前に`aiterm-setup --remove-claude-parent-hooks`を
66
77
  実行する。Aiterm専用の3 hookだけを解除し、他製品のhookと設定を保持する。
67
78
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "mcpName": "io.github.kitepon/aiterm-mcp",
5
5
  "description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
6
6
  "keywords": [
@@ -69,15 +69,17 @@
69
69
  },
70
70
  "dependencies": {
71
71
  "@modelcontextprotocol/sdk": "^1.29.0",
72
+ "ws": "8.21.3",
72
73
  "zod": "^4.4.3"
73
74
  },
74
75
  "devDependencies": {
75
76
  "@types/node": "^25.9.1",
77
+ "@types/ws": "^8.18.1",
76
78
  "parse-srcset": "1.0.2",
77
79
  "parse5": "^7.3.0",
78
80
  "remark-gfm": "^4.0.1",
79
81
  "remark-parse": "^11.0.0",
80
- "unified": "^11.0.5",
81
- "typescript": "^6.0.3"
82
+ "typescript": "^6.0.3",
83
+ "unified": "^11.0.5"
82
84
  }
83
85
  }