aiterm-mcp 0.55.3 → 0.56.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,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.56.0] - 2026-10-07
11
+
12
+ ### 追加
13
+
14
+ - ほかの製品が、Codexの親への回答をAitermの親配送へ頼む命令`aiterm-parent-delivery`を足す(ADR 0098)。製品はCodex用のhookを登録せず、
15
+ Aitermが登録したhookと公式キューで届く。決裁箱(Approval Box)が専用hookを捨てるための入口。
16
+ - `codex verify`・`codex submit`・`codex state`・`provider`。結果は1行のJSON。失敗は`{ok:false, code, message, outcome_unknown}`とexit 1。
17
+ - 設定・受付・実際に届いた事を分けて返す。`verify`の`steer`は設定(`enabled`=hookが登録・承認済みで親がhookの導入後に起きている、
18
+ `disabled`=公式キューだけ)。`submit`は公式キューの受付。`state`は`hook:"emitted"`と`turn_id`(hookがその番へ入れた)、`queued`(今も
19
+ 公式キューにあるか)。
20
+ - 同じ配送idの2回目は`PARENT_DELIVERY_DUPLICATE`で断る。対象はCodexの親だけ。
21
+ - 実物の確かめ(公式のCodex、一時HOME): 別のprocessから頼んで、動いている番へは同じ番へ、止まっている会話は公式キューが起こして届く。
22
+ hookを入れていない環境では`steer:"disabled"`と返り、番の後に届く。
23
+ - `aiterm-setup`が、命令の場所を`~/.config/aiterm-mcp/delivery-provider.json`へ残す。残せなければ成功にしない(`delivery_provider_failed`)。
24
+ - `dist/setup-integrations.js`に`ensureCodexParentSteer(home, options)`を足す。`aiterm-setup`を通らない導入(コンテナの起動時など)が、
25
+ Codexの親配送のhookを登録する入口。homeを引数で受け、その下だけを書く。動いているCodexは要らない。登録済みで承認済みなら何も書かない。
26
+ 失敗は投げずに`{status:"failed", reason_code}`で返す。`writeDeliveryProvider(home, registration)`も同じ所から出す。
27
+ - 今までの動きは変えていない。Aiterm自身の親配送、hookの登録の形、`aiterm-setup --codex-steer`はそのまま。
28
+
29
+ ## [0.55.4] - 2026-10-06
30
+
31
+ ### 変更
32
+
33
+ - 止まっているCodex・Grok・Cursorの席へ`pty_send`で新しいturnを送る時、起動直後のための5秒の確かめを待たない
34
+ (ADR 0097)。受け付けを返すまで毎回5.4〜5.7秒かかっていた。画面を500msおきに11回続けて読む入力受付の確かめ
35
+ (ADR 0014)を、2通目以降の文にも毎回通していた。連携元の本番で、Codexの席への新しいturn17通が
36
+ 5,686〜5,858msに揃っているのを見た。
37
+ - 席のharnessが自分の記録(Codexのrollout、Grokの`events.jsonl`、Cursorのtranscript)で「この起動でturnを1つ終えていて、
38
+ 次のturnを始めていない」と示している時は、100msおきに2回続けて画面が入力待ちなら送る。
39
+ - 実物の席での受け付けまでの時間: Codex 5.6秒→0.66秒、Grok 5.6秒→0.65秒、Cursor 5.4秒→0.41秒(Linux)。
40
+ Macは5.8秒→0.75秒、Windowsは6.5秒→1.4秒(Codex)。
41
+ - 起動直後の最初の文と、記録の上でturnが動いている間に届いた文は、今までどおり11回の確かめを通る。
42
+ - 起動時prompt、`agent_configure`の後、利用上限のmodalを閉じた直後の確かめは変わらない。Claudeの席は元から
43
+ この確かめを通らない。
44
+ - 道具の返りの形と断りの文面は変わらない。
45
+
10
46
  ## [0.55.3] - 2026-10-06
11
47
 
12
48
  ### 修正
@@ -2297,7 +2333,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2297
2333
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2298
2334
  provenance.
2299
2335
 
2300
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.3...HEAD
2336
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.56.0...HEAD
2337
+ [0.56.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.4...v0.56.0
2338
+ [0.55.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.3...v0.55.4
2301
2339
  [0.55.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.2...v0.55.3
2302
2340
  [0.55.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.1...v0.55.2
2303
2341
  [0.55.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.0...v0.55.1
package/README.ja.md CHANGED
@@ -209,7 +209,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
209
209
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
210
210
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
211
211
 
212
- **状態:** 開発継続中 · 現行公開版 **v0.55.3** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.56.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
@@ -528,6 +528,15 @@ agent sessionを閉じると失うものは、次の2項目で分かる。`activ
528
528
  子の結果の数。呼び出した側が誰でも付く。どちらも、分からない時はnull(通常PTY、harnessのprocessを特定できない時。
529
529
  processの数は0.48.0以前が起動したagentも)。別端末が旧版の時は項目ごと無い。nullと項目なしは「分からない」で、0ではない。
530
530
 
531
+ ほかの製品が、Codexの親へ回答を届ける時は、製品のhookを登録せずに`aiterm-parent-delivery`でAitermへ頼める。Aitermが登録したhookと
532
+ 公式キューで届く。`codex verify --thread <uuid>`は親を確かめ、`steer`で設定を返す(`enabled`=hookが登録・承認済みで親がhookの導入後に
533
+ 起きている。`disabled`=公式キューだけで、ターンが終わってから届く)。`codex submit --thread <uuid> --delivery <uuid> --text-file <file|->`は
534
+ 本文を一度だけ渡し、公式キューの受付IDを返す。`codex state --thread <uuid> --delivery <uuid>`は届き方の事実を返す(`hook:"emitted"`と
535
+ `turn_id`=hookがそのターンへ本文を入れた。`queued`=今も公式キューにあるか)。結果は1行のJSONで、失敗は
536
+ `{ok:false, code, message, outcome_unknown}`とexit 1。同じ配送IDの2回目は`PARENT_DELIVERY_DUPLICATE`で断る。対象はCodexの親だけ。
537
+ 命令の場所は`aiterm-setup`が`~/.config/aiterm-mcp/delivery-provider.json`へ残す。Nodeの製品は`aiterm-steer-delivery`(0.3.0以上)の
538
+ `submitCodexParentAnswerViaAiterm`などから呼べる。
539
+
531
540
  MCPの本体を初めて使う時まで起こさない中継の後ろにAitermを登録すると、眠っている本体は、終了した持ち主の配送を引き取れない。
532
541
  `aiterm-delivery-wake --parent <codex|claude|cursor>`は、起こす必要があるかを返す。その種類の親の配送を、終了した持ち主が
533
542
  持ったままならexit 0、そうでなければexit 1、引数の誤りはexit 2。何も出力せず、子processを起こさない。同じ判定を回す席のうち
package/README.md CHANGED
@@ -223,7 +223,7 @@ collection is off by default and performs no network I/O. It ships via
223
223
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
224
224
  Release re-registers the Official MCP Registry entry.
225
225
 
226
- **Status:** actively maintained · current public release **v0.55.3** · 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).
226
+ **Status:** actively maintained · current public release **v0.56.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).
227
227
 
228
228
  ### Update and rollback
229
229
 
@@ -563,6 +563,16 @@ is the number of sub-agent results this session is still waiting for as a parent
563
563
  Aiterm cannot tell (ordinary terminals, agents launched by 0.48.0 or earlier for the process count, or an unresolved harness
564
564
  process), and may be absent when a remote host runs an older Aiterm. Treat null or absent as unknown, not as zero.
565
565
 
566
+ Another product that needs to deliver an answer to a Codex parent can ask Aiterm with `aiterm-parent-delivery` instead of registering its own hooks.
567
+ Aiterm's registered hooks and the official queue deliver it. `codex verify --thread <uuid>` checks the parent and reports configuration in `steer`
568
+ (`enabled`: the hooks are registered and trusted and the parent started after they were installed; `disabled`: official queue only, so the text arrives
569
+ after the turn ends). `codex submit --thread <uuid> --delivery <uuid> --text-file <file|->` hands the text over once and returns the queue's acceptance id.
570
+ `codex state --thread <uuid> --delivery <uuid>` reports how it arrived (`hook:"emitted"` with `turn_id`: the hook put the text into that turn; `queued`:
571
+ whether it is still in the official queue). Each result is one JSON line; failures are `{ok:false, code, message, outcome_unknown}` with exit 1, and a
572
+ reused delivery id is refused with `PARENT_DELIVERY_DUPLICATE`. Only Codex parents are supported. `aiterm-setup` records the command's location in
573
+ `~/.config/aiterm-mcp/delivery-provider.json`. Node products can call it through `aiterm-steer-delivery` (0.3.0 or later), for example
574
+ `submitCodexParentAnswerViaAiterm`.
575
+
566
576
  When Aiterm is registered behind a relay that starts the MCP server only on first use, a sleeping server cannot take over the
567
577
  deliveries of an owner that has exited. `aiterm-delivery-wake --parent <codex|claude|cursor>` answers whether one needs to start:
568
578
  exit 0 when an exited owner still holds a delivery for that parent kind, exit 1 otherwise, exit 2 for bad arguments. It prints
@@ -16,6 +16,23 @@ export function safeStatSize(p) {
16
16
  return 0;
17
17
  }
18
18
  }
19
+ /**
20
+ * 記録の末尾から、行を新しい順に返す。書いている途中の行(改行で終わっていない末尾)がある時はnullを返す。
21
+ * 読む範囲の先頭で切れた行は捨てる。
22
+ */
23
+ export function readTailLinesNewestFirst(file, maxBytes) {
24
+ const size = safeStatSize(file);
25
+ if (size === 0)
26
+ return [];
27
+ const from = Math.max(0, size - maxBytes);
28
+ const text = readFileRange(file, from, size).toString("utf8");
29
+ if (!text.endsWith("\n"))
30
+ return null;
31
+ const lines = text.split("\n");
32
+ if (from > 0)
33
+ lines.shift();
34
+ return lines.filter(line => line.trim() !== "").reverse();
35
+ }
19
36
  export function readFileRange(p, from, to) {
20
37
  const len = Math.max(0, to - from);
21
38
  if (len === 0)
@@ -103,6 +120,8 @@ export function pollGate(intervalMs, now = () => performance.now()) {
103
120
  }
104
121
  export const AGENT_EVENT_MAX_BYTES = 1024 * 1024;
105
122
  export const AGENT_EVENT_TAIL_BYTES = 64 * 1024;
123
+ // turnの境界(開始・終わり)を、harnessの記録の末尾から探す時に読む大きさ。終わった席では境界は末尾の近くにある。
124
+ export const AGENT_TURN_BOUNDARY_TAIL_BYTES = 1024 * 1024;
106
125
  export const CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES = 16 * 1024 * 1024;
107
126
  export const GROK_TRANSCRIPT_INCREMENT_MAX_BYTES = 16 * 1024 * 1024;
108
127
  // session 名はファイルパス(logpath 等)と pipe-pane の /bin/sh 文字列へ流れる。英数 _ - のみ・64字に
@@ -28,6 +28,20 @@ export async function verifyCodexParent(parent, runtime) {
28
28
  }
29
29
  await steer.verifyCodexParent(AITERM_PROFILE, parent, runtime);
30
30
  }
31
+ /**
32
+ * 親を確かめ、渡した文が同じ番へ入るかどうかも返す。ほかの製品がAitermへ配送を頼む入口(aiterm-parent-delivery)が使う。
33
+ * 確かめる中身は`verifyCodexParent`と同じ。hookが有効なのに未承認・親が導入前から動いている時は、今までどおり理由つきで断る。
34
+ */
35
+ export async function checkCodexParent(parent, runtime) {
36
+ const socket = relaySocket(runtime);
37
+ if (socket) {
38
+ await withCodexRelay(socket, request => verifyLoadedParent(request, parent.thread_id), runtime?.timeout_ms);
39
+ return { thread: { thread_id: parent.thread_id, cwd: null, source: null }, steer: "enabled" };
40
+ }
41
+ const thread = await steer.verifyCodexParent(AITERM_PROFILE, parent, runtime);
42
+ const config = runtime ? (runtime.hook_directory ? readCodexHookConfig(runtime.hook_directory) : null) : readCodexHookConfig();
43
+ return { thread, steer: config?.enabled ? "enabled" : "disabled" };
44
+ }
31
45
  export async function submitCodexParentAnswer(parent, deliveryId, text, runtime) {
32
46
  const socket = relaySocket(runtime);
33
47
  if (socket) {
package/dist/core.js CHANGED
@@ -21,8 +21,8 @@ import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./error
21
21
  import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, tmuxNewSession, registeredSessionEnvironment, attachCommand, normalizePaneCommand, atomicShellMultiline, appendMarkSentinel, markShellCommand, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
22
22
  import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_RATE_LIMIT_POLL_MS, pollGate, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
23
23
  import { catalogUnavailable } from "./model-catalog.js";
24
- import { grokAuthPlan, grokAuthStatus, grokAuthPane, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokModelChoices, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
25
- import { codexAuthPlan, codexAuthStatus, codexAuthPane, bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexTuiBusy, codexHelperProcess, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, } from "./harnesses/codex.js";
24
+ import { grokAuthPlan, grokAuthStatus, grokAuthPane, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokModelChoices, grokEventsTranscript, latestGrokCompletion, grokTurnSettled, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
25
+ import { codexAuthPlan, codexAuthStatus, codexAuthPane, bindCodexTranscriptSession, latestCodexCompletion, codexTurnSettled, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexTuiBusy, codexHelperProcess, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, } from "./harnesses/codex.js";
26
26
  import { claudeAuthPlan, claudeAuthStatus, claudeAuthPane, OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, claudeModelChoices, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeUsageLimit, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, claudeApiErrorAfter, } from "./harnesses/claude.js";
27
27
  import { cursorAuthPlan, cursorAuthStatus, cursorAuthPane, bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, cursorModelChoices, buildCursorAgentCmd, cursorAgentArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, cursorPromptHooksRunning, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
28
28
  import { readInterimWords, recordInterimBoundary } from "./interim-words.js";
@@ -59,6 +59,11 @@ const LAUNCH_ECHO_TAIL_CHARS = 24;
59
59
  const LAUNCH_ECHO_MIN_CHARS = 8;
60
60
  const AGENT_TUI_READY_POLL_MS = 500;
61
61
  const AGENT_TUI_READY_STABLE_SAMPLES = 11;
62
+ // 上の11回は、起動の途中に一瞬だけ出る入力欄を採らないための確かめ(ADR 0014)。席のharnessが自分の記録で
63
+ // 「この起動でturnを1つ終えていて、次のturnを始めていない」と示している時は起動の途中ではないので、短い間隔で
64
+ // 続けて確かめるだけで送る(ADR 0097)。記録で示せない時は、今までどおり11回。
65
+ const AGENT_TUI_SETTLED_STABLE_SAMPLES = 2;
66
+ const AGENT_TUI_SETTLED_POLL_MS = 100;
62
67
  const AGENT_TUI_READY_LINES = 45;
63
68
  const CLAUDE_APPROVAL_SCREEN_LINES = 80;
64
69
  // submit座礁観測(dispatch後にcomposerへ送信textが残存していないかの有界チェック)
@@ -3172,6 +3177,8 @@ async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
3172
3177
  const timeoutMs = opts.timeoutMs ?? AGENT_TUI_READY_TIMEOUT_MS;
3173
3178
  const pollMs = opts.pollMs ?? AGENT_TUI_READY_POLL_MS;
3174
3179
  const stableSamples = opts.stableSamples ?? agentTuiReadyStableSamplesTestOverride ?? AGENT_TUI_READY_STABLE_SAMPLES;
3180
+ const settledSamples = opts.settledSamples ?? AGENT_TUI_SETTLED_STABLE_SAMPLES;
3181
+ const settledPollMs = opts.settledPollMs ?? AGENT_TUI_SETTLED_POLL_MS;
3175
3182
  const now = opts.now ?? (() => performance.now());
3176
3183
  const start = now();
3177
3184
  let deadline = start + timeoutMs;
@@ -3179,6 +3186,7 @@ async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
3179
3186
  const firstDrawDeadline = timeoutMs >= AGENT_TUI_READY_TIMEOUT_MS ? start + AGENT_TUI_FIRST_DRAW_TIMEOUT_MS : deadline;
3180
3187
  let samples = 0;
3181
3188
  let readyStreak = 0;
3189
+ let settledStreak = 0;
3182
3190
  let lastScreen = "";
3183
3191
  for (;;) {
3184
3192
  lastScreen = sample();
@@ -3189,9 +3197,20 @@ async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
3189
3197
  readyStreak++;
3190
3198
  if (readyStreak >= stableSamples)
3191
3199
  return { ready: true, samples, lastScreen };
3200
+ if (opts.turnSettled?.()) {
3201
+ settledStreak++;
3202
+ if (settledStreak >= settledSamples)
3203
+ return { ready: true, samples, lastScreen };
3204
+ }
3205
+ else if (settledStreak > 0) {
3206
+ // 記録の上でturnが始まった。短い間隔で数えた分を、11回の確かめへ持ち越さない。
3207
+ settledStreak = 0;
3208
+ readyStreak = 1;
3209
+ }
3192
3210
  }
3193
3211
  else {
3194
3212
  readyStreak = 0;
3213
+ settledStreak = 0;
3195
3214
  if (isAgentTuiActionRequired(kind, lastScreen))
3196
3215
  return { ready: false, samples, lastScreen };
3197
3216
  }
@@ -3202,11 +3221,30 @@ async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
3202
3221
  return { ready: false, samples, lastScreen };
3203
3222
  deadline = firstDrawDeadline;
3204
3223
  }
3205
- await sleepFn(pollMs);
3224
+ await sleepFn(settledStreak > 0 ? settledPollMs : pollMs);
3206
3225
  }
3207
3226
  }
3208
- async function waitAgentTuiReady(name, meta, timeoutMs = AGENT_TUI_READY_TIMEOUT_MS) {
3209
- const result = await waitAgentTuiReadyImpl(meta.kind, () => captureScreen(name, AGENT_TUI_READY_LINES), sleep, { timeoutMs, launchLine: agentLaunchLines.get(name) });
3227
+ /**
3228
+ * 席のharnessが、この起動でturnを1つ終えていて、次のturnを始めていない事を、自分の記録で示しているか。
3229
+ * 記録はこの起動に結び付いた物だけを読む(Codexはlaunch markerかsession ID、Grokは起動時に決めたsession、
3230
+ * Cursorは最初の文に入れたmarker)。起動直後の席は記録が無いのでfalse。読めない時もfalse。
3231
+ * Claudeはturnの印で差し込みか新しいturnかを決めていて、この確かめを通らない。
3232
+ */
3233
+ function agentTurnSettled(meta) {
3234
+ try {
3235
+ if (meta.kind === "codex")
3236
+ return codexTurnSettled(meta);
3237
+ if (meta.kind === "grok")
3238
+ return meta.completion_route === "grok_transcript" && grokTurnSettled(meta);
3239
+ if (meta.kind === "cursor") {
3240
+ return meta.completion_route === "cursor_transcript" && latestCursorCompletion(meta, readTranscriptLines) !== null;
3241
+ }
3242
+ }
3243
+ catch { /* 読めない時は、11回の確かめで決める */ }
3244
+ return false;
3245
+ }
3246
+ async function waitAgentTuiReady(name, meta, timeoutMs = AGENT_TUI_READY_TIMEOUT_MS, o = {}) {
3247
+ const result = await waitAgentTuiReadyImpl(meta.kind, () => captureScreen(name, AGENT_TUI_READY_LINES), sleep, { timeoutMs, launchLine: agentLaunchLines.get(name), ...(o.settledTurn ? { turnSettled: () => agentTurnSettled(meta) } : {}) });
3210
3248
  if (result.ready)
3211
3249
  agentLaunchLines.delete(name);
3212
3250
  return result;
@@ -3403,7 +3441,10 @@ export async function __testWaitAgentTuiReady(kind, samples, opts = {}) {
3403
3441
  const sleeps = [];
3404
3442
  // virtualClockは待った分だけ進む時計。実時間を使わずに締切の扱いを確かめる。
3405
3443
  let clock = 0;
3406
- const { virtualClock, ...waitOpts } = opts;
3444
+ const { virtualClock, settled, ...rest } = opts;
3445
+ let asked = 0;
3446
+ const waitOpts = settled && settled.length > 0
3447
+ ? { ...rest, turnSettled: () => settled[Math.min(asked++, settled.length - 1)] } : rest;
3407
3448
  const result = await waitAgentTuiReadyImpl(kind, () => samples[Math.min(i++, samples.length - 1)], async (ms) => {
3408
3449
  sleeps.push(ms);
3409
3450
  clock += ms;
@@ -3714,9 +3755,10 @@ async function recoverCodexRateLimitModelSwitch(name, meta) {
3714
3755
  return true;
3715
3756
  }
3716
3757
  /** ready待機中に現れたCodex利用上限modalも、同じ一時keep復旧へ戻す。 */
3717
- async function waitAgentTuiReadyAfterCodexRateLimitRecovery(name, meta, timeoutMs) {
3758
+ async function waitAgentTuiReadyAfterCodexRateLimitRecovery(name, meta, timeoutMs, o = {}) {
3718
3759
  let codexRateLimitModelSwitch = await recoverCodexRateLimitModelSwitch(name, meta);
3719
- let ready = await waitAgentTuiReady(name, meta, timeoutMs);
3760
+ // 上限のmodalを閉じた直後は、画面が落ち着くのを11回の確かめで待つ。
3761
+ let ready = await waitAgentTuiReady(name, meta, timeoutMs, codexRateLimitModelSwitch ? {} : o);
3720
3762
  if (!ready.ready && !codexRateLimitModelSwitch) {
3721
3763
  codexRateLimitModelSwitch = await recoverCodexRateLimitModelSwitch(name, meta);
3722
3764
  if (codexRateLimitModelSwitch)
@@ -4189,7 +4231,8 @@ async function dispatchAgentTurnInLock(name, text, o) {
4189
4231
  sendKey(name, limitDialog.dismissKey);
4190
4232
  }
4191
4233
  if (meta.kind !== "claude" || claudeColdStart) {
4192
- const recovery = await waitAgentTuiReadyAfterCodexRateLimitRecovery(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
4234
+ // 上限のpanelを閉じた直後(Grok)は、画面が落ち着くのを11回の確かめで待つ。
4235
+ const recovery = await waitAgentTuiReadyAfterCodexRateLimitRecovery(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS, { settledTurn: !limitDialog });
4193
4236
  codexRateLimitModelSwitch = recovery.codexRateLimitModelSwitch;
4194
4237
  const { ready } = recovery;
4195
4238
  if (!ready.ready) {
@@ -0,0 +1,37 @@
1
+ // ほかの製品が、Aitermの親配送の入口(aiterm-parent-delivery)の場所を知るための記録(ADR 0098)。
2
+ // 製品のprocess(MCP serverや常駐process)は、PATHにnpmの置き場が無い環境で起きる事がある(macOSのアプリ配下など)。
3
+ // setupが、nodeと命令の絶対pathをHOMEの下へ残す。製品は読むだけで、書かない。
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { homedir } from "node:os";
7
+ import { createRequire } from "node:module";
8
+ import { isDeepStrictEqual } from "node:util";
9
+ import { writeJson0600 } from "aiterm-steer-delivery";
10
+ export const DELIVERY_PROVIDER_SCHEMA = "aiterm.delivery-provider.v1";
11
+ const pkg = createRequire(import.meta.url)("../package.json");
12
+ export function deliveryProviderFile(home = process.env.HOME ?? homedir()) {
13
+ return path.join(home, ".config", "aiterm-mcp", "delivery-provider.json");
14
+ }
15
+ /**
16
+ * 登録(nodeの絶対pathとdist/index.js)から、同じ導入のaiterm-parent-deliveryの場所を記録する。変わりが無ければ書かない。
17
+ * MCPの本体の起動時には呼ばない(試験用に別の場所から起こした本体が、導入済みの記録を書き換えるため)。
18
+ */
19
+ export function writeDeliveryProvider(home, registration) {
20
+ const entry = registration.args[0];
21
+ if (!path.isAbsolute(home) || !path.isAbsolute(registration.command) || typeof entry !== "string" || !path.isAbsolute(entry)) {
22
+ throw new Error("delivery providerの記録には、homeと登録の絶対pathが要ります");
23
+ }
24
+ const cli = path.join(path.dirname(entry), "parent-delivery-cli.js");
25
+ if (!fs.existsSync(cli))
26
+ throw new Error("aiterm-parent-deliveryの入口が、登録と同じ導入の中にありません");
27
+ const next = { schema: DELIVERY_PROVIDER_SCHEMA, version: pkg.version, node: registration.command, cli };
28
+ const file = deliveryProviderFile(home);
29
+ try {
30
+ if (isDeepStrictEqual(JSON.parse(fs.readFileSync(file, "utf8")), next))
31
+ return "unchanged";
32
+ }
33
+ catch { /* 無い・読めない時は書く */ }
34
+ fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
35
+ writeJson0600(file, next);
36
+ return "configured";
37
+ }
@@ -11,7 +11,7 @@ import { spawnAgentControlCommand } from "../agent-resolver.js";
11
11
  import * as steer from "aiterm-steer-delivery";
12
12
  import { AITERM_PROFILE } from "../steer-profile.js";
13
13
  import { catalogInvalid, catalogUnavailable, checkedCatalog } from "../model-catalog.js";
14
- import { shq, subagentInstruction, writeScopeLaunchNote, safeStatSize, readFileRange, sleep, agentMetadataPath, writeAgentMetadata, agentEventPath, createEmpty0600, agentLineageFields, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, AGENT_EVENT_TAIL_BYTES, CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES, agentHarness, } from "../agent-shared.js";
14
+ import { shq, subagentInstruction, writeScopeLaunchNote, safeStatSize, readFileRange, sleep, agentMetadataPath, writeAgentMetadata, agentEventPath, createEmpty0600, agentLineageFields, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, AGENT_EVENT_TAIL_BYTES, AGENT_TURN_BOUNDARY_TAIL_BYTES, readTailLinesNewestFirst, CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES, agentHarness, } from "../agent-shared.js";
15
15
  export function realCodexHome() {
16
16
  return process.env.CODEX_HOME || path.join(process.env.HOME ?? os.homedir(), ".codex");
17
17
  }
@@ -261,6 +261,47 @@ export function codexUsageLimit(record) {
261
261
  const error = codexTurnError(record);
262
262
  return error?.info === "usage_limit_exceeded" ? error.message : null;
263
263
  }
264
+ /**
265
+ * この起動のroot rolloutで、最後のturnが終わっていて、次のturnが始まっていないか。
266
+ * Codexはturnの開始でtask_started、終わりでtask_complete、Escで止めた時はturn_aborted(reasonはinterrupted)を書く
267
+ * (0.160.1の実記録)。末尾から見て最初の境界が、task_completeか、止めた時のturn_abortedの時だけtrue。
268
+ * rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はfalse(分からない時は「終わった」と数えない)。
269
+ */
270
+ export function codexTurnSettled(meta) {
271
+ let transcript;
272
+ try {
273
+ transcript = codexRootTranscript(meta);
274
+ }
275
+ catch {
276
+ return false;
277
+ }
278
+ if (!transcript)
279
+ return false;
280
+ const lines = readTailLinesNewestFirst(transcript, AGENT_TURN_BOUNDARY_TAIL_BYTES);
281
+ if (!lines)
282
+ return false;
283
+ for (const line of lines) {
284
+ if (!line.includes("task_started") && !line.includes("task_complete") && !line.includes("turn_aborted"))
285
+ continue;
286
+ let record;
287
+ try {
288
+ record = JSON.parse(line);
289
+ }
290
+ catch {
291
+ return false;
292
+ }
293
+ if (record?.type !== "event_msg")
294
+ continue;
295
+ if (record?.payload?.type === "task_started")
296
+ return false;
297
+ if (record?.payload?.type === "task_complete")
298
+ return typeof record.payload.turn_id === "string" && record.payload.turn_id !== "";
299
+ // 見たのはEscで止めた時の形だけ。他の理由(別のturnへの置き換え等)は、次のturnが続くかも知れないので数えない。
300
+ if (record?.payload?.type === "turn_aborted")
301
+ return record.payload.reason === "interrupted";
302
+ }
303
+ return false;
304
+ }
264
305
  export function latestCodexCompletion(meta, readTranscriptLines) {
265
306
  const transcript = codexRootTranscript(meta);
266
307
  if (!transcript)
@@ -10,7 +10,7 @@ import { randomBytes, randomUUID } from "node:crypto";
10
10
  import { AitermError } from "../errors.js";
11
11
  import { runAgentProtocolCommand, spawnAgentControlCommand } from "../agent-resolver.js";
12
12
  import { catalogInvalid, catalogUnavailable, checkedCatalog, findJsonLine, processSummary } from "../model-catalog.js";
13
- import { shq, subagentInstruction, writeScopeLaunchNote, safeStatSize, readFileRange, sleep, agentMetadataPath, writeAgentMetadata, agentEventPath, createEmpty0600, agentLineageFields, AGENT_DONE_POLL_MS, AGENT_RATE_LIMIT_POLL_MS, pollGate, AGENT_EVENT_MAX_BYTES, GROK_TRANSCRIPT_INCREMENT_MAX_BYTES, agentHarness, } from "../agent-shared.js";
13
+ import { shq, subagentInstruction, writeScopeLaunchNote, safeStatSize, readFileRange, sleep, agentMetadataPath, writeAgentMetadata, agentEventPath, createEmpty0600, agentLineageFields, AGENT_DONE_POLL_MS, AGENT_RATE_LIMIT_POLL_MS, pollGate, AGENT_EVENT_MAX_BYTES, AGENT_TURN_BOUNDARY_TAIL_BYTES, readTailLinesNewestFirst, GROK_TRANSCRIPT_INCREMENT_MAX_BYTES, agentHarness, } from "../agent-shared.js";
14
14
  const GROK_MODELS_MAX_BYTES = 1024 * 1024;
15
15
  const GROK_MODELS_TIMEOUT_MS = 15_000;
16
16
  // grok CLI はモデル未指定だと端末側 default に従うため、ツール契約として既定 slug を固定する。
@@ -191,6 +191,35 @@ export function latestGrokCompletion(meta, readTranscriptLines) {
191
191
  }
192
192
  return latest;
193
193
  }
194
+ /**
195
+ * この起動のevents.jsonlで、最後のturnが終わっていて、次のturnが始まっていないか。
196
+ * 末尾から見て最初の境界が、完了と数えるturn_ended(grokCompletionEvent)の時だけtrue。差し込みの継ぎ目
197
+ * (cancelledかつsend_now)とturn_startedはfalse。記録が無い、書いている途中、境界が読む範囲に無い時もfalse。
198
+ */
199
+ export function grokTurnSettled(meta) {
200
+ const transcript = grokEventsTranscript(meta);
201
+ if (!transcript || !fs.existsSync(transcript))
202
+ return false;
203
+ const lines = readTailLinesNewestFirst(transcript, AGENT_TURN_BOUNDARY_TAIL_BYTES);
204
+ if (!lines)
205
+ return false;
206
+ for (const line of lines) {
207
+ if (!line.includes("turn_started") && !line.includes("turn_ended"))
208
+ continue;
209
+ let record;
210
+ try {
211
+ record = JSON.parse(line);
212
+ }
213
+ catch {
214
+ return false;
215
+ }
216
+ if (record?.type === "turn_started")
217
+ return false;
218
+ if (record?.type === "turn_ended")
219
+ return grokCompletionEvent(meta, record) !== null;
220
+ }
221
+ return false;
222
+ }
194
223
  export function grokInitializationComplete(meta) {
195
224
  const transcript = grokEventsTranscript(meta);
196
225
  if (!transcript || !fs.existsSync(transcript))
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ // aiterm-parent-delivery — ほかの製品が、確定した回答をAitermの親配送へ頼む入口(ADR 0098)。
3
+ // Aitermが登録したhookと公式キューで渡す。頼む製品は自分のhookを登録せず、Aitermの中のfileも書かない。
4
+ // 結果はstdoutへ1行のJSONで返す。成功は {ok:true, schema, …}、失敗は {ok:false, schema, code, message, outcome_unknown} と exit 1。
5
+ // outcome_unknown:true は「受け付けたかどうか確定できない」。その時は送り直さない。
6
+ //
7
+ // aiterm-parent-delivery provider
8
+ // aiterm-parent-delivery codex verify --thread <uuid> [--codex-home <dir>]
9
+ // aiterm-parent-delivery codex submit --thread <uuid> --delivery <uuid> --text-file <file|-> [--codex-home <dir>]
10
+ // aiterm-parent-delivery codex state --thread <uuid> --delivery <uuid> [--codex-home <dir>]
11
+ //
12
+ // 「設定が整っている」「受け付けた」「実際に届いた」は別の事として返す。
13
+ // verify の steer:"enabled" は設定の話(hookが登録・承認済みで、親がhookの導入後に起きている)。届いた事は言わない。
14
+ // submit の queued_submission_id は、公式キューが受け付けた事。
15
+ // state は届き方の事実を返す。hook:"emitted" と turn_id は、Aitermのhookがその番へ本文を入れた事。queued は、今も公式キューに残っているか。
16
+ //
17
+ // 対象はCodexの親だけ。親の指定は、CodexがMCP要求へ付けるthreadIdと、そのCodexのCODEX_HOME。
18
+ import * as fs from "node:fs";
19
+ import * as path from "node:path";
20
+ import { createRequire } from "node:module";
21
+ import { fileURLToPath } from "node:url";
22
+ import { realCodexHome } from "aiterm-steer-delivery";
23
+ import { checkCodexParent, submitCodexParentAnswer, withCodexReceiver } from "./codex-parent-receiver.js";
24
+ import { codexHookDeliveryState, codexHookDirectory, codexInputDirectory } from "./codex-hook-state.js";
25
+ export const PARENT_DELIVERY_SCHEMA = "aiterm.parent-delivery.v1";
26
+ const pkg = createRequire(import.meta.url)("../package.json");
27
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
28
+ const USAGE = "usage: aiterm-parent-delivery provider | codex <verify|submit|state> --thread <uuid> [--delivery <uuid>] [--text-file <file|->] [--codex-home <dir>]";
29
+ class UsageError extends Error {
30
+ }
31
+ /** 同じ配送idの本文が、もう配送の記録にある。送り直しで同じ回答が2回届く事を止める。 */
32
+ class DuplicateError extends Error {
33
+ }
34
+ /** この配送idの所有記録(送る途中・hookが取り出した後)が、Aitermのhookの置き場に残っているか。hookを入れていない環境には記録が無い。 */
35
+ function deliveryRecorded(target, delivery) {
36
+ const root = codexHookDirectory();
37
+ if (!fs.existsSync(path.join(root, "inputs")) || !fs.existsSync(target.codex_home))
38
+ return false;
39
+ const directory = codexInputDirectory(root, target.codex_home, target.thread_id);
40
+ return ["pending", "claims"].some(kind => fs.existsSync(path.join(directory, kind, `${delivery}.json`)));
41
+ }
42
+ function option(args, name) {
43
+ const index = args.indexOf(name);
44
+ if (index < 0)
45
+ return undefined;
46
+ const value = args[index + 1];
47
+ if (value === undefined)
48
+ throw new UsageError(`${name} に値がありません。${USAGE}`);
49
+ return value;
50
+ }
51
+ function uuid(args, name) {
52
+ const value = option(args, name);
53
+ if (value === undefined)
54
+ throw new UsageError(`${name} は必須です。${USAGE}`);
55
+ if (!UUID_RE.test(value))
56
+ throw new UsageError(`${name} はUUIDで指定してください。${USAGE}`);
57
+ return value;
58
+ }
59
+ function parent(args) {
60
+ const home = option(args, "--codex-home");
61
+ if (home !== undefined && !path.isAbsolute(home))
62
+ throw new UsageError(`--codex-home は絶対pathで指定してください。${USAGE}`);
63
+ return { thread_id: uuid(args, "--thread"), codex_home: path.resolve(home ?? realCodexHome()) };
64
+ }
65
+ async function readText(source) {
66
+ if (source !== "-")
67
+ return fs.readFileSync(source, "utf8");
68
+ let text = "";
69
+ process.stdin.setEncoding("utf8");
70
+ for await (const chunk of process.stdin)
71
+ text += chunk;
72
+ return text;
73
+ }
74
+ /** 届き方の事実を読む。何も書き換えない。 */
75
+ async function deliveryState(target, delivery) {
76
+ const state = codexHookDeliveryState(target.codex_home, target.thread_id, delivery);
77
+ let hook = null;
78
+ let turn = null;
79
+ const root = codexHookDirectory();
80
+ if (fs.existsSync(path.join(root, "inputs")) && fs.existsSync(target.codex_home)) {
81
+ const directory = codexInputDirectory(root, target.codex_home, target.thread_id);
82
+ let claim = null;
83
+ try {
84
+ claim = JSON.parse(fs.readFileSync(path.join(directory, "claims", `${delivery}.json`), "utf8"));
85
+ }
86
+ catch (error) {
87
+ if (error.code !== "ENOENT")
88
+ throw error;
89
+ }
90
+ if (claim?.state === "emitted") {
91
+ hook = "emitted";
92
+ turn = typeof claim.turn_id === "string" ? claim.turn_id : null;
93
+ }
94
+ else if (claim?.state === "not_in_queue")
95
+ hook = "not_in_queue";
96
+ else if (state)
97
+ hook = state;
98
+ // 所有権のlinkだけを作った段階(まだ公式キューから取り出していない)は、受け付けた後と同じ。
99
+ else if (claim !== null || fs.existsSync(path.join(directory, "pending", `${delivery}.json`)))
100
+ hook = "pending";
101
+ }
102
+ try {
103
+ const queued = await withCodexReceiver(target, async (request) => {
104
+ let cursor = null;
105
+ do {
106
+ const page = await request("thread/queue/list", { threadId: target.thread_id, cursor, limit: 100 });
107
+ if (!Array.isArray(page?.data))
108
+ throw new Error("queue list invalid");
109
+ if (page.data.some((entry) => entry?.clientUserMessageId === delivery))
110
+ return true;
111
+ cursor = typeof page.nextCursor === "string" ? page.nextCursor : null;
112
+ } while (cursor);
113
+ return false;
114
+ });
115
+ return { state, hook, turn_id: turn, queued };
116
+ }
117
+ catch (error) {
118
+ const code = error?.delivery_code;
119
+ return { state, hook, turn_id: turn, queued: null, queue_error: typeof code === "string" ? code : "CODEX_QUEUE_UNREADABLE" };
120
+ }
121
+ }
122
+ /** この命令の場所と版。製品側の入口(aiterm-steer-delivery)が、呼べる相手かどうかを確かめるのに使う。 */
123
+ export function providerDescription() {
124
+ return { version: pkg.version, node: process.execPath, cli: fileURLToPath(import.meta.url) };
125
+ }
126
+ export async function runParentDelivery(argv) {
127
+ const [kind, command, ...args] = argv;
128
+ if (kind === "provider" && command === undefined)
129
+ return providerDescription();
130
+ if (kind !== "codex")
131
+ throw new UsageError(`対応する親は codex だけです。${USAGE}`);
132
+ switch (command) {
133
+ case "verify": {
134
+ const checked = await checkCodexParent(parent(args));
135
+ return { verified: true, thread: checked.thread, steer: checked.steer };
136
+ }
137
+ case "submit": {
138
+ const target = parent(args);
139
+ const delivery = uuid(args, "--delivery");
140
+ const source = option(args, "--text-file");
141
+ if (source === undefined)
142
+ throw new UsageError(`--text-file は必須です。${USAGE}`);
143
+ const text = await readText(source);
144
+ if (text.length === 0)
145
+ throw new UsageError("本文が空です。文字列は送っていません。");
146
+ // 配送idは本文ごとに新しいUUID。同じidをもう一度受けると、同じ回答が2回届く。
147
+ if (deliveryRecorded(target, delivery))
148
+ throw new DuplicateError("同じ配送idの本文は、もう受け付けています。文字列は送っていません。");
149
+ return await submitCodexParentAnswer(target, delivery, text);
150
+ }
151
+ case "state": {
152
+ const target = parent(args);
153
+ return await deliveryState(target, uuid(args, "--delivery"));
154
+ }
155
+ default: throw new UsageError(`未対応のcommandです: ${String(command)}。${USAGE}`);
156
+ }
157
+ }
158
+ function isDirectExecution() {
159
+ const entry = process.argv[1];
160
+ if (!entry)
161
+ return false;
162
+ try {
163
+ const a = fs.realpathSync(entry);
164
+ const b = fs.realpathSync(fileURLToPath(import.meta.url));
165
+ return a === b || (process.platform === "win32" && a.toLowerCase() === b.toLowerCase());
166
+ }
167
+ catch {
168
+ return false;
169
+ }
170
+ }
171
+ if (isDirectExecution()) {
172
+ try {
173
+ process.stdout.write(JSON.stringify({ ok: true, schema: PARENT_DELIVERY_SCHEMA, ...(await runParentDelivery(process.argv.slice(2))) }) + "\n");
174
+ }
175
+ catch (error) {
176
+ // 配送の誤り(delivery_code)と使い方の誤りは、文面が呼び出し元向けに決めてあるのでそのまま返す。
177
+ // それ以外の例外は、中身(path・本文)を出さずに固定の文面へ落とす。
178
+ const value = error;
179
+ const delivery = typeof value?.delivery_code === "string" ? value.delivery_code : null;
180
+ const usage = error instanceof UsageError;
181
+ const duplicate = error instanceof DuplicateError;
182
+ process.stdout.write(JSON.stringify({
183
+ ok: false, schema: PARENT_DELIVERY_SCHEMA,
184
+ code: delivery ?? (usage ? "PARENT_DELIVERY_USAGE" : duplicate ? "PARENT_DELIVERY_DUPLICATE" : "PARENT_DELIVERY_FAILED"),
185
+ message: delivery || usage || duplicate ? String(value.message) : "aiterm-parent-delivery: operation failed",
186
+ outcome_unknown: value?.outcome_unknown === true,
187
+ }) + "\n");
188
+ process.exitCode = 1;
189
+ }
190
+ }
@@ -1,7 +1,8 @@
1
1
  // Steerの正規導入。公式hookを登録・承認してから、旧中継の起動差し替えを解除する。本体はaiterm-steer-delivery。
2
+ import * as path from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
  import * as steer from "aiterm-steer-delivery";
4
- import { readRelayConfig } from "./codex-relay-config.js";
5
+ import { readRelayConfig, relayConfigDirectory } from "./codex-relay-config.js";
5
6
  import { configureCodexSteer as configureLegacyRelay } from "./setup-codex-relay.js";
6
7
  import { readCodexHookConfig } from "./codex-hook-state.js";
7
8
  import { AITERM_PROFILE } from "./steer-profile.js";
@@ -20,3 +21,41 @@ export async function configureCodexSteer(action = "status", overrides = {}) {
20
21
  ...overrides,
21
22
  });
22
23
  }
24
+ /**
25
+ * aiterm-setupを通らない導入(コンテナの起動時など)が、Codexの親配送のhookを登録する入口(ADR 0098)。
26
+ * - `home`を引数で受け、その下(`.codex`と`.config/aiterm-mcp/codex-parent-hooks`)だけを書く。
27
+ * - 動いているCodexは要らない。Codexの実行ファイルを一時的に起こし、登録の読戻しと承認を公式の口(hooks/list・config/batchWrite)で行う。
28
+ * - 登録済みで、公式の読戻しでも承認済みなら、何も書かない(`changed:false`)。登録が外れていた・commandが変わった時だけ書き直す。
29
+ * - 失敗は投げない。`{status:"failed", reason_code}`で返す(書き換える前の`hooks.json`は`.aiterm-backup`に残る)。
30
+ * 登録より前から動いているCodexは、起き直すまで同じ番への配送を受けられない(`restart_required`)。
31
+ */
32
+ export async function ensureCodexParentSteer(home, options = {}) {
33
+ try {
34
+ if (!path.isAbsolute(home))
35
+ throw new steer.SetupError("codex_steer_home_invalid", "homeは絶対pathで指定してください");
36
+ const directory = path.join(home, ".config", "aiterm-mcp", "codex-parent-hooks");
37
+ const codexHome = options.codex_home ?? path.join(home, ".codex");
38
+ const hook = fileURLToPath(new URL("./codex-parent-hook.js", import.meta.url));
39
+ const overrides = {
40
+ directory, codex_home: codexHome,
41
+ legacy: () => readRelayConfig(relayConfigDirectory(home)),
42
+ verify: (config, approve) => steer.verifyCodexHookRegistration(AITERM_PROFILE, config, approve, directory),
43
+ ...(options.binary ? { findBinary: () => options.binary } : {}),
44
+ ...options.runtime,
45
+ };
46
+ const previous = readCodexHookConfig(directory);
47
+ const expected = steer.codexHookCommand(steer.setupNodeExecutable(process.execPath), hook, directory);
48
+ if (previous?.enabled && previous.command === expected && path.resolve(previous.codex_home) === path.resolve(codexHome)) {
49
+ // 登録の読戻しが通れば書かない。通らない時(ほかの導入がhooks.jsonを書き直して登録が外れた等)は、下で登録し直す。
50
+ const status = await configureCodexSteer("status", overrides).catch(() => null);
51
+ if (status && (status.status === "ready" || status.status === "restart_required"))
52
+ return { ...status, changed: false };
53
+ }
54
+ return { ...await configureCodexSteer("enable", overrides), changed: true };
55
+ }
56
+ catch (error) {
57
+ const value = error;
58
+ const reason = typeof value?.delivery_code === "string" ? value.delivery_code : typeof value?.code === "string" ? value.code : "codex_steer_setup_failed";
59
+ return { status: "failed", reason_code: reason, changed: false };
60
+ }
61
+ }
@@ -9,6 +9,9 @@ import { lazyRelayExecutable } from "./lazy-relay.js";
9
9
  import { SetupError, runSetupCommand } from "./setup-platform.js";
10
10
  import { AITERM_PROFILE } from "./steer-profile.js";
11
11
  export { powershellInvocation } from "./setup-platform.js";
12
+ // aiterm-setupを通らない導入(コンテナの起動時)が、ClaudeとCursorのhookと同じ並びで呼べるように、ここからも出す。
13
+ export { ensureCodexParentSteer } from "./setup-codex-hooks.js";
14
+ export { writeDeliveryProvider, deliveryProviderFile } from "./delivery-provider.js";
12
15
  const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
13
16
  /**
14
17
  * 既にある登録が、今から書く登録と同じ本体を指しているか。
package/dist/setup.js CHANGED
@@ -9,6 +9,7 @@ import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { prepareBackend, runSetupCommand, SetupError } from "./setup-platform.js";
10
10
  import { configureIntegrations, configureParentHooks } from "./setup-integrations.js";
11
11
  import { configureCodexSteer, codexSteerSelected } from "./setup-codex-hooks.js";
12
+ import { writeDeliveryProvider } from "./delivery-provider.js";
12
13
  import { setupNodeExecutable } from "./setup-node.js";
13
14
  /** 実行中のNodeに付属するnpmの既定のglobal root。npmのprefixを環境変数や設定で別の場所へ向けていても変わらない。 */
14
15
  export function nodeDefaultGlobalRoot(execPath = process.execPath, platform = process.platform) {
@@ -95,6 +96,9 @@ export async function runSetup(options = {}) {
95
96
  else
96
97
  result.status = "ready";
97
98
  if (result.status === "ready") {
99
+ // ほかの製品がAitermの親配送を頼む入口(aiterm-parent-delivery)の場所を残す。
100
+ stage = "delivery_provider";
101
+ (options.provider ?? writeDeliveryProvider)(process.env.HOME ?? homedir(), registration);
98
102
  stage = "codex_steer";
99
103
  result.codex_steer = await (options.steer ?? configureCodexSteer)(options.codex_steer ?? (codexSteerSelected() ? "enable" : "status"));
100
104
  if (["failed", "unsupported", "restart_required"].includes(result.codex_steer.status)) {
package/docs/DESIGN.md CHANGED
@@ -68,6 +68,10 @@ harnessと中継が自分のために立てるprocessは数えない(Codexの`
68
68
  harnessが同じ親の下に同じargvで立て直した起動時のprocessも数えない(ADR 0083)。
69
69
  Aitermがprocess表を引くために起こしたprocess(`ps`、WindowsのPowerShellと付いて立つconsole host)も数えない(ADR 0084)。
70
70
  中継の後ろで眠っている本体を配送の引き取りのために起こすかは、`aiterm-delivery-wake`が返す(ADR 0082)。
71
+ ほかの製品がCodexの親へ回答を届ける時は、製品のhookを登録せず、`aiterm-parent-delivery`でAitermの親配送へ頼む(ADR 0098)。
72
+ Aitermの登録済みhookと公式キューで届け、設定(`steer`)・受付(`queued_submission_id`)・実際に届いた事(`hook:"emitted"`と`turn_id`)を分けて返す。
73
+ 命令の場所は`aiterm-setup`が`~/.config/aiterm-mcp/delivery-provider.json`へ残す。`aiterm-setup`を通らない導入は、
74
+ `dist/setup-integrations.js`の`ensureCodexParentSteer(home)`でCodexのhookを登録する。
71
75
  Claude Codeの入力待ちは、起動時の見出しが取得範囲から流れ出た後も、入力欄の形(`❯`行の上下の罫線)で読む。
72
76
  画面本文とargv本文は返さず、活動cursorには画面digestとprocess別CPUだけを持たせる。
73
77
  初回とpane再作成後の差分はnull。区間中にprocessが消えた時は観測できたCPU増分だけを返し、
@@ -104,6 +108,9 @@ Grokは待ち行列へ入れた後に「send now」を押す。旧turnは`cancel
104
108
  新turnが作業を継ぐので、完了判定はこの継ぎ目を完了と数えない。Grokが待ち行列へ入れない時とCursorの入力欄に本文が残る時は`steered`を返さない。
105
109
  Cursorのsubmitはadapterがextended keyboard protocolのEnterへ変換し、呼び出し側は通常のdispatchだけを使う。
106
110
  起動直後のClaude sessionへの初回dispatchは、他harnessと同じくTUIの入力受付を確認してから貼付とEnterを送る。
111
+ Codex・Grok・Cursorの席は、新しいturnを送るたびに入力受付を確かめる。起動の途中の入力欄を採らないよう、500msおきに11回続けて
112
+ 入力待ちを読む(ADR 0014)。席のharnessが自分の記録で「この起動でturnを1つ終えていて、次のturnを始めていない」と示している時は、
113
+ 100msおきに2回で通す(ADR 0097)。記録の上でturnが動いている間は、画面が入力待ちに見えても11回の確かめを通る。
107
114
 
108
115
  ### 親配送の共通モジュール化(設計)
109
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.55.3",
3
+ "version": "0.56.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": [
@@ -44,6 +44,7 @@
44
44
  "aiterm-runtime-errors": "dist/runtime-errors-cli.js",
45
45
  "aiterm-wait": "dist/aiterm-wait-cli.js",
46
46
  "aiterm-delivery-wake": "dist/delivery-wake-cli.js",
47
+ "aiterm-parent-delivery": "dist/parent-delivery-cli.js",
47
48
  "aiterm-update": "dist/update-cli.js"
48
49
  },
49
50
  "files": [
@@ -71,7 +72,7 @@
71
72
  },
72
73
  "dependencies": {
73
74
  "@modelcontextprotocol/sdk": "^1.29.0",
74
- "aiterm-steer-delivery": "^0.2.3",
75
+ "aiterm-steer-delivery": "^0.3.0",
75
76
  "ws": "8.21.3",
76
77
  "zod": "^4.4.3"
77
78
  },