aiterm-mcp 0.53.2 → 0.55.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,47 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.55.0] - 2026-10-06
11
+
12
+ ### 変更
13
+
14
+ - `agent_launch`は、起動時promptの開始を確認できなかった返り(`initial_prompt.status`が`submitted_unconfirmed`)に
15
+ `isError`を付けない(ADR 0093)。sessionは立ち、promptは入力欄を離れ、親への配送も登録済みなので、状態は
16
+ `initial_prompt`と本文で伝える。本文には「turnの開始は未確認(reason=…)。再送もagentの起動し直しもしない。
17
+ 確かめる時は pty_observe」の案内が付く。誤りの印でこの状態を見分けていた呼び出し側は、`initial_prompt.status`を見る。
18
+ 送信の途中で失敗した起動は、今までどおり誤りで返る。`remote`付きの起動は、現地が旧版で誤りを返しても同じ形で返す。
19
+
20
+ ### 修正
21
+
22
+ - Claude Code親が起こした子の回答が、親へ届かない事があった。開始未確認の返りを誤りで返していて、Claude Codeは
23
+ 誤りの返りでは受け口のhook(`PostToolUse`)を走らせない。配送は`sending`のまま残り、返りの本文は「回答は自動配送する。
24
+ 回収は不要」と案内していた。上の変更で、受け口が出来て回答が届く。
25
+ - 動いているClaude Codeを、入力待ち(`idle`/`composer_ready`)と読む事があった。Claude Code(2.1.289〜2.1.291で確認)は、
26
+ 複数行のprompt(貼り付けの形で入る)を送った後の約8秒、足元の行を`paste again to expand`へ置き換える。動作中の印に
27
+ していた`esc to interrupt`がその間は画面に無い。複数行のpromptを付けたClaudeの子の起動は毎回、開始未確認になり、
28
+ `pty_observe`もその間`idle`を返していた。動いている間は入力欄の上に進行行(`✶ Slithering… (3s · ↓ 225 tokens)`)が
29
+ 出続けるので、それも動作中の印にする。Linux・Mac・Windowsの実画面で確かめた。Codex・Grok・Cursorの読み方は変えていない。
30
+
31
+ ## [0.54.0] - 2026-10-06
32
+
33
+ ### 変更
34
+
35
+ - `pty_read(agent_transcript:true)`は、回答が空で終わったturnを誤りにせず、`text`が空文字列、`answer_empty`が`true`の
36
+ 結果で返す(ADR 0092)。`aiterm.pty-read-result.v1`へ`answer_empty`を足した(回答がある時は`false`、terminalの読み取りは`null`)。
37
+ 今までは、回答を空で終えただけのturnと、記録の形が変わって回答を読めなくなった時が、同じ
38
+ 「最終 assistant メッセージを特定できませんでした」の誤りになり、呼び出し側が見分けられなかった。
39
+ Codex(0.160.1)は、道具で用を済ませて回答を空で終えたturnを、`text`が空の`final_answer`として記録に残す。
40
+ 誤りにするのは、回答の場所を記録から見つけられない時だけにした(文面は同じ)。
41
+
42
+ ### 修正
43
+
44
+ - 親への自動配送で、Codexの子が回答を空で終えると、親へ`AGENT_RESULT_UNAVAILABLE: transcript がまだありません。
45
+ ターン完了後に再取得してください`が`outcome=error`で届いていた。turnは終わっているので、
46
+ 「子のターンは完了しましたが、回答の本文は空でした。」を`outcome=done`で届ける。Claude Code・Grok・Cursorの子も同じ。
47
+ - Codexの回答の読み取りは、turn ID付きの本文が空の時に、turn IDの無い旧形式の本文(前のturnの物かもしれない)へ
48
+ 落とす作りだった。turn ID付きの本文が記録にあれば、空でもそれを回答とする。今のCodexは旧形式を書かず、
49
+ 実記録で前のturnの回答が返った例は確認していない。
50
+
10
51
  ## [0.53.2] - 2026-10-05
11
52
 
12
53
  ### 修正
@@ -2215,7 +2256,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2215
2256
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2216
2257
  provenance.
2217
2258
 
2218
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.53.2...HEAD
2259
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.0...HEAD
2260
+ [0.55.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.54.0...v0.55.0
2261
+ [0.54.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.53.2...v0.54.0
2219
2262
  [0.53.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.53.1...v0.53.2
2220
2263
  [0.53.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.53.0...v0.53.1
2221
2264
  [0.53.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.52.3...v0.53.0
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.53.2** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.55.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
@@ -546,7 +546,10 @@ Claude Code初回起動の文字表示テーマ選択では、画面で選択済
546
546
  それでも描かれなければ、今までと同じく未送信(`initial_prompt=not_sent`)で返り、sessionは残る。
547
547
  WindowsのCodexもhook確認を認識し、npm shim経由の起動を一つのharnessとして識別する。
548
548
  初手の`initial_prompt.status`は`not_requested`/`not_sent`/`submitted_unconfirmed`/`started`を区別する。
549
- 未送信・未確認の失敗もsession付きstructuredContentを保持する。未確認のpromptを再送せず、返ったcursorで観測する。
549
+ 未送信の失敗もsession付きstructuredContentを保持する。
550
+ `submitted_unconfirmed`(promptは入力欄を離れたが、確認時間の内に開始を見られなかった)は誤りにしない。`isError`は付かず、
551
+ 本文に未確認の案内が付く。sessionは立っていて、親への配送も登録済み。未確認のpromptを再送せず、agentも起動し直さず、
552
+ 返ったcursorか`pty_observe`で観測する。
550
553
 
551
554
  Codexの実行中承認は`agent_approval({ action: "inspect", session_id })`の`prompt`と`choices`を確認し、
552
555
  `respond`へ`observed_prompt_digest`と`approval_choice`(`approve_once`/`deny`)を渡す。
@@ -648,7 +651,7 @@ handoff contextを前置きできる。この任意経路は`throughline >= 0.9.
648
651
  `THROUGHLINE_BIN`、次に`PATH`から解決し、不在・不正・空のexportはPTY作成前に明示失敗する。
649
652
  任意の`throughline_supplement_file`はThroughline 0.10.8以降と`throughline_source_session`を必要とし、Aitermは内容を解釈せずThroughlineへ渡す。
650
653
 
651
- エージェントの回答が画面tailより長ければ、`pty_read({ agent_transcript:true })`で再promptなしに全文回収する。既存の人間向けcontentは診断suffixを維持し、機械呼出し側は`aiterm.pty-read-result.v1`の`structuredContent.text`から回答本文だけを取得する。Claudeはlaunch相関Stop hook、Codexは通常rollout、Grokは最後の実user行以後の最後の空でないassistantメッセージだけ、Cursorはlaunch IDでbindした通常agent transcriptから同じturnを回収する。
654
+ エージェントの回答が画面tailより長ければ、`pty_read({ agent_transcript:true })`で再promptなしに全文回収する。既存の人間向けcontentは診断suffixを維持し、機械呼出し側は`aiterm.pty-read-result.v1`の`structuredContent.text`から回答本文だけを取得する。Claudeはlaunch相関Stop hook、Codexは通常rollout、Grokは最後の実user行以後の最後の空でないassistantメッセージだけ、Cursorはlaunch IDでbindした通常agent transcriptから同じturnを回収する。回答を空で終えたturnは誤りにせず、`text`が空文字列、`answer_empty`が`true`で返すので、回答を読めなかった時と見分けられる。回答の場所を記録から見つけられない時だけ誤りにする。
652
655
 
653
656
  ### 完了検出(5 層)
654
657
 
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.53.2** · 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.55.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
 
@@ -583,7 +583,10 @@ waits up to 50 seconds from startup. If nothing is drawn by then, it still retur
583
583
  A prompt-free launch without this option retains `startup.status="not_checked"`. `initial_prompt.status` distinguishes
584
584
  `not_requested`, `not_sent`, `submitted_unconfirmed`, and `started`. Failure responses retain structured session information.
585
585
  WindowsのCodexもhook確認を認識し、npm shim経由の起動を一つのharnessとして識別する。
586
- Do not resend an unconfirmed prompt; observe or wait using its returned cursor.
586
+ `submitted_unconfirmed` (the prompt left the composer, but the turn start was not observed within the confirmation window) is
587
+ not a tool error: `isError` is not set, the text carries an unconfirmed-start note, the session is alive and the parent
588
+ delivery stays registered. Do not resend an unconfirmed prompt or relaunch the agent; observe with `pty_observe` or wait
589
+ using its returned cursor.
587
590
 
588
591
  For a live Codex approval, inspect with `agent_approval({ action: "inspect", session_id })`, review `prompt` and `choices`,
589
592
  then respond with `observed_prompt_digest` and `approval_choice` (`approve_once` or `deny`). Unknown or changed dialogs return
@@ -689,7 +692,7 @@ unchanged. Optional `throughline_supplement_file` is passed unchanged to Through
689
692
  `throughline_source_session` and Throughline 0.10.8 or later; Aiterm does not read or classify the supplement. Throughline is resolved through `THROUGHLINE_BIN` and then `PATH`; a missing or invalid
690
693
  export fails before the PTY exists instead of silently launching clean.
691
694
 
692
- When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lines), callers recover it in full with `pty_read({ agent_transcript: true })`. It returns the most recently completed turn's final assistant message in plain text with no re-prompting. The existing human-readable content keeps its diagnostic suffix; machine callers read the answer alone from `structuredContent.text` in `aiterm.pty-read-result.v1`. Claude reads the bounded owner-only result captured by the launch-correlated Stop hook and verifies its digest/byte count; it never reads Claude's private transcript. Durable machine callers should use `claude_turn`: `issue` sends once, `recover` never sends, `pending` is distinct from unsafe or malformed state, and only `completed` carries the exact verified `raw_output`. Codex uses the normal rollout transcript's `task_complete.turn_id`; Grok returns the last non-empty assistant message after the last real user row, excluding tool-use preambles; Cursor uses the normal agent transcript bound to the launch ID and current turn. Missing or ambiguous attribution remains an explicit error.
695
+ When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lines), callers recover it in full with `pty_read({ agent_transcript: true })`. It returns the most recently completed turn's final assistant message in plain text with no re-prompting. The existing human-readable content keeps its diagnostic suffix; machine callers read the answer alone from `structuredContent.text` in `aiterm.pty-read-result.v1`. Claude reads the bounded owner-only result captured by the launch-correlated Stop hook and verifies its digest/byte count; it never reads Claude's private transcript. Durable machine callers should use `claude_turn`: `issue` sends once, `recover` never sends, `pending` is distinct from unsafe or malformed state, and only `completed` carries the exact verified `raw_output`. Codex uses the normal rollout transcript's `task_complete.turn_id`; Grok returns the last non-empty assistant message after the last real user row, excluding tool-use preambles; Cursor uses the normal agent transcript bound to the launch ID and current turn. A turn that completed with an empty answer is not an error: `text` is the empty string and `answer_empty` is `true`, so callers can tell it apart from an answer that could not be read. Missing or ambiguous attribution, including an answer that cannot be located in the record, remains an explicit error.
693
696
 
694
697
  ### Completion detection (5 layers)
695
698
 
package/dist/core.js CHANGED
@@ -2681,6 +2681,8 @@ function agentCompletionCursor(meta) {
2681
2681
  function transcriptUnavailable() {
2682
2682
  throw new AitermError(`transcript がまだありません。ターン完了後に再取得してください。${agentWaitGuide()}`, 2);
2683
2683
  }
2684
+ /** 完了したturnの回答が空だった時に、人とAIが読む表示へ出す文。機械はstructuredContentのanswer_emptyを見る。 */
2685
+ const AGENT_ANSWER_EMPTY_NOTICE = "このターンは完了しましたが、回答の本文は空でした。";
2684
2686
  function transcriptNotFound(vendor) {
2685
2687
  throw new AitermError(`最終 assistant メッセージを特定できませんでした(harness=${agentHarness(vendor)}, vendor=${vendor})。screen で確認してください。`, 2);
2686
2688
  }
@@ -2763,7 +2765,8 @@ export async function readAgentTranscriptResult(name, o = {}) {
2763
2765
  throw new AitermError(`operation ${operationId} はまだ完了していません。同じoperation_idで後から再取得してください。${agentWaitGuide(name)}`, 2);
2764
2766
  }
2765
2767
  const turnId = o.completion?.turn_id ?? done?.turn_id ?? null;
2766
- let text = "";
2768
+ // 各harnessの読み取りは、回答の場所を見つけたら文字列(空でもよい)、見つからなければnullを返す。
2769
+ let text;
2767
2770
  if (meta.kind === "claude") {
2768
2771
  if (!done)
2769
2772
  transcriptUnavailable();
@@ -2780,12 +2783,16 @@ export async function readAgentTranscriptResult(name, o = {}) {
2780
2783
  transcriptUnavailable();
2781
2784
  text = grokTranscriptText(meta, readTranscriptLines, transcriptUnavailable, o.completion?.turn_id);
2782
2785
  }
2783
- if (!text.trim())
2786
+ // 回答の場所が見つからない時だけ誤りにする。場所があって中身が空白だけのturnは「回答が空」として返す。
2787
+ if (text === null)
2784
2788
  transcriptNotFound(meta.kind);
2785
- if (o.lines != null)
2789
+ const answerEmpty = !text.trim();
2790
+ if (answerEmpty)
2791
+ text = "";
2792
+ else if (o.lines != null)
2786
2793
  text = text.split("\n").slice(-o.lines).join("\n");
2787
2794
  const rawChars = text.length;
2788
- const [body, outputMeta] = o.raw ? [text, ""] : reduceOutput(text, name, true);
2795
+ const [body, outputMeta] = answerEmpty || o.raw ? [text, ""] : reduceOutput(text, name, true);
2789
2796
  const transcriptMeta = [
2790
2797
  "agent_transcript",
2791
2798
  `vendor=${meta.kind}`,
@@ -2793,14 +2800,16 @@ export async function readAgentTranscriptResult(name, o = {}) {
2793
2800
  `harness=${agentHarness(meta.kind)}`,
2794
2801
  done?.operation_id ? `operation_id=${done.operation_id}` : null,
2795
2802
  `raw_chars=${rawChars}`,
2803
+ answerEmpty ? "answer_empty=true" : null,
2796
2804
  ].filter(Boolean).join(" ");
2797
2805
  return {
2798
2806
  text: body,
2799
- display: `${body}\n${outputMeta} [${transcriptMeta}]`,
2807
+ display: `${answerEmpty ? AGENT_ANSWER_EMPTY_NOTICE : body}\n${outputMeta} [${transcriptMeta}]`,
2800
2808
  vendor: meta.kind,
2801
2809
  turn_id: turnId,
2802
2810
  harness: agentHarness(meta.kind),
2803
2811
  raw_chars: rawChars,
2812
+ answer_empty: answerEmpty,
2804
2813
  };
2805
2814
  }
2806
2815
  /** 人間向け互換表示を維持する。機械利用は readAgentTranscriptResult の text を使う。 */
@@ -3314,6 +3323,20 @@ async function waitCursorPromptVisibleImpl(text, sample, sleepFn, opts = {}) {
3314
3323
  async function waitCursorPromptVisible(name, text) {
3315
3324
  return waitCursorPromptVisibleImpl(text, () => captureScreen(name, AGENT_TUI_READY_LINES), sleep);
3316
3325
  }
3326
+ // 起動時promptは入力欄を離れたが、確認時間の内にturnの開始を見られなかった時の案内。この返りは誤り(isError)にしない。
3327
+ // 誤りで返すと、Claude Code親は受け口のhook(PostToolUse)を走らせず、登録済みの配送が届かない(ADR 0093)。
3328
+ // 親が同じ依頼で起動し直すと子が2つ動くので、再送も再起動もしない事を先に書く。
3329
+ export const INITIAL_PROMPT_UNCONFIRMED_MARK = "turnの開始は未確認";
3330
+ export function initialPromptUnconfirmedNote(name, reason) {
3331
+ return (`注意: ${INITIAL_PROMPT_UNCONFIRMED_MARK}(reason=${reason})。promptは入力欄を離れているので、再送もagentの起動し直しもしない。` +
3332
+ `開始していれば結果は上の方法で届く。確かめる時は pty_observe(${name}) で状態を見る。`);
3333
+ }
3334
+ // 起動そのものは普通に返り、初手の開始だけが未確認の返りか。送信の途中で失敗した起動も同じstatusを持つ
3335
+ // (その時のstartup.reasonは失敗の理由になる)ので、起動が普通に返った形(ready/composer_ready)と組で見る。
3336
+ export function launchStartUnconfirmed(structured) {
3337
+ return structured.initial_prompt?.status === "submitted_unconfirmed"
3338
+ && structured.startup?.status === "ready" && structured.startup?.reason === "composer_ready";
3339
+ }
3317
3340
  export function agentSubmitResidueWarning(name, residue) {
3318
3341
  if (residue !== true)
3319
3342
  return "";
@@ -3613,6 +3636,7 @@ async function sendInitialAgentPromptInLock(name, text, o) {
3613
3636
  return {
3614
3637
  text: `initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset} harness=${agentHarness(meta.kind)}\n` +
3615
3638
  `起動時 prompt を送信した。${agentDispatchGuide(name, startOffset)}` +
3639
+ (delivery.status === "submitted_unconfirmed" ? `\n${initialPromptUnconfirmedNote(name, delivery.reason)}` : "") +
3616
3640
  agentSubmitResidueWarning(name, residue.residue),
3617
3641
  event_cursor: startOffset,
3618
3642
  submit_residue: residue.residue,
@@ -281,13 +281,42 @@ export function claudeTuiReady(screen) {
281
281
  // その下に描画された現在のcomposerを優先する。
282
282
  return !/^❯\s*(?:\d+\.|\[|Enable selected\b|No,\s*exit\b|Yes,\s*I trust this folder\b)/iu.test(lastMarker);
283
283
  }
284
+ // Claude Codeは動いている間、入力欄の上へ進行行を出し続ける(2.1.291の実画面 2026-10-06)。
285
+ // ✻ Swirling… (running UserPromptSubmit hook · 0s)
286
+ // * Swirling…
287
+ // ✶ Slithering… (3s · ↓ 225 tokens · thought for 2s)
288
+ // ✢ コマンド実行中… (12s · ↓ 1.0k tokens · thinking) ← 作業の一覧を使う時は、進行中の項目の文になる
289
+ // ⎿ ◼ 下の命令を1回だけ動かす ← 一覧は進行行の下へ字下げで並ぶ
290
+ // 終わると同じ位置が「✻ Cogitated for 11s · done 4:45 AM」へ変わる(「…」が無く、「·」で区切る)。
291
+ const CLAUDE_PROGRESS_LINE_RE = /^[·✢✳✶✻✽*∗] [^·…\s][^·…]*…(?: \(.*\))?\s*$/u;
292
+ // 入力欄の上の罫線から上へ、空行と字下げの行を飛ばし、行頭から始まる最初の行が進行行かを見る。
293
+ // 会話欄の行(「●」の回答、「❯」の依頼文、字下げの道具の出力)は進行行の形にならない。
294
+ function claudeTurnProgress(screen) {
295
+ const lines = screen.split(/\r?\n/u);
296
+ let marker = lines.length - 1;
297
+ while (marker >= 0 && !CLAUDE_COMPOSER_MARKER_RE.test(lines[marker]))
298
+ marker--;
299
+ if (marker <= 0 || !/^\s*─{8,}\s*$/u.test(lines[marker - 1]))
300
+ return false;
301
+ for (let i = marker - 2; i >= 0; i--) {
302
+ if (lines[i].trim() === "" || /^\s/u.test(lines[i]))
303
+ continue;
304
+ return CLAUDE_PROGRESS_LINE_RE.test(lines[i]);
305
+ }
306
+ return false;
307
+ }
308
+ // 足元の「esc to interrupt」は、貼り付けの知らせ(paste again to expand)へ置き換わる間は出ない。複数行のpromptを
309
+ // 送った後の約8秒がそうで、その間は動いているのに入力待ちと読んでいた(2.1.291、2026-10-06)。進行行も動作中の印にする。
310
+ export function claudeTuiBusy(screen) {
311
+ return /esc to interrupt/i.test(screen.split("\n").slice(-32).join("\n")) || claudeTurnProgress(screen);
312
+ }
284
313
  export function claudePaneObservation(screen) {
285
314
  const tail = screen.split("\n").slice(-32).join("\n");
286
315
  if (claudeLoginMethodMenu(screen))
287
316
  return { state: "blocked", reason: "vendor_onboarding_required" };
288
317
  if (/Do you want to proceed\?/.test(tail) && /(?:^|\n)\s*[❯>]?\s*\d+\.\s+(?:Yes|No)\s*$/m.test(tail))
289
318
  return { state: "blocked", reason: "tool_approval" };
290
- if (/esc to interrupt/i.test(tail))
319
+ if (claudeTuiBusy(screen))
291
320
  return { state: "busy", reason: "turn_running" };
292
321
  if (claudeTuiReady(screen))
293
322
  return { state: "idle", reason: "composer_ready" };
@@ -618,6 +618,9 @@ export function codexMoreReasoningChoice(screen) {
618
618
  return null;
619
619
  }
620
620
  // 回収対象turnの最終assistantメッセージをroot rollout transcriptから抽出する。
621
+ // 回答の場所が記録にあれば、中身が空でも文字列を返す。場所が見つからない時(記録の形が変わった時を含む)はnull。
622
+ // Codexは回答を空で終えたturnも、textが空のfinal_answerをturn ID付きで残す(0.160.1の実記録 2026-10-06。
623
+ // task_completeのlast_agent_messageはnull)。
621
624
  export function codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable, exactCompletion = false) {
622
625
  if (!meta.codex_home || !meta.vendor_session_id)
623
626
  transcriptUnavailable();
@@ -642,7 +645,7 @@ export function codexTranscriptText(meta, turnId, readTranscriptLines, transcrip
642
645
  if (typeof payload.last_agent_message === "string")
643
646
  return payload.last_agent_message;
644
647
  const text = matching.text();
645
- if (text)
648
+ if (text !== null)
646
649
  return text;
647
650
  transcriptUnavailable();
648
651
  }
@@ -650,7 +653,7 @@ export function codexTranscriptText(meta, turnId, readTranscriptLines, transcrip
650
653
  transcriptUnavailable();
651
654
  }
652
655
  const matching = codexTurnAnswer();
653
- let finalAnswer = "";
656
+ let finalAnswer = null;
654
657
  for (const line of lines) {
655
658
  if (!line.trim())
656
659
  continue;
@@ -670,7 +673,9 @@ export function codexTranscriptText(meta, turnId, readTranscriptLines, transcrip
670
673
  finalAnswer = payload.message;
671
674
  }
672
675
  }
673
- return matching.text() || finalAnswer;
676
+ // turnの本文が記録にあれば、空でもそれが回答。agent_message(turn IDの無い旧形式)は別turnの物かもしれないので、
677
+ // turnの本文が見つからず、かつ空でない時だけ使う。
678
+ return matching.text() ?? (finalAnswer?.trim() ? finalAnswer : null);
674
679
  }
675
680
  // turnのassistant本文を集める。Codexは作業途中の報告をphase=commentary、回答をphase=final_answerで残すので、
676
681
  // final_answerがあるturnは途中の報告を回答に含めない。phaseの無い旧形式はturnの本文を全部つなぐ。
@@ -691,7 +696,10 @@ function codexTurnAnswer() {
691
696
  finals.push(item.text);
692
697
  }
693
698
  },
699
+ // turnのassistant本文が1つも無ければnull(回答の場所が見つからない)。
694
700
  text() {
701
+ if (all.length === 0)
702
+ return null;
695
703
  return (finals.length > 0 ? finals : all).join("\n");
696
704
  },
697
705
  };
@@ -245,6 +245,10 @@ export function cursorTranscriptText(meta, readTranscriptLines, transcriptUnavai
245
245
  transcriptUnavailable();
246
246
  // tool_useで区切った本文の塊を持つ。道具を呼ぶ前の文は作業途中の報告なので、回答は最後の塊だけにする。
247
247
  let current = [[]];
248
+ // 回答の場所が分かるturnか。assistant行があり、そのpartがtextとtool_useだけの時に限る(実記録にあるのはこの2種類)。
249
+ // 知らないpartがあるturnは、本文がそこへ移ったのかもしれないので「回答が空」とは言わない。
250
+ let assistantRows = 0;
251
+ let unknownParts = false;
248
252
  let completed = null;
249
253
  for (const line of readTranscriptLines(transcript)) {
250
254
  if (!line.trim())
@@ -256,22 +260,34 @@ export function cursorTranscriptText(meta, readTranscriptLines, transcriptUnavai
256
260
  catch {
257
261
  continue;
258
262
  }
259
- if (record?.role === "user")
263
+ if (record?.role === "user") {
260
264
  current = [[]];
265
+ assistantRows = 0;
266
+ unknownParts = false;
267
+ }
261
268
  if (record?.role === "assistant" && Array.isArray(record?.message?.content)) {
269
+ assistantRows += 1;
262
270
  for (const part of record.message.content) {
263
271
  if (part?.type === "text" && typeof part?.text === "string")
264
272
  current.at(-1).push(part.text);
265
- else if (part?.type === "tool_use" && current.at(-1).length > 0)
266
- current.push([]);
273
+ else if (part?.type === "tool_use") {
274
+ if (current.at(-1).length > 0)
275
+ current.push([]);
276
+ }
277
+ else
278
+ unknownParts = true;
267
279
  }
268
280
  }
269
- if (record?.type === "turn_ended")
270
- completed = current.map((chunk) => [...chunk]);
281
+ if (record?.type === "turn_ended") {
282
+ completed = { chunks: current.map((chunk) => [...chunk]), located: assistantRows > 0 && !unknownParts };
283
+ }
271
284
  }
272
285
  if (completed === null)
273
286
  transcriptUnavailable();
274
- return (completed.filter((chunk) => chunk.length > 0).at(-1) ?? []).join("\n");
287
+ const text = (completed.chunks.filter((chunk) => chunk.length > 0).at(-1) ?? []).join("\n");
288
+ if (text.trim())
289
+ return text;
290
+ return completed.located ? "" : null;
275
291
  }
276
292
  export function createCursorAgentMetadata(name, cwd, initialPrompt, writeScope, lineage) {
277
293
  const launchId = randomBytes(16).toString("hex");
@@ -500,9 +500,12 @@ export function grokTranscriptText(meta, readTranscriptLines, transcriptUnavaila
500
500
  const replies = records
501
501
  .slice(lastUser + 1, end)
502
502
  .filter((record) => record?.type === "assistant" && typeof record?.content === "string")
503
- .map((record) => record.content.trim())
504
- .filter(Boolean);
505
- return replies.at(-1) ?? "";
503
+ .map((record) => record.content.trim());
504
+ // contentが文字列のassistant行が1つも無ければnull(回答の場所が見つからない)。
505
+ // 道具を呼ぶだけの行はcontentが空文字列で残るので、そういう行だけのturnは「回答が空」になる。
506
+ if (replies.length === 0)
507
+ return null;
508
+ return replies.filter(Boolean).at(-1) ?? "";
506
509
  }
507
510
  export function createGrokAgentMetadata(kind, name, cwd, initialPrompt, authPath, writeScope, lineageContext) {
508
511
  const launchId = randomBytes(16).toString("hex");
package/dist/index.js CHANGED
@@ -413,6 +413,7 @@ registerRemoteAwareTool("pty_read", {
413
413
  description: "セッションの出力をトークン削減して読む(既定は前回読取位置からの増分)。" +
414
414
  "削減: 制御文字除去 / 反復圧縮 / head+tail 折りたたみ+復元ヒント+メタ併記。" +
415
415
  "agent_transcript:true は agent session の直近完了ターンの最終 assistant メッセージを公開されたharness記録から平文で返す。" +
416
+ "回答が空で終わったターンは誤りにせず、空のtextとanswer_empty:trueを返す。" +
416
417
  "長い回答が screen tail で切れた時の回収用。",
417
418
  inputSchema: {
418
419
  session_id: z.string(),
@@ -454,6 +455,12 @@ registerRemoteAwareTool("pty_read", {
454
455
  turn_id: z.string().nullable(),
455
456
  harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]).nullable(),
456
457
  raw_chars: z.number().int().nonnegative().nullable(),
458
+ // remote付きは現地の結果をそのまま返す。この項目が無い旧版(0.53.2以前)の結果も通すのでoptional。
459
+ answer_empty: z
460
+ .boolean()
461
+ .nullable()
462
+ .optional()
463
+ .describe("agent_transcriptで、完了したturnの回答が空だった時だけtrue(textは空文字列)。回答を読めなかった時は誤りを返す。terminalではnull"),
457
464
  },
458
465
  }, async ({ session_id, wait, until, until_regex, timeout, screen, full, lines, line_range, raw, rtk, agent_transcript, operation_id }) => {
459
466
  try {
@@ -478,6 +485,7 @@ registerRemoteAwareTool("pty_read", {
478
485
  turn_id: result.turn_id,
479
486
  harness: result.harness,
480
487
  raw_chars: result.raw_chars,
488
+ answer_empty: result.answer_empty,
481
489
  },
482
490
  };
483
491
  }
@@ -521,6 +529,7 @@ registerRemoteAwareTool("pty_read", {
521
529
  turn_id: null,
522
530
  harness: null,
523
531
  raw_chars: null,
532
+ answer_empty: null,
524
533
  },
525
534
  };
526
535
  }
@@ -904,12 +913,17 @@ async function launchRemoteAgent(target, harness, args, extra) {
904
913
  return fail(new Error(`別端末でagentは起動しましたが、回答配送を登録できませんでした(session_id=${structured.session_id}): ${e instanceof Error ? e.message : String(e)}`));
905
914
  }
906
915
  const waited = await remoteCompletionWait(delivery, target, structured.session_id, structured.event_cursor);
916
+ // 現地が開始未確認を誤りで返す版(0.54.0以前)でも、ここでは誤りにしない(ADR 0093)。案内が無ければ足す。
917
+ const unconfirmed = core.launchStartUnconfirmed(structured);
918
+ const noted = result.content.some(item => item.type === "text" && item.text.includes(core.INITIAL_PROMPT_UNCONFIRMED_MARK));
907
919
  return {
908
920
  content: [...result.content, remoteNote(target, result),
921
+ ...(unconfirmed && !noted
922
+ ? [{ type: "text", text: core.initialPromptUnconfirmedNote(structured.session_id, structured.initial_prompt.reason) }] : []),
909
923
  { type: "text", text: `以後このsessionを操作する時は、同じremoteを付けて呼ぶ。${deliveryIdLine(delivery)}` }],
910
924
  structuredContent: { ...structured, ...waited, ...(delivery?.result() ? { parent_delivery: delivery.result() } : {}),
911
925
  remote_host: remoteLabel(target), remote_version: result.remote_version },
912
- ...(result.isError ? { isError: true } : {}),
926
+ ...(result.isError && !unconfirmed ? { isError: true } : {}),
913
927
  };
914
928
  }
915
929
  async function launchAgent(kind, args, extra) {
@@ -958,10 +972,11 @@ async function launchAgent(kind, args, extra) {
958
972
  }
959
973
  : {}),
960
974
  };
975
+ // 開始未確認(submitted_unconfirmed)は誤りにしない。sessionは立ち、promptは入力欄を離れ、配送も登録済み。
976
+ // 誤りで返すとClaude Code親は受け口のhookを走らせず、子の回答が届かない(ADR 0093)。状態はinitial_promptと本文で伝える。
961
977
  return {
962
978
  content: [{ type: "text", text: `session_id: ${sid}\n${hint}${deliveryIdLine(delivery)}` }],
963
979
  structuredContent: structured,
964
- ...(initialDelivery.status === "submitted_unconfirmed" ? { isError: true } : {}),
965
980
  };
966
981
  }
967
982
  catch (e) {
@@ -300,7 +300,8 @@ export class ParentDeliveryManager {
300
300
  ...(record.boundary.remote ? { remote: record.boundary.remote } : {}) });
301
301
  if (record.state !== "waiting")
302
302
  return;
303
- record.text = answer.text;
303
+ // 回答を空で終えたturnも完了として届ける。親が完了を知る手段はこの配送だけなので、本文の代わりに知らせを入れる。
304
+ record.text = answer.text.trim() ? answer.text : "子のターンは完了しましたが、回答の本文は空でした。";
304
305
  }
305
306
  else {
306
307
  record.text = completion.error ?? completion.rate_limit ?? "子のセッションが閉じられたため、回答はありません。";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.53.2",
3
+ "version": "0.55.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": [