aiterm-mcp 0.58.1 → 0.58.3

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,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.58.3] - 2026-10-12
11
+
12
+ ### 修正
13
+
14
+ - `agent_configure`が、Claude Codeの席のmodel/effortを替える時に、端末の`~/.claude/settings.json`の既定を書き換えていた件を直す(ADR 0111)。
15
+ 今までは`/model <名前>`と`/effort <値>`を打っていた。Claude Code(2.1.293・2.1.296で見た)は、この2つで選んだ値を
16
+ 「新しい会話の既定」として保存する(`model`と、`modelSettings.<機種>.effortLevel`)。1つの席を替えただけで、その端末でこの後に
17
+ 引数なしで立つ全部のClaude Codeの既定が変わり、利用者が自分で決めた既定も上書きしていた。
18
+ - `/model`を引数なしで開いた選ぶ画面を読み、行とeffortを合わせて、`s`(use this session only)で選ぶ。Enter(set as default)は押さない。
19
+ - 会話がcacheされている席で出る「Switch model?」の確かめは、1番(Yes, switch to …)が選ばれている時だけ進める。
20
+ - `model`は、catalogのID(`claude-opus-5-5`)、別名(`opus`)、表示名(`Opus 5.5`)のどれでも受ける。
21
+ - 選べない時は、選ぶ画面をEscで閉じて、何も変えずに断る。`s`の選び方が無い版では、保存する選び方へ落ちずに断る
22
+ (`CLAUDE_MODEL_SESSION_ONLY_UNAVAILABLE`)。選ぶ画面に無いmodelは`CLAUDE_MODEL_NOT_IN_PICKER`(文に選べる機種を並べる)、
23
+ 行はあるが選べないmodel(席のClaude Codeが古い時の「Update Claude Code to use this model」)は`CLAUDE_MODEL_NOT_SELECTABLE`、
24
+ そのmodelで選べないeffortは`CLAUDE_EFFORT_NOT_SELECTABLE`。今までは、Claude Codeが受け付けない値でも成功で返っていた。
25
+ - `reasoning_effort`が`low`/`medium`/`high`/`xhigh`/`max`/`ultracode`のどれでもない時は、打つ前に断る。
26
+ - 引数と返りの形(`aiterm.agent-configure-result.v1`)、Codex・Grok・Cursorの席の動きは変わらない。
27
+ - 「この会話だけ」の替えなので、席を閉じて立て直すと端末の既定へ戻る。立て直す時は`agent_launch`へ同じ値を渡す。
28
+ - 選ぶ画面に無いmodel(`opus[1m]`のような名前、古い機種のID)は、動いている席では替えられなくなる。今までは`/model <名前>`で通っていた
29
+ (端末の既定へ保存されていた)。席を閉じて、`agent_launch`の`model`で渡して立て直す。
30
+
31
+ ### 追加
32
+
33
+ - `pty_list`の構造の返り(`aiterm.pty-list-result.v1`)の席ごとの項目に、harness側の会話ID`vendor_session_id`を足す。
34
+ Claude CodeとGrokは起動時に`--session-id`で渡した値(席の中で会話を移した後は追わない)、CodexとCursorは掴んだ後の値。
35
+ 通常PTYと、まだ分からない席は`null`。文字の返りには前から載っていた。`remote`の先が0.58.2以前の時は、項目そのものが無い。
36
+
37
+ ## [0.58.2] - 2026-10-11
38
+
39
+ ### 修正
40
+
41
+ - Windowsで、sshの側(`remote`付きの呼び出しを含む)やserviceの下で立てた端末・席が、一覧から消えたり、途中で止められたりする件を直す(ADR 0109)。
42
+ psmux 3.3.8は、画面のある側でどの命令が呼ばれても、開けない本体の控えを「終わった物」として消す(psmux #650。直しは未公開)。
43
+ 画面の無いlogonで立てた本体は、同じ利用者の画面のある側から開けないので、ほかの席が動くたびに控えが消えていた。
44
+ その間、Aitermは`missing / session_missing`や「paneの活動観測を取得できません」を返し、席ごと止められる事もあった
45
+ (Windowsの実物で、sshの側で立てたGrokの席が4回中2回、文を送った20秒ほど後に消えた)。
46
+ - 端末を立てた直後に、本体のprocessへ「同じ利用者が名前を問う権利」(`PROCESS_QUERY_LIMITED_INFORMATION`)だけを足す。画面のある側が本体を開けるようになり、控えは消されない。
47
+ - 足すのは、psmuxが3.3.8で、画面のあるlogonの印(`SESSIONNAME`)が無い時だけ。アプリや端末から起こしたAitermでは、何も起こさない。
48
+ - sshの側で端末を1つ立てるたびに、約0.35秒かかる。足せなかった時も端末は使え、理由を1行だけstderrへ書く。
49
+ - 更新の前に立てた端末には効かない。
50
+ - Grokが文を受け取らなかった時に、待ち続けず、次の文と混ぜないようにする(ADR 0110)。
51
+ Grok(1.0.50)は、送った文を中のagentが受け取らないと、120秒で自分から取り消して、文を入力欄へ戻す。会話の記録には何も出ない
52
+ (Windowsでは、起こすstdioのMCPが8個以上の時に毎回こうなる。Grokの側の不具合で、これ自体はAitermでは直らない)。
53
+ - 完了待ちは、時間切れ(既定600秒)まで待たずに、結果なしの終了(`error`、exit 7)で返す。`error`の文に、Grokが受け取らなかった事を書く。
54
+ - 入力欄へ戻された文は、次の文を送る前に消す(返りの`pane_input_recovery`に`grok_unaccepted_prompt_cleared`)。今までは、前の文と次の文がつながって1通になっていた。
55
+ - 受け取り待ちの間(画面の「Waiting for the agent to accept the prompt」)に送った文は、差し込まずに、送らずに断る(`GROK_PROMPT_NOT_ACCEPTED_YET`)。
56
+ 今までは差し込みの道へ入り、文がGrokの待ち行列に残っていた(`STEER_STILL_QUEUED`、または`agent_steer`で返っていた)。
57
+
10
58
  ## [0.58.1] - 2026-10-10
11
59
 
12
60
  ### 修正
@@ -2476,7 +2524,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2476
2524
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2477
2525
  provenance.
2478
2526
 
2479
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.1...HEAD
2527
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.3...HEAD
2528
+ [0.58.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.2...v0.58.3
2529
+ [0.58.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.1...v0.58.2
2480
2530
  [0.58.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.0...v0.58.1
2481
2531
  [0.58.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.4...v0.58.0
2482
2532
  [0.57.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.3...v0.57.4
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.58.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.58.3** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
package/README.md CHANGED
@@ -227,7 +227,7 @@ collection is off by default and performs no network I/O. It ships via
227
227
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
228
228
  Release re-registers the Official MCP Registry entry.
229
229
 
230
- **Status:** actively maintained · current public release **v0.58.1** · 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).
230
+ **Status:** actively maintained · current public release **v0.58.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).
231
231
 
232
232
  ### Update and rollback
233
233
 
@@ -622,7 +622,7 @@ continue to use `claude_approval`.
622
622
  | `pty_read` | Read output, token-reduced (incremental by default) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk`, `agent_transcript`, `operation_id` |
623
623
  | `pty_key` | Send a control key | `session_id`, `key` (`C-c`/`Enter`/`Up`…) |
624
624
  | `pty_close` | Close idempotently; return `closed` / `already_closed` | `session_id` |
625
- | `pty_list` | Text and structured session list, with explicitly requested non-secret environment values | `env_keys?` |
625
+ | `pty_list` | Text and structured session list (each agent session carries its harness conversation ID as `vendor_session_id`), with explicitly requested non-secret environment values | `env_keys?` |
626
626
  | `pty_observe` | Pane/harness liveness, native process identity, state, and activity | `session_id`, `cursor?` |
627
627
  | `agent_launch` | Canonical agent launch; harness and model are independent | `harness`, `prompt?`, `model?`, `reasoning_effort?`, `cwd?`, `write_scope?`, `trust_project?`, `env_vars?`, `throughline_source_session?`, `throughline_supplement_file?` |
628
628
  | `agent_auth` | 公式CLIの認証を開始・確認・取消し、公式URL・device code・入力待ちを返す | `harness`, `action`, `session_id?`, `cwd?`, `env_vars?`, `relogin?` |
@@ -664,6 +664,8 @@ enables collection on that host.
664
664
 
665
665
  `agent_configure({ session_id, model?, reasoning_effort? })` changes a running Claude, Codex, Grok, or Cursor TUI through the harness's standard controls, preserving the PTY and conversation context.
666
666
 
667
+ Claude Codeの席では、`/model`の選ぶ画面の`s`(use this session only)で替える。`/model <名前>`・`/effort <値>`は選んだ値を端末の`~/.claude/settings.json`へ新しい会話の既定として保存するので、打たない(ADR 0111)。その選び方が無い版、選ぶ画面に無いmodel、そのmodelで選べないeffortは、何も変えずに断る(`CLAUDE_MODEL_SESSION_ONLY_UNAVAILABLE`・`CLAUDE_MODEL_NOT_IN_PICKER`・`CLAUDE_EFFORT_NOT_SELECTABLE`)。席を閉じて立て直すと端末の既定へ戻るので、立て直す時は`agent_launch`へ同じ`model`・`reasoning_effort`を渡す。
668
+
667
669
  `agent_auth({ harness, action:"start"|"status"|"cancel", session_id?, cwd?, env_vars?, relogin? })`は、各harnessの公式CLIで認証を進める。Claudeは`claude auth login`、CodexとGrokは`login --device-auth`、Cursorは`NO_OPEN_BROWSER=1 cursor-agent login`を使い、資格情報は各CLIだけが保存する。Aitermは資格情報を読取り・copy・編集せず、独自OAuthも実装しない。`remote`は他toolと同じ標準対応。
668
670
 
669
671
  `start`は、公式の状態が認証済みなら何も起こさず`authenticated`(`session_id:null`)を返す。`relogin:true`を付けると、認証済みに見えても公式ログインを開始し、`session_id`付きの結果を返す。別のアカウントへ入り直す時と、ログインの期限切れを状態から見抜けない時に使う。Aitermは資格情報を消さず、置き換えは公式CLIが行う。ただし、公式CLIがログインを始めた時点で元のログインを消す事がある(Codex 0.160.0の`codex login --device-auth`は、始めた時点で`auth.json`を消す。途中で`cancel`しても戻らない)。Claude CodeとCursorは、途中で`cancel`すれば元の資格情報が残る(無効な資格情報で確認)。
package/dist/core.js CHANGED
@@ -18,12 +18,12 @@ import { paneTokenHint } from "./harnesses/pane-tokens.js";
18
18
  import { unfinishedDeliveriesOwnedBy } from "./parent-delivery-owners.js";
19
19
  import { readRuntimeProcesses, processSubtree, parentProcess, processIdentity, backgroundProcesses, lazyRelayProcess, aitermProcessProbe, windowsConsoleHost, processElapsedSeconds } from "./process-runtime.js";
20
20
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
21
- import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, tmuxNewSession, registeredSessionEnvironment, attachCommand, normalizePaneCommand, atomicShellMultiline, appendMarkSentinel, markShellCommand, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
21
+ import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, tmuxNewSession, openPsmuxServerToDesktop, 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, grokTurnSettled, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
24
+ import { grokAuthPlan, grokAuthStatus, grokAuthPane, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokModelChoices, grokEventsTranscript, latestGrokCompletion, grokTurnSettled, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokPromptAwaitingAccept, grokUnacceptedPromptRestored, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
25
25
  import { codexAuthPlan, codexAuthStatus, codexAuthPane, bindCodexTranscriptSession, latestCodexCompletion, codexTurnSettled, codexRunningTurn, 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
- 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";
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, claudeModelPicker, claudeModelPickerRow, claudeModelSwitchConfirm, claudeModelSwitchResult, 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";
29
29
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
@@ -695,6 +695,8 @@ export function openSession(name, shell = "bash", envVars = []) {
695
695
  const launch = sessionEnvironmentLaunch(shell, environment);
696
696
  const r = tmuxNewSession(true, nm, launch, environment.map(entry => entry.slice(0, entry.indexOf("="))));
697
697
  if (r.code === 0) {
698
+ // Windowsで、画面の無いlogon(ssh・service)で立てた本体の控えを、画面のある側のpsmuxに消させない(psmux 3.3.8)。
699
+ openPsmuxServerToDesktop(nm);
698
700
  if (launch.register_after_start)
699
701
  for (const entry of environment) {
700
702
  const at = entry.indexOf("=");
@@ -1336,7 +1338,8 @@ export function listSessionsResult(envKeys = []) {
1336
1338
  }
1337
1339
  const meta = tryLoadAgentMetadata(name);
1338
1340
  sessions.push({ session_id: name, current_command: normalizePaneCommand(command), attached: Number(attached) > 0,
1339
- width: Number(width), height: Number(height), harness: meta ? agentHarness(meta.kind) : null, environment });
1341
+ width: Number(width), height: Number(height), harness: meta ? agentHarness(meta.kind) : null,
1342
+ vendor_session_id: meta?.vendor_session_id ?? null, environment });
1340
1343
  }
1341
1344
  return { schema: "aiterm.pty-list-result.v1", observed_at: new Date().toISOString(), sessions };
1342
1345
  }
@@ -3126,7 +3129,14 @@ export async function observeAgentDone(name, o = {}) {
3126
3129
  return observeCursorDone(meta, timeout, o.cursor, (session) => captureScreen(session, 0), o.signal);
3127
3130
  }
3128
3131
  if (meta.kind === "grok" && meta.completion_route === "grok_transcript") {
3129
- return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit, o.signal);
3132
+ // 上限の知らせと、受け取られなかった文の知らせは、同じ見回りで読んだ1枚の画面から見る(画面を読むたびにtmuxを起動する)。
3133
+ let read = { at: -Infinity, screen: "" };
3134
+ const screen = (session) => {
3135
+ if (performance.now() - read.at > AGENT_RATE_LIMIT_POLL_MS / 4)
3136
+ read = { at: performance.now(), screen: captureScreen(session, 0) };
3137
+ return read.screen;
3138
+ };
3139
+ return observeGrokDone(meta, timeout, o.cursor, (_kind, session) => grokRateLimitDialog(screen(session))?.message ?? null, o.signal, session => grokUnacceptedPromptRestored(screen(session)));
3130
3140
  }
3131
3141
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
3132
3142
  // 境界の優先順: dispatch receipt の event_cursor(起動順序に依存しない)→ operation相関
@@ -4110,6 +4120,166 @@ export async function listAgentModels(kind, options = {}) {
4110
4120
  case "cursor": return cursorModelChoices(bin, cwd);
4111
4121
  }
4112
4122
  }
4123
+ const CLAUDE_MODEL_PICKER_TIMEOUT_MS = 8_000;
4124
+ const CLAUDE_MODEL_PICKER_KEY_TIMEOUT_MS = 1_500;
4125
+ const CLAUDE_MODEL_SWITCH_TIMEOUT_MS = 15_000;
4126
+ const CLAUDE_EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"];
4127
+ /**
4128
+ * Claude Codeの席のmodel/effortを、この会話だけで替える(ADR 0111)。
4129
+ * `/model <名前>` と `/effort <値>` は、選んだ値を端末の ~/.claude/settings.json へ新しい会話の既定として保存する
4130
+ * (2.1.296で確かめた)。引数なしの `/model` の選ぶ画面で s を押すと保存されないので、画面を1歩ずつ読んでそちらを打つ。
4131
+ * 保存しない選び方が画面に無い時、行やeffortを選べない時は、Escで閉じて何も変えずに断る。
4132
+ */
4133
+ async function configureClaudeForSessionOnly(name, meta, model, effort) {
4134
+ const screen = () => captureScreen(name, AGENT_TUI_READY_LINES);
4135
+ const until = async (read, timeoutMs) => {
4136
+ const deadline = performance.now() + timeoutMs;
4137
+ do {
4138
+ const got = read();
4139
+ if (got)
4140
+ return got;
4141
+ await sleep(100);
4142
+ } while (performance.now() < deadline);
4143
+ return null;
4144
+ };
4145
+ // Escは、選ぶ画面か確かめの画面が出ている間だけ打つ(入力欄へ戻った後のEscは、ほかの操作を取り消してしまう)。
4146
+ const refuse = async (code, reason) => {
4147
+ for (let i = 0; i < 3; i++) {
4148
+ const now = screen();
4149
+ if (!claudeModelPicker(now) && !claudeModelSwitchConfirm(now))
4150
+ break;
4151
+ sendMenuChoice(name, "Escape");
4152
+ await sleep(300);
4153
+ }
4154
+ throw new AitermError(`${code} vendor=claude session=${name}\n${reason}。modelとreasoning_effortは変えていません。`, 2);
4155
+ };
4156
+ const picker = () => claudeModelPicker(screen());
4157
+ const cursorOf = (p) => p?.rows.find((row) => row.cursor) ?? null;
4158
+ // 1回押して、画面の今の行かeffortが変わるまで待つ。変わらなければnull(端で止まった・キーが落ちた)。
4159
+ // キーが落ちる事があるので、変わらなければ1度だけ押し直す。
4160
+ const press = async (key, changed) => {
4161
+ for (let attempt = 0; attempt < 2; attempt++) {
4162
+ sendMenuChoice(name, key);
4163
+ const next = await until(() => { const p = picker(); return p && changed(p) ? p : null; }, CLAUDE_MODEL_PICKER_KEY_TIMEOUT_MS);
4164
+ if (next)
4165
+ return next;
4166
+ }
4167
+ return null;
4168
+ };
4169
+ await sendAgentPromptText(name, "/model");
4170
+ let now = await until(picker, CLAUDE_MODEL_PICKER_TIMEOUT_MS);
4171
+ if (!now) {
4172
+ // 読めない形の画面が開いたままだと、席が次の文を受け取れない。入力待ちでも動作中でもない時だけ、Escを1度打って閉じる。
4173
+ let state = claudePaneObservation(screen());
4174
+ if (state.state === "unknown") {
4175
+ sendMenuChoice(name, "Escape");
4176
+ await sleep(500);
4177
+ state = claudePaneObservation(screen());
4178
+ }
4179
+ throw new AitermError(`CLAUDE_MODEL_PICKER_NOT_SHOWN vendor=claude session=${name} state=${state.state}/${state.reason}\n` +
4180
+ "Claude Codeの /model の選ぶ画面を読めません(Claude Codeの版で画面の形が変わった見込み)。modelとreasoning_effortは変えていません。", 2);
4181
+ }
4182
+ if (!now.session_only) {
4183
+ return refuse("CLAUDE_MODEL_SESSION_ONLY_UNAVAILABLE", "このClaude Codeの /model の選ぶ画面に、この会話だけで使う選び方(s to use this session only)がありません。" +
4184
+ "端末の既定を書き換える選び方は使いません");
4185
+ }
4186
+ let targetIndex = null;
4187
+ if (model) {
4188
+ // 見えていない行は、下へ送りながら集める(一周するか、端で止まるまで)。
4189
+ const known = new Map(now.rows.map((row) => [row.index, row]));
4190
+ const start = cursorOf(now);
4191
+ if (!start)
4192
+ return refuse("CLAUDE_MODEL_PICKER_UNREADABLE", "選ぶ画面の今の行を読めません");
4193
+ let at = start.index;
4194
+ const seen = new Set([at]);
4195
+ for (let i = 0; i < 80; i++) {
4196
+ const before = at;
4197
+ const next = await press("Down", (p) => { const c = cursorOf(p); return !!c && c.index !== before; });
4198
+ if (!next)
4199
+ break;
4200
+ now = next;
4201
+ for (const row of next.rows)
4202
+ known.set(row.index, row);
4203
+ at = cursorOf(next).index;
4204
+ if (seen.has(at))
4205
+ break;
4206
+ seen.add(at);
4207
+ }
4208
+ const target = claudeModelPickerRow([...known.values()], model);
4209
+ if (!target) {
4210
+ const names = [...known.values()].sort((a, b) => a.index - b.index).map((row) => row.label).join(", ");
4211
+ return refuse("CLAUDE_MODEL_NOT_IN_PICKER", `Claude Codeの /model の選ぶ画面に ${model} がありません(選べる機種: ${names})`);
4212
+ }
4213
+ for (let i = 0; i < 80 && at !== target.index; i++) {
4214
+ const before = at;
4215
+ const next = await press(target.index > at ? "Down" : "Up", (p) => { const c = cursorOf(p); return !!c && c.index !== before; });
4216
+ if (!next)
4217
+ break;
4218
+ now = next;
4219
+ at = cursorOf(next).index;
4220
+ }
4221
+ if (at !== target.index)
4222
+ return refuse("CLAUDE_MODEL_PICKER_UNREADABLE", `選ぶ画面で ${target.label} の行へ動けません`);
4223
+ // 起きた後にClaude Codeが更新された席では、「Update Claude Code to use this model」の行が出る。その行にはEnterもsも無い。
4224
+ if (!now.session_only) {
4225
+ return refuse("CLAUDE_MODEL_NOT_SELECTABLE", `${target.label} は、この席のClaude Codeでは選べません(選ぶ画面のその行に、選ぶためのキーが出ていません。席のClaude Codeが古い見込み)`);
4226
+ }
4227
+ targetIndex = target.index;
4228
+ }
4229
+ if (effort) {
4230
+ const wanted = CLAUDE_EFFORT_ORDER.indexOf(effort);
4231
+ const row = cursorOf(now)?.label ?? model ?? "このmodel";
4232
+ // そのmodelに無い段は飛ばされて行き来になるので、回数で打ち切る。
4233
+ for (let i = 0; i < CLAUDE_EFFORT_ORDER.length + 3 && now.effort !== effort; i++) {
4234
+ const current = now.effort;
4235
+ const from = current === null ? -1 : CLAUDE_EFFORT_ORDER.indexOf(current);
4236
+ if (from < 0)
4237
+ return refuse("CLAUDE_EFFORT_NOT_SELECTABLE", `${row} では reasoning_effort を選べません`);
4238
+ const next = await press(wanted > from ? "Right" : "Left", (p) => p.effort !== current);
4239
+ if (!next)
4240
+ break;
4241
+ now = next;
4242
+ }
4243
+ if (now.effort !== effort)
4244
+ return refuse("CLAUDE_EFFORT_NOT_SELECTABLE", `${row} では reasoning_effort=${effort} を選べません`);
4245
+ }
4246
+ // 打つ直前に、もう1度読んで確かめる。
4247
+ const last = picker();
4248
+ const lastRow = cursorOf(last);
4249
+ if (!last || !lastRow || !last.session_only
4250
+ || (targetIndex !== null && lastRow.index !== targetIndex)
4251
+ || (effort && last.effort !== effort)) {
4252
+ return refuse("CLAUDE_MODEL_PICKER_UNREADABLE", "選ぶ直前の画面が、選んだ行・effortと合いません");
4253
+ }
4254
+ sendMenuChoice(name, "s");
4255
+ let confirmed = false;
4256
+ const result = await until(() => {
4257
+ const text = screen();
4258
+ const confirm = claudeModelSwitchConfirm(text);
4259
+ if (confirm === "yes_selected" && !confirmed) {
4260
+ // 会話が今のmodelでcacheされている時の確かめ。1番(Yes, switch)が選ばれている時だけ進める。
4261
+ confirmed = true;
4262
+ sendMenuChoice(name, "Enter");
4263
+ return null;
4264
+ }
4265
+ if (confirm || claudeModelPicker(text))
4266
+ return null;
4267
+ return claudeModelSwitchResult(text);
4268
+ }, CLAUDE_MODEL_SWITCH_TIMEOUT_MS);
4269
+ if (!result)
4270
+ return refuse("CLAUDE_MODEL_SWITCH_UNCONFIRMED", "Claude Codeの切り替えの知らせを確認できません");
4271
+ if (result.scope === "saved_as_default") {
4272
+ throw new AitermError(`CLAUDE_MODEL_SAVED_AS_DEFAULT vendor=claude session=${name}\n` +
4273
+ `Claude Codeが、選んだ値を端末の既定として保存したと返しました(${result.line})。~/.claude/settings.json を確かめてください。`, 2);
4274
+ }
4275
+ if (result.scope !== "session_only") {
4276
+ throw new AitermError(`CLAUDE_MODEL_SWITCH_REFUSED vendor=claude session=${name}\n` +
4277
+ `Claude Codeがmodelを切り替えませんでした(${result.line})。modelとreasoning_effortは変えていません。`, 2);
4278
+ }
4279
+ const ready = await waitAgentTuiReady(name, meta, AGENT_TUI_READY_TIMEOUT_MS);
4280
+ if (!ready.ready)
4281
+ throw new AitermError("Claudeのmodel変更完了を確認できません", 2);
4282
+ }
4113
4283
  export async function configureAgent(name, opts) {
4114
4284
  assertSessionName(name);
4115
4285
  const model = opts.model?.trim() || null;
@@ -4129,13 +4299,14 @@ export async function configureAgent(name, opts) {
4129
4299
  throw new AitermError(`agent session '${name}' は入力待ちではありません`, 2);
4130
4300
  }
4131
4301
  if (meta.kind === "claude") {
4132
- if (model) {
4133
- await sendAgentPromptText(name, `/model ${model}`);
4134
- const modelReady = await waitAgentTuiReady(name, meta, AGENT_TUI_READY_TIMEOUT_MS);
4135
- if (!modelReady.ready)
4136
- throw new AitermError(`Claudeのmodel変更完了を確認できません`, 2);
4137
- }
4138
- if (effort) {
4302
+ // ultracodeはClaude Codeが自分で「this session only」と返すsession設定で、選ぶ画面には出ない。
4303
+ const level = effort && CLAUDE_EFFORTS.has(effort) ? effort : null;
4304
+ if (effort && !level && effort !== "ultracode") {
4305
+ throw new AitermError("Claude Code の reasoning_effort は low/medium/high/xhigh/max/ultracode のいずれかです。設定は変えていません。", 2);
4306
+ }
4307
+ if (model || level)
4308
+ await configureClaudeForSessionOnly(name, meta, model, level);
4309
+ if (effort && !level) {
4139
4310
  await sendAgentPromptText(name, `/effort ${effort}`);
4140
4311
  const effortReady = await waitAgentTuiReady(name, meta, AGENT_TUI_READY_TIMEOUT_MS);
4141
4312
  if (!effortReady.ready)
@@ -4354,6 +4525,20 @@ async function dispatchAgentTurnInLock(name, text, o) {
4354
4525
  }
4355
4526
  sendKey(name, limitDialog.dismissKey);
4356
4527
  }
4528
+ // Grokが受け取らずに入力欄へ戻した文が残っていると、次の文とつながって1通になる。送る前に消す。
4529
+ // 入力欄に文がある時のC-cは、入力欄を空にするだけ(Grok 1.0.46・1.0.50実測)。空の時に押すと終了の構えになるので、文がある時だけ押す。
4530
+ let grokRestoredPromptCleared = false;
4531
+ if (meta.kind === "grok" && !limitDialog && grokUnacceptedPromptRestored(captureScreen(name, 0))) {
4532
+ sendKey(name, "C-c");
4533
+ for (let i = 0; i < 20 && !grokRestoredPromptCleared; i++) {
4534
+ await sleep(100);
4535
+ grokRestoredPromptCleared = !grokUnacceptedPromptRestored(captureScreen(name, 0));
4536
+ }
4537
+ if (!grokRestoredPromptCleared) {
4538
+ throw new AitermError(`GROK_UNACCEPTED_PROMPT_NOT_CLEARED vendor=grok session=${name}\n` +
4539
+ "Grokが受け取らずに入力欄へ戻した前の文を、消せませんでした。文字列は送信していません。pty_read(screen:true)で入力欄を確かめてください。", 2);
4540
+ }
4541
+ }
4357
4542
  if (meta.kind !== "claude" || claudeColdStart) {
4358
4543
  // 上限のpanelを閉じた直後(Grok)は、画面が落ち着くのを11回の確かめで待つ。
4359
4544
  const recovery = await waitAgentTuiReadyAfterCodexRateLimitRecovery(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS, { settledTurn: !limitDialog });
@@ -4380,6 +4565,8 @@ async function dispatchAgentTurnInLock(name, text, o) {
4380
4565
  paneInputRecovery.push("codex_rate_limit_model_switch_kept_current");
4381
4566
  if (limitDialog)
4382
4567
  paneInputRecovery.push("grok_rate_limit_dialog_dismissed");
4568
+ if (grokRestoredPromptCleared)
4569
+ paneInputRecovery.push("grok_unaccepted_prompt_cleared");
4383
4570
  const startOffset = agentCompletionCursor(meta);
4384
4571
  // promptなしで起動したCursorは、最初のdispatch時点ではtranscriptとの相関markerをまだ持たない。
4385
4572
  // その1回だけlaunch contextを加え、以後はbind済みconversationへ通常textだけを送る。
@@ -4539,6 +4726,12 @@ async function sendAgentMessageInLock(name, text, o) {
4539
4726
  // Codexは回答を流している間、画面に動作中の行を出さない。その間に届いた文を、Codexは動いているturnへ取り込む(ADR 0101)。
4540
4727
  if (!running && meta.kind === "codex" && await codexTurnRunningBehindIdleScreen(name, meta, screen))
4541
4728
  running = true;
4729
+ // Grokが前の文をまだ受け取っていない間は、差し込む先の番が無い。待ち行列へ入れても動かず、Grokが前の文を取り消した後も残る。
4730
+ if (running && meta.kind === "grok" && grokPromptAwaitingAccept(screen)) {
4731
+ throw new AitermError(`GROK_PROMPT_NOT_ACCEPTED_YET vendor=grok session=${name}\n` +
4732
+ "Grokが前の文をまだ受け取っていません(画面の「Waiting for the agent to accept the prompt」)。文字列は送信していません。" +
4733
+ "前の文の完了待ちが終わってから送り直してください。Grokは120秒受け取れないと、前の文を自分から取り消します。", 2);
4734
+ }
4542
4735
  if (running) {
4543
4736
  return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery, preface: o.preface });
4544
4737
  }
@@ -373,6 +373,97 @@ export function claudeUsageLimit(screen) {
373
373
  }
374
374
  return null;
375
375
  }
376
+ function lastLineIndex(lines, re) {
377
+ for (let i = lines.length - 1; i >= 0; i--)
378
+ if (re.test(lines[i]))
379
+ return i;
380
+ return -1;
381
+ }
382
+ export function claudeModelPicker(screen) {
383
+ const lines = screen.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").split(/\r?\n/u);
384
+ const head = lastLineIndex(lines, /^\s*Select model\s*$/u);
385
+ if (head < 0)
386
+ return null;
387
+ const body = lines.slice(head + 1);
388
+ const foot = body.findIndex((line) => /\bEsc to cancel\b/u.test(line));
389
+ if (foot < 0)
390
+ return null;
391
+ const rows = [];
392
+ let effort = null;
393
+ for (const line of body.slice(0, foot)) {
394
+ const row = /^\s*([❯↓↑]\s+)?(\d+)\.\s+(.*?)\s*$/u.exec(line);
395
+ if (row) {
396
+ const name = row[3].split(/\s{2,}/u)[0];
397
+ rows.push({
398
+ index: Number(row[2]),
399
+ label: name.replace(/\s*✔$/u, ""),
400
+ cursor: row[1]?.includes("❯") === true,
401
+ current: /✔$/u.test(name),
402
+ });
403
+ continue;
404
+ }
405
+ const level = /(?:^|\s)(low|medium|high|xhigh|max) effort\b/iu.exec(line);
406
+ if (level)
407
+ effort = level[1].toLowerCase();
408
+ }
409
+ if (rows.length === 0)
410
+ return null;
411
+ return { rows, effort, session_only: /\bs to use this session only\b/u.test(body[foot]) };
412
+ }
413
+ /**
414
+ * 指定されたmodel(catalogのID・別名・表示名)が、選ぶ画面のどの行かを決める。
415
+ * `claude-opus-5-5` は「Opus 5.5」、別名の `opus` はその系統で番号が一番若い行(catalogの並びで最新)。
416
+ */
417
+ export function claudeModelPickerRow(rows, model) {
418
+ const want = model.trim().toLowerCase();
419
+ const sorted = [...rows].sort((a, b) => a.index - b.index);
420
+ const id = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?$/u.exec(want);
421
+ const name = id ? `${id[1]} ${id[2]}${id[3] ? `.${id[3]}` : ""}` : want;
422
+ return sorted.find((row) => row.label.toLowerCase() === name)
423
+ ?? (want === "default" ? sorted.find((row) => /^default\b/iu.test(row.label)) : undefined)
424
+ ?? (/^[a-z]+$/u.test(want) ? sorted.find((row) => row.label.toLowerCase().startsWith(`${want} `) && /^\S+ \d/u.test(row.label)) : undefined)
425
+ ?? null;
426
+ }
427
+ // 会話が今のmodelでcacheされている時だけ、sの後に出る確かめ(2.1.293・2.1.296)。
428
+ // Switch model?
429
+ // Your next response will be slower and use more tokens
430
+ // ❯ 1. Yes, switch to Opus 5.5
431
+ // 2. No, go back
432
+ export function claudeModelSwitchConfirm(screen) {
433
+ const lines = screen.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").split(/\r?\n/u);
434
+ const head = lastLineIndex(lines, /^\s*Switch model\?\s*$/u);
435
+ if (head < 0)
436
+ return null;
437
+ const body = lines.slice(head + 1);
438
+ if (!body.some((line) => /^\s*(?:❯\s*)?1\.\s*Yes, switch to\b/u.test(line)))
439
+ return null;
440
+ return body.some((line) => /^\s*❯\s*1\.\s*Yes, switch to\b/u.test(line)) ? "yes_selected" : "other_selected";
441
+ }
442
+ export function claudeModelSwitchResult(screen) {
443
+ const lines = screen.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").split(/\r?\n/u);
444
+ const command = lastLineIndex(lines, /^\s*❯\s*\/model\s*$/u);
445
+ if (command < 0)
446
+ return null;
447
+ const start = lines.findIndex((line, i) => i > command && /^\s*⎿\s+\S/u.test(line));
448
+ if (start < 0)
449
+ return null;
450
+ const parts = [lines[start].replace(/^\s*⎿\s+/u, "").trim()];
451
+ for (const line of lines.slice(start + 1)) {
452
+ if (!/^\s{3,}\S/u.test(line) || /^\s*⎿/u.test(line))
453
+ break;
454
+ parts.push(line.trim());
455
+ }
456
+ const line = parts.join(" ");
457
+ if (/\bblocked by a PreModelSwitch hook\b/u.test(line))
458
+ return { scope: "blocked", line };
459
+ if (/^Kept model as\b/u.test(line))
460
+ return { scope: "kept", line };
461
+ if (!/^(?:Set model to|Model set to)\b/u.test(line))
462
+ return null;
463
+ if (/\bsaved as your default\b/u.test(line))
464
+ return { scope: "saved_as_default", line };
465
+ return /\bfor this session only\b/u.test(line) ? { scope: "session_only", line } : null;
466
+ }
376
467
  // Claude Code 2.1.282 は選択肢へ「1.」などの番号を付ける。番号の有無のどちらも受ける。
377
468
  export function claudeLoginMethodMenu(screen) {
378
469
  const tail = screen.split("\n").slice(-32).join("\n");
@@ -246,7 +246,7 @@ export function grokInitializationComplete(meta) {
246
246
  }
247
247
  return false;
248
248
  }
249
- export async function observeGrokDone(meta, timeout, requestedCursor, detectRateLimit, signal) {
249
+ export async function observeGrokDone(meta, timeout, requestedCursor, detectRateLimit, signal, detectUnacceptedPrompt = () => false) {
250
250
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
251
251
  const transcript = grokEventsTranscript(meta);
252
252
  if (!transcript)
@@ -257,9 +257,11 @@ export async function observeGrokDone(meta, timeout, requestedCursor, detectRate
257
257
  let malformedEvents = 0;
258
258
  let discardLeadingFragment = false;
259
259
  let initializedBoundary = false;
260
+ let turnStarted = false;
261
+ let unacceptedSeen = 0;
260
262
  const deadline = performance.now() + timeout * 1000;
261
263
  const rateLimitDue = pollGate(AGENT_RATE_LIMIT_POLL_MS);
262
- const observation = (outcome, ev = null, rateLimit = null) => ({
264
+ const observation = (outcome, ev = null, rateLimit = null, error = null) => ({
263
265
  schema: "aiterm.agent-wait-result.v1",
264
266
  session_id: meta.aiterm_session,
265
267
  launch_id: meta.launch_id,
@@ -272,7 +274,7 @@ export async function observeGrokDone(meta, timeout, requestedCursor, detectRate
272
274
  malformed_events: malformedEvents,
273
275
  at: ev?.at ?? null,
274
276
  rate_limit: rateLimit,
275
- error: ev?.done_status === "turn_error" ? ev.reason : null,
277
+ error: error ?? (ev?.done_status === "turn_error" ? ev.reason : null),
276
278
  error_kind: null,
277
279
  });
278
280
  for (;;) {
@@ -311,7 +313,10 @@ export async function observeGrokDone(meta, timeout, requestedCursor, detectRate
311
313
  continue;
312
314
  }
313
315
  try {
314
- const done = grokCompletionEvent(meta, JSON.parse(line));
316
+ const record = JSON.parse(line);
317
+ if (record?.type === "turn_started")
318
+ turnStarted = true;
319
+ const done = grokCompletionEvent(meta, record);
315
320
  if (done) {
316
321
  const limited = done.done_status === "turn_error"
317
322
  ? detectRateLimit(meta.kind, meta.aiterm_session) : null;
@@ -329,6 +334,12 @@ export async function observeGrokDone(meta, timeout, requestedCursor, detectRate
329
334
  const limited = detectRateLimit(meta.kind, meta.aiterm_session);
330
335
  if (limited)
331
336
  return observation("rate_limited", null, limited);
337
+ // Grokが文を受け取らずに入力欄へ戻した時は、記録に何も出ないので、画面の知らせで終わりを知る。
338
+ // 送った直後は前の番の知らせがまだ画面に残っている事があるので、この待ちの間に番が始まっておらず、
339
+ // 間を置いた2回の読みが続けて同じ時だけ、結果なしの終了として返す(1回だけの照会では返さない)。
340
+ unacceptedSeen = !turnStarted && detectUnacceptedPrompt(meta.aiterm_session) ? unacceptedSeen + 1 : 0;
341
+ if (unacceptedSeen >= 2)
342
+ return observation("error", null, null, GROK_UNACCEPTED_PROMPT_ERROR);
332
343
  }
333
344
  if (performance.now() >= deadline)
334
345
  return observation(timeout === 0 ? "running" : "timeout");
@@ -418,6 +429,30 @@ export function grokTuiBusy(screen) {
418
429
  || screen.includes("Responding...")
419
430
  || screen.includes("[stop]");
420
431
  }
432
+ // Grok(1.0.50実測)は、送った文を中のagentが10秒受け取らないと、足元の表示を「Waiting for the agent to accept the prompt…」に変え、
433
+ // 120秒で自分から取り消して、文を入力欄へ戻す(「The agent did not accept your prompt within 120s. Your text is back in the input box; …」)。
434
+ // 会話の記録(events.jsonl)には、番の行が1つも出ない。
435
+ /** 動いている表示が「受け取り待ち」か。差し込む先の番は、まだ無い。 */
436
+ export function grokPromptAwaitingAccept(screen) {
437
+ const live = screen.split("\n").filter(line => /Waiting for|Responding|\[stop\]/u.test(line)).at(-1);
438
+ return live?.includes("Waiting for the agent to accept the prompt") ?? false;
439
+ }
440
+ /**
441
+ * 受け取られなかった文が、入力欄へ戻されたままか。
442
+ * 知らせは次の番が始まった後も画面の上に残るので、知らせより後に動いている表示が無く、今の入力欄に文がある時だけtrue。
443
+ */
444
+ export function grokUnacceptedPromptRestored(screen) {
445
+ const clean = screen.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
446
+ const at = clean.lastIndexOf("The agent did not accept your prompt within");
447
+ if (at < 0)
448
+ return false;
449
+ const after = clean.slice(at);
450
+ if (grokTuiBusy(after))
451
+ return false;
452
+ const composer = [...after.matchAll(/^[ \t]*│[ \t]*[❯>]([^\n]*?)│?[ \t]*$/gmu)].at(-1);
453
+ return !!composer && composer[1].trim() !== "";
454
+ }
455
+ export const GROK_UNACCEPTED_PROMPT_ERROR = "Grokが文を受け取りませんでした(Grokの画面の知らせ: The agent did not accept your prompt)。文は実行されず、Grokの入力欄へ戻されています。";
421
456
  // 現在の質問カードだけを認識する。選択肢が空でも操作欄は残る。
422
457
  // footer後に描画があれば過去のカードなので、会話履歴からキー操作を起こさない。
423
458
  export function grokRateLimitDialog(screen) {
package/dist/index.js CHANGED
@@ -618,6 +618,9 @@ registerRemoteAwareTool("pty_list", {
618
618
  session_id: z.string(), current_command: z.string(), attached: z.boolean(),
619
619
  width: z.number(), height: z.number(),
620
620
  harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]).nullable(),
621
+ // 0.58.3より前の現地(remote)は返さないので、無い事も受ける。
622
+ vendor_session_id: z.string().nullable().optional().describe("agent sessionのharness側の会話ID。Claude CodeとGrokは起動時に--session-idで渡した値(席の中で会話を移した後は追わない)、" +
623
+ "CodexとCursorは分かった後の値。通常PTYと、まだ分からない席はnull"),
621
624
  environment: z.record(z.string(), z.string().nullable()),
622
625
  })),
623
626
  },
@@ -833,7 +836,9 @@ registerRemoteAwareTool("agent_auth", {
833
836
  });
834
837
  registerRemoteAwareTool("agent_configure", {
835
838
  description: "起動済みのClaude/Codex/Grok/Cursor agent sessionを再起動せず、会話contextを保ったままmodel/reasoning effortを変更する。" +
836
- "各harnessのCLI標準model操作を使う。Cursorのreasoning_effort変更はmodelと同時指定する。",
839
+ "各harnessのCLI標準model操作を使う。Cursorのreasoning_effort変更はmodelと同時指定する。" +
840
+ "Claude Codeの席では /model の選ぶ画面の「この会話だけ」(s)で替え、端末の ~/.claude/settings.json の既定は書き換えない。" +
841
+ "その選び方が無い版、選ぶ画面に無いmodel、そのmodelで選べないeffortは、何も変えずに断る。",
837
842
  inputSchema: {
838
843
  session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
839
844
  model: z.string().min(1).nullish().describe("変更後のmodel。省略時はmodelを変更しない"),
@@ -42,6 +42,7 @@ export function psmuxVersionSupported(output) {
42
42
  return major > 3 || (major === 3 && (minor > 3 || (minor === 3 && patch >= 8)));
43
43
  }
44
44
  let winPsmuxOk = false;
45
+ let winPsmuxVersion = null;
45
46
  export function ensureWinPsmux(observe = true) {
46
47
  if (winPsmuxOk)
47
48
  return;
@@ -56,8 +57,124 @@ export function ensureWinPsmux(observe = true) {
56
57
  ptyDependencyError("psmux -V が失敗しました。`psmux -V` が通るか確認してください。", observe);
57
58
  if (!psmuxVersionSupported(r.stdout ?? ""))
58
59
  ptyDependencyError("psmux 3.3.8以上が必要です。winget upgrade --id marlocarlo.psmux --source winget で更新してください。", observe);
60
+ winPsmuxVersion = /^psmux (\d+\.\d+\.\d+)(?:\s|$)/mu.exec(r.stdout ?? "")?.[1] ?? null;
59
61
  winPsmuxOk = true;
60
62
  }
63
+ // ---- psmux 3.3.8: 画面の無いlogonで立てた本体の控えが、画面のある側から消される(psmux #650)----
64
+ // sshで入った側やserviceの下(Terminal Servicesのsession 0)で立てたpsmuxの本体を、同じ利用者の、画面のある側の
65
+ // 普段の権限のprocessは開けない(OpenProcessがERROR_ACCESS_DENIED)。psmux 3.3.8は「開けない=本体は終わっている」と
66
+ // 読み、画面のある側でpsmuxが1回呼ばれるたびに(`psmux -V`でも)、その端末の控え(~/.psmux の port・key・pid・sid)を消す。
67
+ // 本体が5秒以内に書き戻すまで端末は一覧から消え、その間にこちら側でpsmuxを呼ぶと、控えの無い本体として止められる
68
+ // (実被弾 2026-10-11 fox: sshの側で立てたGrokの席が、4回中2回、文を送った20秒ほど後に消えた)。
69
+ // 直しはpsmuxのmasterに入っているが、公開された版は3.3.8が最後。それまでの間、立てた本体へ
70
+ // 「同じ利用者が名前を問う権利(PROCESS_QUERY_LIMITED_INFORMATION)」だけを足す。画面のある側が本体を開けるようになり、控えは消されない。
71
+ const PSMUX_DESKTOP_ACCESS_VERSION = "3.3.8";
72
+ const PSMUX_SERVER_PID_WAIT_MS = 5500;
73
+ const PSMUX_REGISTRY_SETTLE_MS = 6000;
74
+ const PSMUX_DESKTOP_ACCESS_POLL_MS = 250;
75
+ let psmuxOnDesktop = false;
76
+ /** 手当てが要る見込みか。processを起こさずに決める。 */
77
+ export function psmuxDesktopAccessWanted(version, env = process.env) {
78
+ if (version !== PSMUX_DESKTOP_ACCESS_VERSION)
79
+ return false;
80
+ // SESSIONNAME(Console・RDP-Tcp#N)は画面のあるlogonの印。ある時は画面のある側で、手当ては要らない。
81
+ // 無いのは、ssh・serviceの下か、画面のある側のタスク。どちらかは、手当てのscriptがsessionの番号で見分ける。
82
+ return !env.SESSIONNAME;
83
+ }
84
+ /** 本体へ権利を足すPowerShellのscript。出力は1行: `ok`(足した)・`desktop`(画面のある側なので何もしない)・それ以外(失敗の理由)。 */
85
+ export function psmuxServerAccessScript(serverPid) {
86
+ return `$ErrorActionPreference = 'Stop'
87
+ if ([Diagnostics.Process]::GetCurrentProcess().SessionId -ne 0) { 'desktop'; exit 0 }
88
+ Add-Type -TypeDefinition @"
89
+ using System; using System.Runtime.InteropServices;
90
+ public static class AitermPsmuxAccess {
91
+ [DllImport("kernel32.dll", SetLastError=true)] static extern IntPtr OpenProcess(uint access, bool inherit, uint pid);
92
+ [DllImport("kernel32.dll")] static extern bool CloseHandle(IntPtr h);
93
+ [DllImport("kernel32.dll")] static extern IntPtr LocalFree(IntPtr p);
94
+ [DllImport("advapi32.dll")] static extern uint GetSecurityInfo(IntPtr handle, int objectType, uint info, IntPtr owner, IntPtr group, out IntPtr dacl, IntPtr sacl, out IntPtr sd);
95
+ [DllImport("advapi32.dll", CharSet=CharSet.Unicode)] static extern uint SetEntriesInAclW(uint count, ref EXPLICIT_ACCESS entries, IntPtr oldAcl, out IntPtr newAcl);
96
+ [DllImport("advapi32.dll")] static extern uint SetSecurityInfo(IntPtr handle, int objectType, uint info, IntPtr owner, IntPtr group, IntPtr dacl, IntPtr sacl);
97
+ [DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)] static extern bool ConvertStringSidToSidW(string sid, out IntPtr psid);
98
+ [StructLayout(LayoutKind.Sequential)] struct TRUSTEE { public IntPtr pMultipleTrustee; public int MultipleTrusteeOperation; public int TrusteeForm; public int TrusteeType; public IntPtr ptstrName; }
99
+ [StructLayout(LayoutKind.Sequential)] struct EXPLICIT_ACCESS { public uint grfAccessPermissions; public int grfAccessMode; public uint grfInheritance; public TRUSTEE Trustee; }
100
+ public static string Grant(uint pid, string sid, uint mask) {
101
+ IntPtr h = OpenProcess(0x00060000, false, pid); // READ_CONTROL | WRITE_DAC
102
+ if (h == IntPtr.Zero) return "OpenProcess " + Marshal.GetLastWin32Error();
103
+ try {
104
+ IntPtr dacl, sd, psid, newAcl;
105
+ uint r = GetSecurityInfo(h, 6, 4, IntPtr.Zero, IntPtr.Zero, out dacl, IntPtr.Zero, out sd); // SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION
106
+ if (r != 0) return "GetSecurityInfo " + r;
107
+ if (!ConvertStringSidToSidW(sid, out psid)) return "ConvertStringSidToSid " + Marshal.GetLastWin32Error();
108
+ EXPLICIT_ACCESS ea = new EXPLICIT_ACCESS();
109
+ ea.grfAccessPermissions = mask; ea.grfAccessMode = 1; ea.grfInheritance = 0; // GRANT_ACCESS
110
+ ea.Trustee.TrusteeForm = 0; ea.Trustee.TrusteeType = 1; ea.Trustee.ptstrName = psid; // TRUSTEE_IS_SID, TRUSTEE_IS_USER
111
+ r = SetEntriesInAclW(1, ref ea, dacl, out newAcl);
112
+ if (r != 0) return "SetEntriesInAcl " + r;
113
+ r = SetSecurityInfo(h, 6, 4, IntPtr.Zero, IntPtr.Zero, newAcl, IntPtr.Zero);
114
+ LocalFree(newAcl); LocalFree(psid); LocalFree(sd);
115
+ return r == 0 ? "ok" : "SetSecurityInfo " + r;
116
+ } finally { CloseHandle(h); }
117
+ }
118
+ }
119
+ "@
120
+ [AitermPsmuxAccess]::Grant(${serverPid}, [Security.Principal.WindowsIdentity]::GetCurrent().User.Value, 0x1000)
121
+ `;
122
+ }
123
+ function runPsmuxServerAccessScript(serverPid) {
124
+ const powershell = path.win32.join(process.env.SystemRoot ?? process.env.windir ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
125
+ const encoded = Buffer.from(psmuxServerAccessScript(serverPid), "utf16le").toString("base64");
126
+ const r = spawnSync(powershell, ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded], { encoding: "utf8", windowsHide: true, timeout: 15000 });
127
+ if (r.error)
128
+ return `powershell ${r.error.code ?? "error"}`;
129
+ return (r.stdout ?? "").trim().split(/\r?\n/).pop() || `powershell exit ${r.status}`;
130
+ }
131
+ /**
132
+ * 立てたばかりの端末の本体を、画面のある側から問えるようにする(上の説明)。端末を作った直後に1回呼ぶ。
133
+ * 失敗しても端末は使えるので、投げない。足せなかった時は、理由を1行だけstderrへ書く。
134
+ */
135
+ export function openPsmuxServerToDesktop(session, o = {}) {
136
+ if (o.wanted === undefined) {
137
+ if (!isWin || psmuxOnDesktop)
138
+ return "skipped";
139
+ ensureWinPsmux(false);
140
+ }
141
+ if (!(o.wanted ?? psmuxDesktopAccessWanted(winPsmuxVersion)))
142
+ return "skipped";
143
+ // 問うだけの命令なので、窓を隠して起こす。consoleを持たないprocessから呼ばれた時に、この手当てで窓を増やさない。
144
+ const tmux = o.tmux ?? ((...args) => {
145
+ const r = spawnSync(psmuxBin(), ["-L", WIN_NS, ...args], { encoding: "utf8", windowsHide: true, env: tmuxSpawnEnv(), timeout: 10000 });
146
+ return { code: r.status ?? 1, stdout: r.stdout ?? "" };
147
+ });
148
+ const pause = o.pause ?? ((ms) => { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); });
149
+ // 立てた直後に控えを消されていると、本体のpidを問えない。本体が控えを書き戻す(5秒以内)まで待つ。
150
+ // 立てて10秒以内の本体は、控えが無くても止められない。待ちは、足しても10秒より短くしてある。
151
+ let serverPid = 0;
152
+ for (let waited = 0;; waited += PSMUX_DESKTOP_ACCESS_POLL_MS) {
153
+ const r = tmux("display-message", "-p", "-t", session, "#{pid}");
154
+ serverPid = r.code === 0 && /^\d+$/.test(r.stdout.trim()) ? Number(r.stdout.trim()) : 0;
155
+ if (serverPid > 0 || waited >= PSMUX_SERVER_PID_WAIT_MS)
156
+ break;
157
+ pause(PSMUX_DESKTOP_ACCESS_POLL_MS);
158
+ }
159
+ const result = serverPid > 0 ? (o.grant ?? runPsmuxServerAccessScript)(serverPid) : "psmuxの本体のpidを問えません";
160
+ if (result === "desktop") {
161
+ // 画面のある側のprocessは、何度立てても同じ答えになる。以後は問わない。
162
+ if (o.wanted === undefined)
163
+ psmuxOnDesktop = true;
164
+ return "desktop";
165
+ }
166
+ if (result !== "ok") {
167
+ process.stderr.write(`aiterm: PSMUX_SERVER_ACCESS_NOT_GRANTED session=${session} ${result.slice(0, 200)}\n`);
168
+ return "failed";
169
+ }
170
+ // 足す前に消された控えが戻るのを待ってから、端末を使い始める。
171
+ for (let waited = 0; waited < PSMUX_REGISTRY_SETTLE_MS; waited += PSMUX_DESKTOP_ACCESS_POLL_MS) {
172
+ if (tmux("has-session", "-t", session).code === 0)
173
+ break;
174
+ pause(PSMUX_DESKTOP_ACCESS_POLL_MS);
175
+ }
176
+ return "granted";
177
+ }
61
178
  // tmux が見つからないときの説明。macOS は tmux を同梱せず、Homebrew の bin は GUI 起動時の PATH に
62
179
  // 入らないため、原因と対処(導入・自動探索・AITERM_TMUX 上書き)を正直に提示する。
63
180
  function tmuxMissingMessage() {
package/docs/DESIGN.md CHANGED
@@ -37,7 +37,8 @@ UTF-8のBase64を復元したscriptblockのdot-sourceを使う。変数と作業
37
37
  sentinelの方言は端末runtimeが実効shellから決める。SSH先の現在の標準PowerShell promptを
38
38
  検出した場合は、Aiterm hostのOSに関係なくPowerShell構文を使う。過去のpromptでは切り替えない。
39
39
 
40
- `pty_list`はtextと構造化したsession一覧を返す。`env_keys`は帰属等の非秘密キーの明示照会だけで、
40
+ `pty_list`はtextと構造化したsession一覧を返す。agent sessionには、harness側の会話ID(`vendor_session_id`。Claude CodeとGrokは
41
+ 起動時に`--session-id`で渡した値、CodexとCursorは掴んだ後の値)を載せる。`env_keys`は帰属等の非秘密キーの明示照会だけで、
41
42
  psmuxが出力した余分な環境値を返さない。通常PTYとagentへ`AITERM_SESSION_ID`を注入し、
42
43
  `env_vars`の継承とsessionへの登録もAitermが所有する。古いtmuxは子へのenv注入とsession登録を使う。
43
44
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.58.1",
3
+ "version": "0.58.3",
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": [