aiterm-mcp 0.30.0 → 0.31.1

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,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.31.1] - 2026-09-06
11
+
12
+ ### Fixed
13
+
14
+ - Grok/Composerがread-only sandboxの適用に失敗して終了した時、入力受付を待ち続けて再送を案内していた。初回promptと通常dispatchの入力受付確認で`GROK_SANDBOX_STARTUP_FAILED`を即時に返し、CLIの原因と未送信を明示する。設定とsandboxは変更しない。
15
+
16
+ ## [0.31.0] - 2026-09-04
17
+
18
+ ### Fixed
19
+
20
+ - Claude Codeのturnが529 Overloaded等のAPIエラーで打ち切られるとStop hookが走らず、`aiterm-wait`が永久に未完了を返し続けていた(実被弾 2026-09-03: BellTeamのClaude席で70分の待機)。会話記録(`<config dir>/projects/<cwd slug>/<session-id>.jsonl`)に観測開始後に増えた`isApiErrorMessage:true`の行を読み、新しいoutcome `error`(exit 7、`error`にエラー本文)で返す。
21
+ - Grokの`turn_ended outcome=error`を完了境界として扱わず待ち続けていた。同じくoutcome `error`で返す。Cursorは`turn_ended`のstatusを問わず既に終了を返していたため変更なし。Codexはエラー終了の記録形が実測できておらず未対応。
22
+
10
23
  ## [0.30.0] - 2026-09-04
11
24
 
12
25
  ### Added
@@ -1546,7 +1559,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1546
1559
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1547
1560
  provenance.
1548
1561
 
1549
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.30.0...HEAD
1562
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.31.1...HEAD
1563
+ [0.31.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.31.0...v0.31.1
1564
+ [0.31.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.30.0...v0.31.0
1550
1565
  [0.30.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.31...v0.30.0
1551
1566
  [0.29.31]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.30...v0.29.31
1552
1567
  [0.29.30]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.29...v0.29.30
package/README.ja.md CHANGED
@@ -153,7 +153,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
153
153
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
154
154
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
155
155
 
156
- **状態:** 開発継続中 · 現行公開版 **v0.30.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
156
+ **状態:** 開発継続中 · 現行公開版 **v0.31.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
157
157
 
158
158
  ### 更新と巻き戻し
159
159
 
@@ -193,6 +193,8 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
193
193
 
194
194
  `agent_launch`は任意の`write_scope`も受ける。Codex/Grokのread-onlyは`--sandbox read-only`、Cursorは公式`--mode ask`で実効化する。path説明は同等CLI引数がないためdeclaration-only。
195
195
 
196
+ Grok/Composerがread-only sandboxの適用を拒否した場合、prompt送信時に`GROK_SANDBOX_STARTUP_FAILED`とCLIの原因を返す。例えばhookのパスにシンボリックリンクがあるとGrok CLIは起動を拒否する。設定の管理元で原因を修正し、対象sessionを`pty_close`して起動し直す。Aitermはsandboxを解除したりhookをコピーしたりしない。
197
+
196
198
  ```text
197
199
  agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
198
200
  prompt: "port test/legacy.py to vitest",
@@ -201,7 +203,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
201
203
  → { session_id: "codex1", … } # Codex が永続端末で稼働開始
202
204
  pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
203
205
  pty_send("codex1", "also fix the imports it broke") # 非ブロックdispatch=event_cursor入りreceipt
204
- $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeout(未完了) / 4=closed。回収は pty_read(agent_transcript:true)
206
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeout(未完了) / 4=closed / 7=error(APIエラー等でturn打ち切り)。回収は pty_read(agent_transcript:true)
205
207
  → 操舵し、Codex の次の入力境界で返る
206
208
  ```
207
209
 
package/README.md CHANGED
@@ -169,7 +169,7 @@ collection is off by default and performs no network I/O. It ships via
169
169
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
170
170
  Release re-registers the Official MCP Registry entry.
171
171
 
172
- **Status:** actively maintained · current public release **v0.30.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).
172
+ **Status:** actively maintained · current public release **v0.31.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).
173
173
 
174
174
  ### Update and rollback
175
175
 
@@ -215,6 +215,8 @@ The human-readable launch text is accompanied by an `aiterm.agent-launch-result.
215
215
 
216
216
  `agent_launch` accepts an optional `write_scope`: either `"read-only"` or a human-readable description of writable paths. Codex/Grok use `--sandbox read-only`; Cursor uses its official read-only `--mode ask`. A path description remains declaration-only because these CLI launch surfaces provide no equivalent path allowlist flag.
217
217
 
218
+ Grok/Composerがread-only sandboxの適用を拒否した場合、prompt送信時に`GROK_SANDBOX_STARTUP_FAILED`とCLIの原因を返す。hookパスのシンボリックリンクなど、CLIが示した原因を設定の管理元で修正し、対象sessionを`pty_close`して起動し直す。Aitermはsandboxを解除したりhookをコピーしたりしない。
219
+
218
220
  For a correlated Claude turn stopped at `Do you want to proceed?`, use `claude_approval(action: "inspect", ...)` to capture the active operation and SHA-256 screen digest, review the displayed command, then call `respond` with that exact digest and either `approve_once` or `deny`. The relay rechecks the operation and screen under the send lock, never exposes arbitrary input or permanent approval, keeps the active marker intact, and records a prompt-free owner-only receipt. `pty_send(force: true)` does not bypass this boundary.
219
221
 
220
222
  ```text
@@ -226,7 +228,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
226
228
  pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
227
229
  pty_send("codex1", "also fix the imports it broke")
228
230
  → non-blocking dispatch; receipt carries event_cursor
229
- $ aiterm-wait --session codex1 --cursor <event_cursor> # never in the parent's foreground; exit 0=done, 3=timeout (not done), 4=closed
231
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # never in the parent's foreground; exit 0=done, 3=timeout (not done), 4=closed, 7=error (turn aborted by an API error)
230
232
  pty_read("codex1", { agent_transcript: true }) → collect the full answer
231
233
  ```
232
234
 
@@ -2,7 +2,7 @@
2
2
  // aiterm-wait — agent turn 完了eventの純リーダー観測CLI。
3
3
  // 完了/timeout/close を1行のJSON receiptで返してexitする。lock・PTY・dispatch状態には一切触れない。
4
4
  // 親AIホストのバックグラウンドタスクとして起動し、exitを「観測終了の通知」として使う。
5
- // exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 6=rate_limited=harness利用上限 / 1=エラー)。
5
+ // exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 6=rate_limited=harness利用上限 / 7=error=turnがエラー終了 / 1=waiter自身のエラー)。
6
6
  // --timeout 0 は待たずに一度だけ観測する照会で、未完了は running(timeout と混同させない)。
7
7
  // receipt の outcome が正で、done 以外は未完了。timeout の既定は core の DEFAULT_AGENT_DONE_TIMEOUT(600秒)。
8
8
  import { fileURLToPath } from "node:url";
@@ -75,6 +75,7 @@ const OUTCOME_EXIT_CODES = {
75
75
  timeout: 3,
76
76
  closed: 4,
77
77
  rate_limited: 6,
78
+ error: 7,
78
79
  };
79
80
  export async function main(argv) {
80
81
  const cmd = parseArgs(argv);
package/dist/core.js CHANGED
@@ -16,9 +16,9 @@ import * as rtk from "./rtk.js";
16
16
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
17
17
  import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, settlePaneLog, paneCwdArgument, } from "./tmux-runtime.js";
18
18
  import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
19
- import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
19
+ import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
20
20
  import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexLaunchBlockingDialog, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
21
- import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, } from "./harnesses/claude.js";
21
+ import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, } from "./harnesses/claude.js";
22
22
  import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
23
23
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
24
24
  export { AitermError } from "./errors.js";
@@ -2251,7 +2251,7 @@ function assertInitialPromptNotPendingForSend(name, force) {
2251
2251
  `${agentWaitGuide(name)}完了後に再度 pty_send するか、手動介入が必要な場合だけ force:true を明示してください。`, 2);
2252
2252
  }
2253
2253
  // aiterm-wait の exit 契約(CLI と各所の案内文で共有する正)。exit≠完了: outcome が done の時だけ完了。
2254
- export const AITERM_WAIT_OUTCOME_NOTE = `exit 0=done / 3=timeout(既定${DEFAULT_AGENT_DONE_TIMEOUT}秒・未完了) / 4=closed。receiptのoutcomeが正で、done以外は未完了`;
2254
+ export const AITERM_WAIT_OUTCOME_NOTE = `exit 0=done / 3=timeout(既定${DEFAULT_AGENT_DONE_TIMEOUT}秒・未完了) / 4=closed / 7=error(harnessの記録でturnがAPIエラー等で打ち切られた。結果は無い)。receiptのoutcomeが正で、done以外は未完了`;
2255
2255
  function quoteWindowsProcessArgument(value) {
2256
2256
  if (value !== "" && !/[\s"]/u.test(value))
2257
2257
  return value;
@@ -2393,7 +2393,12 @@ export async function observeAgentDone(name, o = {}) {
2393
2393
  let cursor = startOffset;
2394
2394
  let carry = "";
2395
2395
  let malformedEvents = 0;
2396
- const observation = (outcome, ev = null, rateLimit = null) => ({
2396
+ // Claudeの会話記録はこの観測開始時点の末尾から先だけを読む。過去turnのAPIエラー行を今回の
2397
+ // 終了と誤認しないため。dispatch直後に待機を始める親(receiptのwait_process)を前提にする。
2398
+ const transcriptFile = claudeSessionTranscriptPath(meta);
2399
+ let transcriptCursor = transcriptFile ? safeStatSize(transcriptFile) : 0;
2400
+ let transcriptCarry = "";
2401
+ const observation = (outcome, ev = null, rateLimit = null, apiError = null) => ({
2397
2402
  schema: "aiterm.agent-wait-result.v1",
2398
2403
  session_id: meta.aiterm_session,
2399
2404
  launch_id: meta.launch_id,
@@ -2404,8 +2409,9 @@ export async function observeAgentDone(name, o = {}) {
2404
2409
  vendor_session_id: ev?.vendor_session_id ?? meta.vendor_session_id ?? null,
2405
2410
  turn_id: ev?.turn_id ?? null,
2406
2411
  malformed_events: malformedEvents,
2407
- at: ev?.at ?? null,
2412
+ at: ev?.at ?? apiError?.at ?? null,
2408
2413
  rate_limit: rateLimit,
2414
+ error: apiError?.text ?? null,
2409
2415
  });
2410
2416
  for (;;) {
2411
2417
  if (!fs.existsSync(metadataFile))
@@ -2431,6 +2437,24 @@ export async function observeAgentDone(name, o = {}) {
2431
2437
  if (scanned.event)
2432
2438
  return observation("done", scanned.event);
2433
2439
  }
2440
+ if (transcriptFile && fs.existsSync(transcriptFile)) {
2441
+ const transcriptSize = safeStatSize(transcriptFile);
2442
+ if (transcriptSize < transcriptCursor) {
2443
+ transcriptCursor = 0;
2444
+ transcriptCarry = "";
2445
+ }
2446
+ if (transcriptSize > transcriptCursor) {
2447
+ transcriptCarry += readFileRange(transcriptFile, transcriptCursor, transcriptSize).toString("utf8");
2448
+ transcriptCursor = transcriptSize;
2449
+ const lines = transcriptCarry.split("\n");
2450
+ transcriptCarry = lines.pop() ?? "";
2451
+ for (const line of lines) {
2452
+ const apiError = claudeApiErrorFromLine(line);
2453
+ if (apiError)
2454
+ return observation("error", null, null, apiError);
2455
+ }
2456
+ }
2457
+ }
2434
2458
  // timeout=0 は「待たずに一度だけ見る」照会=未完了は失敗ではなく running。
2435
2459
  // 1秒以上を指定した待機の未完了は従来どおり timeout で、待ち方の意味は変えない。
2436
2460
  {
@@ -2518,6 +2542,8 @@ async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
2518
2542
  for (;;) {
2519
2543
  lastScreen = sample();
2520
2544
  samples++;
2545
+ if (kind === "grok" || kind === "composer")
2546
+ assertGrokSandboxNotRejected(lastScreen);
2521
2547
  if (isAgentTuiIdleReady(kind, lastScreen)) {
2522
2548
  readyStreak++;
2523
2549
  if (readyStreak >= stableSamples)
@@ -2,6 +2,7 @@
2
2
  // core 所有のサービス(transcript 不在エラー)は引数で注入し、
3
3
  // 依存方向を core → harnesses → agent-shared の一方向に保つ。
4
4
  import * as fs from "node:fs";
5
+ import * as os from "node:os";
5
6
  import * as path from "node:path";
6
7
  import { createHash, randomBytes, randomUUID } from "node:crypto";
7
8
  import { fileURLToPath } from "node:url";
@@ -216,3 +217,45 @@ export function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperat
216
217
  writeAgentMetadata(meta);
217
218
  return meta;
218
219
  }
220
+ // ---------------------------------------------------------------- APIエラー終了の検知(会話記録)
221
+ // Claude CodeはAPIエラー(529 Overloaded等)でturnを打ち切る時、Stop hookを走らせない。
222
+ // 代わりに会話記録(<config dir>/projects/<cwd slug>/<session-id>.jsonl)へ
223
+ // `type:"assistant", isApiErrorMessage:true, apiErrorStatus:<code>` の1行を書く(実測 2026-09-03、
224
+ // BellTeamのチャイム席で70分の待機を生んだ529)。完了eventだけを待つと永久に running になるため、
225
+ // dispatch後に増えた記録行からこの印を読み、outcome=error として親へ返す。
226
+ export function claudeConfigDir() {
227
+ return process.env.CLAUDE_CONFIG_DIR ?? path.join(process.env.HOME ?? os.homedir(), ".claude");
228
+ }
229
+ // Claude Codeのproject slug: cwdの英数字以外を1文字ずつ "-" にする(実測: "/Users/kite/.throughline-x" → "-Users-kite--throughline-x")。
230
+ export function claudeProjectSlug(cwd) {
231
+ return cwd.replace(/[^a-zA-Z0-9]/g, "-");
232
+ }
233
+ export function claudeSessionTranscriptPath(meta) {
234
+ if (meta.kind !== "claude" || !meta.vendor_session_id)
235
+ return null;
236
+ return path.join(claudeConfigDir(), "projects", claudeProjectSlug(meta.cwd ?? process.cwd()), `${meta.vendor_session_id}.jsonl`);
237
+ }
238
+ export function claudeApiErrorFromLine(line) {
239
+ if (!line.trim())
240
+ return null;
241
+ let record;
242
+ try {
243
+ record = JSON.parse(line);
244
+ }
245
+ catch {
246
+ return null;
247
+ }
248
+ if (record?.type !== "assistant" || record?.isApiErrorMessage !== true)
249
+ return null;
250
+ const content = record?.message?.content;
251
+ const text = typeof content === "string"
252
+ ? content
253
+ : Array.isArray(content)
254
+ ? content.map((part) => (typeof part?.text === "string" ? part.text : "")).join("")
255
+ : "";
256
+ const status = record?.apiErrorStatus;
257
+ return {
258
+ text: text.trim() || (status != null ? `API Error: ${String(status)}` : "API Error"),
259
+ at: typeof record?.timestamp === "string" ? record.timestamp : null,
260
+ };
261
+ }
@@ -272,6 +272,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, detectRat
272
272
  malformed_events: malformedEvents,
273
273
  at: ev?.at ?? null,
274
274
  rate_limit: rateLimit,
275
+ error: null,
275
276
  });
276
277
  for (;;) {
277
278
  if (!fs.existsSync(metadataFile))
@@ -202,6 +202,7 @@ export async function observeCursorDone(meta, timeout, requestedCursor, detectRa
202
202
  malformed_events: malformedEvents,
203
203
  at: ev?.at ?? null,
204
204
  rate_limit: rateLimit,
205
+ error: null,
205
206
  });
206
207
  for (;;) {
207
208
  if (!fs.existsSync(metadataFile))
@@ -81,7 +81,7 @@ export function grokEventsTranscript(meta) {
81
81
  export function grokCompletionEvent(meta, record) {
82
82
  if ((meta.kind !== "grok" && meta.kind !== "composer") ||
83
83
  record?.type !== "turn_ended" ||
84
- (record?.outcome !== "completed" && record?.outcome !== "cancelled"))
84
+ (record?.outcome !== "completed" && record?.outcome !== "cancelled" && record?.outcome !== "error"))
85
85
  return null;
86
86
  const turnId = typeof record?.ts === "string" || typeof record?.ts === "number" ? String(record.ts) : null;
87
87
  return {
@@ -93,7 +93,9 @@ export function grokCompletionEvent(meta, record) {
93
93
  turn_id: turnId,
94
94
  operation_id: null,
95
95
  reason: `Grok transcript turn_ended:${record.outcome}`,
96
- done_status: "turn_done",
96
+ // Grok CLIはAPIエラー等でturnを打ち切る時も turn_ended を書き、outcome だけが error になる
97
+ // (実測: 手元の events.jsonl に outcome=error が17件)。完了と同じ境界だが結果は無い。
98
+ done_status: record.outcome === "error" ? "turn_error" : "turn_done",
97
99
  stop_hook_active: false,
98
100
  at: typeof record?.ts === "string" ? record.ts : new Date().toISOString(),
99
101
  };
@@ -166,6 +168,7 @@ export async function observeGrokDone(meta, timeout, requestedCursor, detectRate
166
168
  malformed_events: malformedEvents,
167
169
  at: ev?.at ?? null,
168
170
  rate_limit: rateLimit,
171
+ error: ev?.done_status === "turn_error" ? ev.reason : null,
169
172
  });
170
173
  for (;;) {
171
174
  if (!fs.existsSync(metadataFile))
@@ -204,7 +207,7 @@ export async function observeGrokDone(meta, timeout, requestedCursor, detectRate
204
207
  try {
205
208
  const done = grokCompletionEvent(meta, JSON.parse(line));
206
209
  if (done)
207
- return observation("done", done);
210
+ return observation(done.done_status === "turn_error" ? "error" : "done", done);
208
211
  }
209
212
  catch {
210
213
  malformedEvents++;
@@ -259,6 +262,17 @@ export function grokEnvTokens(meta) {
259
262
  "GROK_DISABLE_AUTOUPDATER=1",
260
263
  ];
261
264
  }
265
+ export function assertGrokSandboxNotRejected(screen) {
266
+ // Grok 1.0.13はsandboxを適用できないと入力欄を作らず終了する。
267
+ // CLIが確定した拒否だけを拾い、警告単独や入力欄の引用では判定しない。
268
+ const failure = screen.match(/^[ \t]*error: could not apply the 'read-only' sandbox profile;[^\n]*/m);
269
+ if (!failure)
270
+ return;
271
+ const warning = screen.match(/^[ \t]*warning: sandbox could not be applied:[\s\S]*?(?=\n[ \t]*error:)/m);
272
+ throw new AitermError("GROK_SANDBOX_STARTUP_FAILED: Grok CLIがread-only sandboxを適用できず起動を拒否しました。文字列は送信していません。\n" +
273
+ `${warning?.[0] ?? ""}\n${failure[0]}\n` +
274
+ "CLIが示した設定の問題を、その設定の管理元で修正してください。修正後はpty_closeで対象sessionを閉じ、agent_launchで起動し直してください。", 2);
275
+ }
262
276
  export function grokTuiReady(screen) {
263
277
  // Grok Build 0.2.117 は起動完了後に製品名を消し、model footerだけを残す。
264
278
  // Composerも同じfrontendでmodel名だけが異なるため、両方をharness UIの根拠にする。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.30.0",
3
+ "version": "0.31.1",
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": [