aiterm-mcp 0.58.0 → 0.58.2

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,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.58.2] - 2026-10-11
11
+
12
+ ### 修正
13
+
14
+ - Windowsで、sshの側(`remote`付きの呼び出しを含む)やserviceの下で立てた端末・席が、一覧から消えたり、途中で止められたりする件を直す(ADR 0109)。
15
+ psmux 3.3.8は、画面のある側でどの命令が呼ばれても、開けない本体の控えを「終わった物」として消す(psmux #650。直しは未公開)。
16
+ 画面の無いlogonで立てた本体は、同じ利用者の画面のある側から開けないので、ほかの席が動くたびに控えが消えていた。
17
+ その間、Aitermは`missing / session_missing`や「paneの活動観測を取得できません」を返し、席ごと止められる事もあった
18
+ (Windowsの実物で、sshの側で立てたGrokの席が4回中2回、文を送った20秒ほど後に消えた)。
19
+ - 端末を立てた直後に、本体のprocessへ「同じ利用者が名前を問う権利」(`PROCESS_QUERY_LIMITED_INFORMATION`)だけを足す。画面のある側が本体を開けるようになり、控えは消されない。
20
+ - 足すのは、psmuxが3.3.8で、画面のあるlogonの印(`SESSIONNAME`)が無い時だけ。アプリや端末から起こしたAitermでは、何も起こさない。
21
+ - sshの側で端末を1つ立てるたびに、約0.35秒かかる。足せなかった時も端末は使え、理由を1行だけstderrへ書く。
22
+ - 更新の前に立てた端末には効かない。
23
+ - Grokが文を受け取らなかった時に、待ち続けず、次の文と混ぜないようにする(ADR 0110)。
24
+ Grok(1.0.50)は、送った文を中のagentが受け取らないと、120秒で自分から取り消して、文を入力欄へ戻す。会話の記録には何も出ない
25
+ (Windowsでは、起こすstdioのMCPが8個以上の時に毎回こうなる。Grokの側の不具合で、これ自体はAitermでは直らない)。
26
+ - 完了待ちは、時間切れ(既定600秒)まで待たずに、結果なしの終了(`error`、exit 7)で返す。`error`の文に、Grokが受け取らなかった事を書く。
27
+ - 入力欄へ戻された文は、次の文を送る前に消す(返りの`pane_input_recovery`に`grok_unaccepted_prompt_cleared`)。今までは、前の文と次の文がつながって1通になっていた。
28
+ - 受け取り待ちの間(画面の「Waiting for the agent to accept the prompt」)に送った文は、差し込まずに、送らずに断る(`GROK_PROMPT_NOT_ACCEPTED_YET`)。
29
+ 今までは差し込みの道へ入り、文がGrokの待ち行列に残っていた(`STEER_STILL_QUEUED`、または`agent_steer`で返っていた)。
30
+
31
+ ## [0.58.1] - 2026-10-10
32
+
33
+ ### 修正
34
+
35
+ - Windowsで、Codexの親へ文を届けた後に、端末の窓(Windows Terminal)が一瞬出る事がある件を直す(共通パッケージ`aiterm-steer-delivery` 0.4.3)。
36
+ 0.58.0で足した「寝ている会話を起こす見張り」(ADR 0108)は、切り離したprocess(consoleを持たない)で動く。
37
+ CodexのSteerを有効にしていない端末では、見張りがCodexの実行ファイルを`where codex`で探し、その子に窓を隠す指定が無かった。
38
+ consoleを持たないprocessが、窓を隠さずにconsoleの子を起こすと、窓が一瞬出る。
39
+ - Steerが有効な端末(実行ファイルの場所がhookの設定にある)では、ここを通らない。その道では、0.58.0も窓を出していない(Windowsの実物で測った)。
40
+ - 共通パッケージが子のprocessを起こす所の全部に、窓を隠す指定を付けた。
41
+ - 共通パッケージを0.4.3へ上げる。0.4.2の直し(Claude Codeが起動し直して再開した会話へのchannelの配送)は、channelを使う製品向けで、Aitermの動きは変わらない。
42
+
10
43
  ## [0.58.0] - 2026-10-09
11
44
 
12
45
  ### 修正
@@ -2464,7 +2497,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2464
2497
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2465
2498
  provenance.
2466
2499
 
2467
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.0...HEAD
2500
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.2...HEAD
2501
+ [0.58.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.1...v0.58.2
2502
+ [0.58.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.0...v0.58.1
2468
2503
  [0.58.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.4...v0.58.0
2469
2504
  [0.57.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.3...v0.57.4
2470
2505
  [0.57.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.2...v0.57.3
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.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.58.2** · 動作対象は 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.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).
230
+ **Status:** actively maintained · current public release **v0.58.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).
231
231
 
232
232
  ### Update and rollback
233
233
 
package/dist/core.js CHANGED
@@ -18,10 +18,10 @@ 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
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";
@@ -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("=");
@@ -3126,7 +3128,14 @@ export async function observeAgentDone(name, o = {}) {
3126
3128
  return observeCursorDone(meta, timeout, o.cursor, (session) => captureScreen(session, 0), o.signal);
3127
3129
  }
3128
3130
  if (meta.kind === "grok" && meta.completion_route === "grok_transcript") {
3129
- return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit, o.signal);
3131
+ // 上限の知らせと、受け取られなかった文の知らせは、同じ見回りで読んだ1枚の画面から見る(画面を読むたびにtmuxを起動する)。
3132
+ let read = { at: -Infinity, screen: "" };
3133
+ const screen = (session) => {
3134
+ if (performance.now() - read.at > AGENT_RATE_LIMIT_POLL_MS / 4)
3135
+ read = { at: performance.now(), screen: captureScreen(session, 0) };
3136
+ return read.screen;
3137
+ };
3138
+ return observeGrokDone(meta, timeout, o.cursor, (_kind, session) => grokRateLimitDialog(screen(session))?.message ?? null, o.signal, session => grokUnacceptedPromptRestored(screen(session)));
3130
3139
  }
3131
3140
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
3132
3141
  // 境界の優先順: dispatch receipt の event_cursor(起動順序に依存しない)→ operation相関
@@ -4354,6 +4363,20 @@ async function dispatchAgentTurnInLock(name, text, o) {
4354
4363
  }
4355
4364
  sendKey(name, limitDialog.dismissKey);
4356
4365
  }
4366
+ // Grokが受け取らずに入力欄へ戻した文が残っていると、次の文とつながって1通になる。送る前に消す。
4367
+ // 入力欄に文がある時のC-cは、入力欄を空にするだけ(Grok 1.0.46・1.0.50実測)。空の時に押すと終了の構えになるので、文がある時だけ押す。
4368
+ let grokRestoredPromptCleared = false;
4369
+ if (meta.kind === "grok" && !limitDialog && grokUnacceptedPromptRestored(captureScreen(name, 0))) {
4370
+ sendKey(name, "C-c");
4371
+ for (let i = 0; i < 20 && !grokRestoredPromptCleared; i++) {
4372
+ await sleep(100);
4373
+ grokRestoredPromptCleared = !grokUnacceptedPromptRestored(captureScreen(name, 0));
4374
+ }
4375
+ if (!grokRestoredPromptCleared) {
4376
+ throw new AitermError(`GROK_UNACCEPTED_PROMPT_NOT_CLEARED vendor=grok session=${name}\n` +
4377
+ "Grokが受け取らずに入力欄へ戻した前の文を、消せませんでした。文字列は送信していません。pty_read(screen:true)で入力欄を確かめてください。", 2);
4378
+ }
4379
+ }
4357
4380
  if (meta.kind !== "claude" || claudeColdStart) {
4358
4381
  // 上限のpanelを閉じた直後(Grok)は、画面が落ち着くのを11回の確かめで待つ。
4359
4382
  const recovery = await waitAgentTuiReadyAfterCodexRateLimitRecovery(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS, { settledTurn: !limitDialog });
@@ -4380,6 +4403,8 @@ async function dispatchAgentTurnInLock(name, text, o) {
4380
4403
  paneInputRecovery.push("codex_rate_limit_model_switch_kept_current");
4381
4404
  if (limitDialog)
4382
4405
  paneInputRecovery.push("grok_rate_limit_dialog_dismissed");
4406
+ if (grokRestoredPromptCleared)
4407
+ paneInputRecovery.push("grok_unaccepted_prompt_cleared");
4383
4408
  const startOffset = agentCompletionCursor(meta);
4384
4409
  // promptなしで起動したCursorは、最初のdispatch時点ではtranscriptとの相関markerをまだ持たない。
4385
4410
  // その1回だけlaunch contextを加え、以後はbind済みconversationへ通常textだけを送る。
@@ -4539,6 +4564,12 @@ async function sendAgentMessageInLock(name, text, o) {
4539
4564
  // Codexは回答を流している間、画面に動作中の行を出さない。その間に届いた文を、Codexは動いているturnへ取り込む(ADR 0101)。
4540
4565
  if (!running && meta.kind === "codex" && await codexTurnRunningBehindIdleScreen(name, meta, screen))
4541
4566
  running = true;
4567
+ // Grokが前の文をまだ受け取っていない間は、差し込む先の番が無い。待ち行列へ入れても動かず、Grokが前の文を取り消した後も残る。
4568
+ if (running && meta.kind === "grok" && grokPromptAwaitingAccept(screen)) {
4569
+ throw new AitermError(`GROK_PROMPT_NOT_ACCEPTED_YET vendor=grok session=${name}\n` +
4570
+ "Grokが前の文をまだ受け取っていません(画面の「Waiting for the agent to accept the prompt」)。文字列は送信していません。" +
4571
+ "前の文の完了待ちが終わってから送り直してください。Grokは120秒受け取れないと、前の文を自分から取り消します。", 2);
4572
+ }
4542
4573
  if (running) {
4543
4574
  return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery, preface: o.preface });
4544
4575
  }
@@ -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) {
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.58.0",
3
+ "version": "0.58.2",
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": [
@@ -72,7 +72,7 @@
72
72
  },
73
73
  "dependencies": {
74
74
  "@modelcontextprotocol/sdk": "^1.29.0",
75
- "aiterm-steer-delivery": "^0.4.1",
75
+ "aiterm-steer-delivery": "^0.4.3",
76
76
  "ws": "8.21.3",
77
77
  "zod": "^4.4.3"
78
78
  },