aiterm-mcp 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.31.0] - 2026-09-04
11
+
12
+ ### Fixed
13
+
14
+ - 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`にエラー本文)で返す。
15
+ - Grokの`turn_ended outcome=error`を完了境界として扱わず待ち続けていた。同じくoutcome `error`で返す。Cursorは`turn_ended`のstatusを問わず既に終了を返していたため変更なし。Codexはエラー終了の記録形が実測できておらず未対応。
16
+
10
17
  ## [0.30.0] - 2026-09-04
11
18
 
12
19
  ### Added
@@ -1546,7 +1553,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
1546
1553
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
1547
1554
  provenance.
1548
1555
 
1549
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.30.0...HEAD
1556
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.31.0...HEAD
1557
+ [0.31.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.30.0...v0.31.0
1550
1558
  [0.30.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.31...v0.30.0
1551
1559
  [0.29.31]: https://github.com/kitepon/aiterm-mcp/compare/v0.29.30...v0.29.31
1552
1560
  [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.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
157
157
 
158
158
  ### 更新と巻き戻し
159
159
 
@@ -201,7 +201,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
201
201
  → { session_id: "codex1", … } # Codex が永続端末で稼働開始
202
202
  pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
203
203
  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)
204
+ $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeout(未完了) / 4=closed / 7=error(APIエラー等でturn打ち切り)。回収は pty_read(agent_transcript:true)
205
205
  → 操舵し、Codex の次の入力境界で返る
206
206
  ```
207
207
 
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.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).
173
173
 
174
174
  ### Update and rollback
175
175
 
@@ -226,7 +226,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
226
226
  pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
227
227
  pty_send("codex1", "also fix the imports it broke")
228
228
  → 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
229
+ $ 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
230
  pty_read("codex1", { agent_transcript: true }) → collect the full answer
231
231
  ```
232
232
 
@@ -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
@@ -18,7 +18,7 @@ import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, past
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
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";
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
  {
@@ -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++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.30.0",
3
+ "version": "0.31.0",
4
4
  "mcpName": "io.github.kitepon/aiterm-mcp",
5
5
  "description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
6
6
  "keywords": [