aiterm-mcp 0.27.3 → 0.27.6

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.
@@ -2,12 +2,12 @@
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 / 1=エラー)。
5
+ // exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 6=rate_limited=vendor利用上限 / 1=エラー)。
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";
9
9
  import * as fs from "node:fs";
10
- import { AitermError, observeAgentDone } from "./core.js";
10
+ import { AitermError, detectAgentRateLimit, observeAgentDone } from "./core.js";
11
11
  const SESSION_RE = /^[A-Za-z0-9_-]{1,64}$/;
12
12
  const OPERATION_RE = /^sha256:[0-9a-f]{64}$/;
13
13
  const USAGE = "usage: aiterm-wait --session <name> [--cursor <event_cursor>] [--operation sha256:<64hex>] [--timeout <sec>]" +
@@ -74,14 +74,39 @@ const OUTCOME_EXIT_CODES = {
74
74
  running: 5,
75
75
  timeout: 3,
76
76
  closed: 4,
77
+ rate_limited: 6,
77
78
  };
78
79
  export async function main(argv) {
79
80
  const cmd = parseArgs(argv);
80
- const result = await observeAgentDone(cmd.session, {
81
- operation_id: cmd.operationId,
82
- timeout: cmd.timeout,
83
- cursor: cmd.cursor,
84
- });
81
+ let result;
82
+ try {
83
+ result = await observeAgentDone(cmd.session, {
84
+ operation_id: cmd.operationId,
85
+ timeout: cmd.timeout,
86
+ cursor: cmd.cursor,
87
+ });
88
+ }
89
+ catch (e) {
90
+ // Grok は利用上限に達すると auth 正本を自壊させることがあり(2026-08-22 実測)、
91
+ // metadata 検証が「未ログイン」という嘘の原因で落ちる。pane log に上限バナーが
92
+ // あるなら、誤診でなく rate_limited として typed に回答する。
93
+ if (e instanceof AitermError && /Grok 認証正本/.test(e.message)) {
94
+ const limited = detectAgentRateLimit("grok", cmd.session);
95
+ if (limited) {
96
+ emit({
97
+ ok: false,
98
+ code: "AGENT_RATE_LIMITED",
99
+ vendor: "grok",
100
+ rate_limit: limited,
101
+ message: "Grok が利用上限に達している。別のベンダーのモデルに切り替えてください。" +
102
+ "(認証正本の不在は上限時に CLI が auth を自壊させた痕跡で、未ログインの誤診に注意)",
103
+ });
104
+ process.exitCode = 6;
105
+ return;
106
+ }
107
+ }
108
+ throw e;
109
+ }
85
110
  emit(result);
86
111
  process.exitCode = OUTCOME_EXIT_CODES[result.outcome];
87
112
  }
package/dist/core.js CHANGED
@@ -2998,6 +2998,48 @@ export function agentWaitGuide(session) {
2998
2998
  const cmd = `aiterm-wait --session ${session ?? "<session_id>"} --cursor 0`;
2999
2999
  return `完了通知は ${agentWaitLaunchForm(cmd)} で受ける(親はここで待たない・polling 不要)。receipt の outcome=done を確認してから再取得する。`;
3000
3000
  }
3001
+ // vendor 別の利用上限バナー。検知は「報告」専用で、完了判定や自動復旧には使わない。
3002
+ // 出典(2026-08-22): grok は live 実バナーで検証、codex/claude はインストール済み実バイナリの
3003
+ // 埋込文字列から抽出(codex: "You've hit your usage limit for" / claude: "Usage limit reached ·
3004
+ // continuing automatically when it resets"。Claude Code はリセット時に自動継続する設計なので、
3005
+ // この報告は「今は上限で止まっている」の観測であり恒久停止を意味しない)。
3006
+ const AGENT_RATE_LIMIT_PATTERNS = {
3007
+ grok: [/You hit your weekly limit/i, /Weekly limit left:\s*0%/i],
3008
+ composer: [/You hit your weekly limit/i, /Weekly limit left:\s*0%/i],
3009
+ codex: [/You'?ve hit your usage limit/i],
3010
+ claude: [/Usage limit reached/i],
3011
+ };
3012
+ const AGENT_RATE_LIMIT_SCAN_BYTES = 16 * 1024;
3013
+ // pane log の末尾から上限バナーを探す。読めない・無い・対象 vendor でないは全て null(誤検知より取りこぼし側へ倒す)。
3014
+ export function detectAgentRateLimit(kind, aitermSession) {
3015
+ const patterns = AGENT_RATE_LIMIT_PATTERNS[kind];
3016
+ if (!patterns)
3017
+ return null;
3018
+ const file = logpath(aitermSession);
3019
+ let size;
3020
+ try {
3021
+ size = fs.statSync(file).size;
3022
+ }
3023
+ catch {
3024
+ return null;
3025
+ }
3026
+ if (size === 0)
3027
+ return null;
3028
+ let text;
3029
+ try {
3030
+ text = readFileRange(file, Math.max(0, size - AGENT_RATE_LIMIT_SCAN_BYTES), size).toString("utf8");
3031
+ }
3032
+ catch {
3033
+ return null;
3034
+ }
3035
+ const clean = text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
3036
+ for (const re of patterns) {
3037
+ const match = clean.match(re);
3038
+ if (match)
3039
+ return match[0];
3040
+ }
3041
+ return null;
3042
+ }
3001
3043
  async function observeCodexDone(meta, timeout, requestedCursor) {
3002
3044
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
3003
3045
  let transcript = codexRootTranscript(meta);
@@ -3008,7 +3050,7 @@ async function observeCodexDone(meta, timeout, requestedCursor) {
3008
3050
  let discardLeadingFragment = false;
3009
3051
  let initializedBoundary = false;
3010
3052
  const deadline = performance.now() + timeout * 1000;
3011
- const observation = (outcome, ev = null) => ({
3053
+ const observation = (outcome, ev = null, rateLimit = null) => ({
3012
3054
  schema: "aiterm.agent-wait-result.v1",
3013
3055
  session_id: meta.aiterm_session,
3014
3056
  launch_id: meta.launch_id,
@@ -3019,6 +3061,7 @@ async function observeCodexDone(meta, timeout, requestedCursor) {
3019
3061
  turn_id: ev?.turn_id ?? null,
3020
3062
  malformed_events: malformedEvents,
3021
3063
  at: ev?.at ?? null,
3064
+ rate_limit: rateLimit,
3022
3065
  });
3023
3066
  for (;;) {
3024
3067
  if (!fs.existsSync(metadataFile))
@@ -3067,6 +3110,11 @@ async function observeCodexDone(meta, timeout, requestedCursor) {
3067
3110
  }
3068
3111
  }
3069
3112
  }
3113
+ {
3114
+ const limited = detectAgentRateLimit(meta.kind, meta.aiterm_session);
3115
+ if (limited)
3116
+ return observation("rate_limited", null, limited);
3117
+ }
3070
3118
  if (performance.now() >= deadline)
3071
3119
  return observation(timeout === 0 ? "running" : "timeout");
3072
3120
  await sleep(AGENT_DONE_POLL_MS);
@@ -3084,7 +3132,7 @@ async function observeGrokDone(meta, timeout, requestedCursor) {
3084
3132
  let discardLeadingFragment = false;
3085
3133
  let initializedBoundary = false;
3086
3134
  const deadline = performance.now() + timeout * 1000;
3087
- const observation = (outcome, ev = null) => ({
3135
+ const observation = (outcome, ev = null, rateLimit = null) => ({
3088
3136
  schema: "aiterm.agent-wait-result.v1",
3089
3137
  session_id: meta.aiterm_session,
3090
3138
  launch_id: meta.launch_id,
@@ -3095,6 +3143,7 @@ async function observeGrokDone(meta, timeout, requestedCursor) {
3095
3143
  turn_id: ev?.turn_id ?? null,
3096
3144
  malformed_events: malformedEvents,
3097
3145
  at: ev?.at ?? null,
3146
+ rate_limit: rateLimit,
3098
3147
  });
3099
3148
  for (;;) {
3100
3149
  if (!fs.existsSync(metadataFile))
@@ -3141,6 +3190,11 @@ async function observeGrokDone(meta, timeout, requestedCursor) {
3141
3190
  }
3142
3191
  }
3143
3192
  }
3193
+ {
3194
+ const limited = detectAgentRateLimit(meta.kind, meta.aiterm_session);
3195
+ if (limited)
3196
+ return observation("rate_limited", null, limited);
3197
+ }
3144
3198
  if (performance.now() >= deadline)
3145
3199
  return observation(timeout === 0 ? "running" : "timeout");
3146
3200
  await sleep(AGENT_DONE_POLL_MS);
@@ -3173,7 +3227,7 @@ export async function observeAgentDone(name, o = {}) {
3173
3227
  let cursor = startOffset;
3174
3228
  let carry = "";
3175
3229
  let malformedEvents = 0;
3176
- const observation = (outcome, ev = null) => ({
3230
+ const observation = (outcome, ev = null, rateLimit = null) => ({
3177
3231
  schema: "aiterm.agent-wait-result.v1",
3178
3232
  session_id: meta.aiterm_session,
3179
3233
  launch_id: meta.launch_id,
@@ -3184,6 +3238,7 @@ export async function observeAgentDone(name, o = {}) {
3184
3238
  turn_id: ev?.turn_id ?? null,
3185
3239
  malformed_events: malformedEvents,
3186
3240
  at: ev?.at ?? null,
3241
+ rate_limit: rateLimit,
3187
3242
  });
3188
3243
  for (;;) {
3189
3244
  if (!fs.existsSync(metadataFile))
@@ -3211,6 +3266,11 @@ export async function observeAgentDone(name, o = {}) {
3211
3266
  }
3212
3267
  // timeout=0 は「待たずに一度だけ見る」照会=未完了は失敗ではなく running。
3213
3268
  // 1秒以上を指定した待機の未完了は従来どおり timeout で、待ち方の意味は変えない。
3269
+ {
3270
+ const limited = detectAgentRateLimit(meta.kind, meta.aiterm_session);
3271
+ if (limited)
3272
+ return observation("rate_limited", null, limited);
3273
+ }
3214
3274
  if (performance.now() >= deadline)
3215
3275
  return observation(timeout === 0 ? "running" : "timeout");
3216
3276
  await sleep(AGENT_DONE_POLL_MS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.27.3",
3
+ "version": "0.27.6",
4
4
  "mcpName": "io.github.kitepon/aiterm-mcp",
5
5
  "description": "Persistent tmux terminal MCP for launching and driving Claude, Codex, Grok, or Composer from any MCP client, cross-vendor or same-vendor. Also runs durable PTY sessions for SSH, containers, and REPLs.",
6
6
  "keywords": [
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "license": "MIT",
28
28
  "author": {
29
- "name": "Quo / クオ at kitepon.dev",
29
+ "name": "Quo / \u30af\u30aa at kitepon.dev",
30
30
  "url": "https://kitepon.dev/"
31
31
  },
32
32
  "repository": {