aiterm-mcp 0.27.3 → 0.27.7

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/dist/core.js CHANGED
@@ -10,13 +10,15 @@
10
10
  import { spawnSync } from "node:child_process";
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
- import * as os from "node:os";
14
- import { createHash, randomBytes, randomUUID } from "node:crypto";
15
- import { fileURLToPath } from "node:url";
13
+ import { createHash, randomBytes } from "node:crypto";
16
14
  import * as rtk from "./rtk.js";
17
15
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
18
- import { isWin, SOCKDIR, SOCK, WIN_NS, tmuxCommand, tmuxCommandWithInput } from "./tmux-runtime.js";
19
- import { resolveAgentBin, spawnAgentControlCommand, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
16
+ import { isWin, SOCKDIR, tmuxCommand, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, settlePaneLog, paneCwdArgument, } from "./tmux-runtime.js";
17
+ 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, agentLineageFields, } from "./agent-shared.js";
18
+ import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, grokInitializationComplete, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./vendors/grok.js";
19
+ import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./vendors/codex.js";
20
+ 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 "./vendors/claude.js";
21
+ import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
20
22
  export { AitermError } from "./errors.js";
21
23
  export { tmuxSpawnEnv } from "./tmux-runtime.js";
22
24
  // 完了検出
@@ -37,20 +39,13 @@ const NON_POSIX_MARK_SHELLS = new Set(["fish", "csh", "tcsh"]);
37
39
  // エコーは rc=%d(リテラル)。数字アンカーでエコーに免疫化し、部分一致による早期誤完了を防ぐ(B1)。
38
40
  // send の printf 書式(`rc=%d`)と対で保守すること。
39
41
  const MARK_DONE_RE = /<<<AITERM_DONE rc=[0-9]+>>>/;
40
- const LAUNCH_ID_RE = /^[0-9a-f]{32}$/;
41
- const OPERATION_ID_RE = /^sha256:[0-9a-f]{64}$/;
42
42
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
43
43
  const AGENT_LINEAGE_RE = /^[A-Za-z0-9_.:-]+(?:>[A-Za-z0-9_.:-]+)*$/;
44
- const AGENT_DONE_POLL_MS = 100;
45
44
  const AGENT_DONE_SETTLE_MIN_MS = 250;
46
45
  const AGENT_DONE_SCREEN_SETTLE_POLL_MS = 100;
47
46
  const AGENT_DONE_SCREEN_SETTLE_MAX_POLLS = 5;
48
47
  const AGENT_DONE_SCREEN_SETTLE_MIN_SAMPLES = 3;
49
48
  const AGENT_SUBMIT_DELAY_MS = 250;
50
- const AGENT_EVENT_MAX_BYTES = 1024 * 1024;
51
- const AGENT_EVENT_TAIL_BYTES = 64 * 1024;
52
- const CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES = 16 * 1024 * 1024;
53
- const GROK_TRANSCRIPT_INCREMENT_MAX_BYTES = 16 * 1024 * 1024;
54
49
  const AGENT_METADATA_NEGATIVE_CACHE_TTL_MS = 2_000;
55
50
  const AGENT_TUI_READY_TIMEOUT_MS = 30_000;
56
51
  const AGENT_TUI_READY_POLL_MS = 500;
@@ -63,10 +58,6 @@ const AGENT_SUBMIT_RESIDUE_POLL_MS = 300;
63
58
  const AGENT_SUBMIT_RESIDUE_MAX_SAMPLES = 5;
64
59
  const AGENT_SUBMIT_RESIDUE_TAIL_CHARS = 32;
65
60
  const AGENT_SUBMIT_RESIDUE_MIN_TAIL_CHARS = 8;
66
- const GROK_AUTH_MAX_BYTES = 64 * 1024;
67
- const GROK_MODELS_MAX_BYTES = 1024 * 1024;
68
- const GROK_MODELS_TIMEOUT_MS = 15_000;
69
- const CLAUDE_RESULT_MAX_BYTES = 4 * 1024 * 1024;
70
61
  // 出力削減(RTK の CAP 思想を移植)
71
62
  const MAX_LINES_BEFORE_ELIDE = 60;
72
63
  const HEAD_LINES = 30;
@@ -116,9 +107,6 @@ const KEYMAP = {
116
107
  function tmux(...args) {
117
108
  return tmuxCommand(true, ...args);
118
109
  }
119
- function tmuxWithInput(input, ...args) {
120
- return tmuxCommandWithInput(true, input, ...args);
121
- }
122
110
  function tmuxCleanup(...args) {
123
111
  return tmuxCommand(false, ...args);
124
112
  }
@@ -129,13 +117,7 @@ function paneCurrentCommand(name) {
129
117
  const r = tmux("display-message", "-p", "-t", name, "#{pane_current_command}");
130
118
  if (r.code !== 0)
131
119
  return "";
132
- const cmd = r.stdout.trim();
133
- // Windows(psmux)は "bash.exe" やフルパス形で報告しうる。SHELLS 等の POSIX 名
134
- // 集合と突合できるよう、basename・.exe 除去・小文字化へ正規化する(Windows の
135
- // 実行ファイル名は case-insensitive)。POSIX は従来どおり無加工。
136
- if (!isWin)
137
- return cmd;
138
- return path.basename(cmd).replace(/\.exe$/i, "").toLowerCase();
120
+ return normalizePaneCommand(r.stdout.trim());
139
121
  }
140
122
  function pasteBufferSupportsNoSanitizeFlag() {
141
123
  const listed = tmux("list-commands");
@@ -187,104 +169,6 @@ function assertSendTextSize(text, context = "送信文字列") {
187
169
  throw new AitermError(`${context}が${MAX_SEND_BYTES} bytesを超えています(${bytes} bytes)`, 2);
188
170
  }
189
171
  }
190
- // session 名はファイルパス(logpath 等)と pipe-pane の /bin/sh 文字列へ流れる。英数 _ - のみ・64字に
191
- // 限定し、パストラバーサル(../)とシェルインジェクション(' でのクオート破り・$・; 等)を全入口で断つ。
192
- function assertSessionName(name) {
193
- if (!/^[A-Za-z0-9_-]{1,64}$/.test(name))
194
- throw new AitermError(`session 名は英数字と _ - のみ・64文字以内にしてください: ${JSON.stringify(name)}`, 2);
195
- }
196
- function currentUid() {
197
- // Windows(native)は process.getuid を持たない。以前はここで throw していたが、
198
- // agent metadata の存在確認経由で素の pty_send まで巻き込んで全 send を殺していた。
199
- // Windows の fs.Stats.uid は常に 0 なので、ここも 0 を返せば owner 比較
200
- // (st.uid !== currentUid()) は自然に通過する。Windows は POSIX owner 検証を
201
- // 持たない(NTFS ACL は別体系)という既知の制約の明示的受容であり、POSIX 側は
202
- // getuid をそのまま返すため挙動不変。
203
- if (typeof process.getuid !== "function")
204
- return 0;
205
- return process.getuid();
206
- }
207
- function runtimeStateBase() {
208
- const xdg = process.env.XDG_RUNTIME_DIR;
209
- if (xdg) {
210
- try {
211
- if (fs.statSync(xdg).isDirectory())
212
- return xdg;
213
- }
214
- catch {
215
- /* XDG_RUNTIME_DIR が壊れている CI/非 login 環境では os.tmpdir() に戻す */
216
- }
217
- }
218
- return os.tmpdir();
219
- }
220
- function stateRoot() {
221
- const uid = currentUid();
222
- const base = runtimeStateBase();
223
- return path.join(base, `aiterm-mcp-${uid}`);
224
- }
225
- function ensureStateRoot() {
226
- // state root は OS が与える per-user runtime dir(XDG_RUNTIME_DIR / os.tmpdir())の下に作る。
227
- // 以前はここで symlink・owner・mode を検査していたが、共有 /tmp に敵対的な同居主体がいる
228
- // 前提の防御であり、対応 OS の既定配置では成立しない(オーナー裁定 2026-08-19)。
229
- // 作成時の 0o700 は検査ではなく妥当な既定として残す。経路の異常は以降の
230
- // open/stat が OS エラーとしてそのまま露出させる。
231
- const root = stateRoot();
232
- fs.mkdirSync(root, { recursive: true, mode: 0o700 });
233
- fs.mkdirSync(path.join(root, "agents"), { recursive: true, mode: 0o700 });
234
- return root;
235
- }
236
- function agentsDir() {
237
- return path.join(ensureStateRoot(), "agents");
238
- }
239
- function agentEventPath(name, launchId) {
240
- assertSessionName(name);
241
- if (!LAUNCH_ID_RE.test(launchId))
242
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
243
- return path.join(agentsDir(), `${name}.${launchId}.events.jsonl`);
244
- }
245
- function agentMetadataPath(name, launchId) {
246
- assertSessionName(name);
247
- if (!LAUNCH_ID_RE.test(launchId))
248
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
249
- return path.join(agentsDir(), `${name}.${launchId}.agent.json`);
250
- }
251
- function agentWaitLockPath(name, launchId) {
252
- assertSessionName(name);
253
- if (!LAUNCH_ID_RE.test(launchId))
254
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
255
- return path.join(agentsDir(), `${name}.${launchId}.wait.lock`);
256
- }
257
- function agentManagedClaudeSettingsPath(name, launchId) {
258
- assertSessionName(name);
259
- if (!LAUNCH_ID_RE.test(launchId))
260
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
261
- return path.join(agentsDir(), `${name}.${launchId}.claude-settings.json`);
262
- }
263
- function agentClaudeResultPath(name, launchId) {
264
- assertSessionName(name);
265
- if (!LAUNCH_ID_RE.test(launchId))
266
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
267
- return path.join(agentsDir(), `${name}.${launchId}.claude-result.json`);
268
- }
269
- function agentClaudeOperationPath(name, launchId) {
270
- assertSessionName(name);
271
- if (!LAUNCH_ID_RE.test(launchId))
272
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
273
- return path.join(agentsDir(), `${name}.${launchId}.claude-operation.json`);
274
- }
275
- function agentClaudeApprovalReceiptPath(name, launchId) {
276
- assertSessionName(name);
277
- if (!LAUNCH_ID_RE.test(launchId))
278
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
279
- return path.join(agentsDir(), `${name}.${launchId}.claude-approval.json`);
280
- }
281
- function agentClaudeDispatchReceiptPath(name, launchId, operationId) {
282
- assertSessionName(name);
283
- if (!LAUNCH_ID_RE.test(launchId))
284
- throw new AitermError(`launch_id が不正です: ${launchId}`, 2);
285
- const validated = validateOperationId(operationId);
286
- return path.join(agentsDir(), `${name}.${launchId}.${validated.slice("sha256:".length)}.claude-dispatch`);
287
- }
288
172
  function existingAgentsDir() {
289
173
  // 既存の agents dir があればその path、無ければ null(作成はしない)。
290
174
  // 以前はここで symlink・owner・mode を検査していたが撤去した(オーナー裁定 2026-08-19)。
@@ -381,8 +265,7 @@ function readLastcmd(name) {
381
265
  }
382
266
  }
383
267
  export function attachHint(name) {
384
- // Windows は native psmux(tmux CLI 互換)を -L namespace で叩く。
385
- const cmd = isWin ? `psmux -L ${WIN_NS} attach -t ${name}` : `tmux -S ${SOCK} attach -t ${name}`;
268
+ const cmd = attachCommand(name);
386
269
  return (`このセッションを自分の目で見る/介入する:\n` +
387
270
  ` ${cmd}\n` +
388
271
  ` (抜けるには Ctrl-b d)`);
@@ -502,27 +385,6 @@ function captureScreen(name, lines) {
502
385
  const r = tmux(...args);
503
386
  return r.code === 0 ? r.stdout : "";
504
387
  }
505
- const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
506
- // Windows 専用: pipe-pane のログは psmux server 側の in-process sink が書く=完了検知と
507
- // 書き手が別 process のため、完了/セッション消滅の報告後も末尾数百バイトが遅れて現れうる。
508
- // 完了と判定する直前にログサイズが伸びなくなるまで待ち、readOutput が末尾欠けの出力を
509
- // 返さないようにする(POSIX の tmux は /bin/sh sink・同一 fs で実測上不要)。
510
- async function settleWinLog(name) {
511
- let prev = -1;
512
- for (let i = 0; i < 8; i++) {
513
- let sz = 0;
514
- try {
515
- sz = fs.statSync(logpath(name)).size;
516
- }
517
- catch {
518
- sz = 0;
519
- }
520
- if (sz === prev)
521
- return;
522
- prev = sz;
523
- await sleep(POLL * 1000);
524
- }
525
- }
526
388
  /** 完了境界: dead / mark sentinel 一致 / until 一致 / (出力静止 ∧ シェル復帰)=quiescent / (ネスト中+until/mark無しで出力静止)=nested(未確定・早期返却) / timeout。 */
527
389
  async function waitCompletion(name, untilStr, untilRegex, timeout) {
528
390
  // 締切は単調時計で測る。Date.now() は NTP 補正やサスペンドで巻き戻り、長時間待ちで誤判定する(Python は time.monotonic)。
@@ -591,8 +453,7 @@ async function waitCompletion(name, untilStr, untilRegex, timeout) {
591
453
  catch {
592
454
  /* noop */
593
455
  }
594
- if (isWin)
595
- await settleWinLog(name);
456
+ await settlePaneLog(logpath(name), POLL);
596
457
  return [true, "mark"];
597
458
  }
598
459
  if (until && until(neu))
@@ -602,8 +463,7 @@ async function waitCompletion(name, untilStr, untilRegex, timeout) {
602
463
  // 静止時のみ has-session を叩いて dead を判定する。dead 検出は最大 1 poll 遅れるだけ。
603
464
  const growing = lastSize !== null && size > lastSize;
604
465
  if (!growing && !sessionExists(name)) {
605
- if (isWin)
606
- await settleWinLog(name);
466
+ await settlePaneLog(logpath(name), POLL);
607
467
  return [true, "dead"];
608
468
  }
609
469
  if (size === lastSize) {
@@ -619,8 +479,7 @@ async function waitCompletion(name, untilStr, untilRegex, timeout) {
619
479
  stable = 0;
620
480
  }
621
481
  else if (!until && !markActive && SHELLS.has(fg)) {
622
- if (isWin)
623
- await settleWinLog(name);
482
+ await settlePaneLog(logpath(name), POLL);
624
483
  return [true, "quiescent"]; // 出力静止 ∧ シェル復帰 = 確証つき完了
625
484
  }
626
485
  else if (!until && !markActive && fg !== "") {
@@ -630,8 +489,7 @@ async function waitCompletion(name, untilStr, untilRegex, timeout) {
630
489
  // until/markActive のときは指定した証拠を待つべく早期返却せず、shell builtinや
631
490
  // 非シェル前面(sleep 等)でも待ち続ける。
632
491
  // fg==="" は前面コマンド取得失敗=ネスト断定不可なので早期返却せず従来どおり timeout まで待つ。
633
- if (isWin)
634
- await settleWinLog(name);
492
+ await settlePaneLog(logpath(name), POLL);
635
493
  return [false, "nested"];
636
494
  }
637
495
  }
@@ -707,8 +565,8 @@ export function openSession(name, shell = "bash") {
707
565
  throw new AitermError(`session '${nm}' は既に存在します(list で確認)`, 2);
708
566
  }
709
567
  else {
710
- // -f は端末個人の設定ファイルを読まないための空 config(Windows は NUL デバイス)。
711
- const r = tmux("new-session", "-d", "-s", nm, ...banner, "-f", isWin ? "NUL" : "/dev/null", shell);
568
+ // -f は端末個人の設定ファイルを読まないための空 config。
569
+ const r = tmux("new-session", "-d", "-s", nm, ...banner, "-f", TMUX_EMPTY_CONFIG, shell);
712
570
  if (r.code === 0)
713
571
  break;
714
572
  // 自動採番かつ「重複名」由来の失敗(他エージェントが同名を先に取った)なら次名でリトライ。
@@ -851,34 +709,15 @@ export function send(name, text, o = {}) {
851
709
  const partial = i > 0
852
710
  ? " 先行chunkはPTYに入力済みでEnterは未送信です。再送前に入力を確認・消去してください。"
853
711
  : "";
854
- // psmux の load-buffer は stdin (`-`) 非対応で path 位置引数だけを取る。
855
- // Windows は owner-only の SOCKDIR 内へ一時ファイル経由で渡す。
856
- let loaded;
857
- if (isWin) {
858
- const chunkFile = path.join(SOCKDIR, `${bufferName}.chunk`);
859
- try {
860
- fs.writeFileSync(chunkFile, chunks[i], { encoding: "utf8" });
861
- loaded = tmux("load-buffer", "-b", bufferName, chunkFile);
862
- }
863
- finally {
864
- try {
865
- fs.unlinkSync(chunkFile);
866
- }
867
- catch { /* noop */ }
868
- }
869
- }
870
- else {
871
- loaded = tmuxWithInput(chunks[i], "load-buffer", "-b", bufferName, "-");
872
- }
712
+ const loaded = loadPtyBufferChunk(true, bufferName, chunks[i]);
873
713
  if (loaded.code !== 0) {
874
714
  tmuxCleanup("delete-buffer", "-b", bufferName);
875
715
  throw new AitermError(`tmux bufferへの送信準備に失敗しました` +
876
716
  `(chunk ${i + 1}/${chunks.length}): ${loaded.stderr.trim() || `code=${loaded.code}`}.${partial}`, 2);
877
717
  }
878
- // -r: LF→CR 置換を無効化。-Sは対応新版だけでvis(3)制御文字変換を無効化する。
718
+ // -Sは対応新版だけでvis(3)制御文字変換を無効化する。
879
719
  // send 自身の raw/sanitize 契約だけを真実とし、tmux 側で黙って再変換させない。
880
- // psmux は -r 非対応(受理フラグは d/p/b/t のみ)のため Windows では付けない。
881
- const pasteArgs = isWin ? ["paste-buffer", "-d"] : ["paste-buffer", "-d", "-r"];
720
+ const pasteArgs = pasteBufferBaseArgs();
882
721
  if (o.bracketedPaste)
883
722
  pasteArgs.push("-p");
884
723
  if (pasteSupportsNoSanitize)
@@ -1270,309 +1109,9 @@ function createAgentLineageContext(kind, sessionId, seed) {
1270
1109
  delegationAllowed: true,
1271
1110
  };
1272
1111
  }
1273
- function agentLineageFields(context) {
1274
- return {
1275
- agent_role: context.agentRole,
1276
- parent_session_id: context.parentSessionId,
1277
- delegation_depth: context.delegationDepth,
1278
- lineage: context.lineage,
1279
- delegation_allowed: context.delegationAllowed,
1280
- };
1281
- }
1282
- function subagentInstruction(meta) {
1283
- if (meta.agent_role !== "subagent" ||
1284
- !meta.parent_session_id ||
1285
- !Number.isSafeInteger(meta.delegation_depth) ||
1286
- !meta.lineage ||
1287
- meta.delegation_allowed !== true) {
1288
- throw new AitermError("sub-agent instructionに必要なlineage metadataがありません", 2);
1289
- }
1290
- return [
1291
- "<aiterm_subagent_context>",
1292
- "あなたはaitermから起動されたsub-agentであり、root agentではありません。",
1293
- `AITERM_AGENT_LAUNCH_ID=${meta.launch_id}`,
1294
- `role=${meta.agent_role}`,
1295
- `parent_session_id=${meta.parent_session_id}`,
1296
- `delegation_depth=${meta.delegation_depth}`,
1297
- `lineage=${meta.lineage}`,
1298
- "delegation_allowed=true",
1299
- "任務の所有権を保ち、結果を親へ返してください。必要なら追加のsub-agentへ委譲してよいです。",
1300
- "ただし、同じ任務全体を同型agentへ反射的に丸投げして自己複製ループを作らないでください。",
1301
- "</aiterm_subagent_context>",
1302
- ].join("\n");
1303
- }
1304
1112
  const DEFAULT_AGENT_DONE_TIMEOUT = 600;
1305
1113
  const agentMetadataNegativeCache = new Map();
1306
1114
  let agentTuiReadyStableSamplesTestOverride = null;
1307
- function claudeHookScriptPath() {
1308
- return path.join(path.dirname(fileURLToPath(import.meta.url)), "claude-stop-hook.js");
1309
- }
1310
- // process.execPath は Homebrew 等では Cellar の版付き実体を指す。長寿命 MCP server の起動後に
1311
- // runtime が更新されるとその実体だけが消え、既に生成済みの hook が exit 127 になる。
1312
- // hook は server と同じ継承 PATH から node を毎回解決し、安定した package script を実行する。
1313
- function nodeHookCommand(hookScript) {
1314
- return `${shq("node")} ${shq(hookScript)}`;
1315
- }
1316
- function safeStatSize(p) {
1317
- try {
1318
- return fs.statSync(p).size;
1319
- }
1320
- catch {
1321
- return 0;
1322
- }
1323
- }
1324
- function readFileRange(p, from, to) {
1325
- const len = Math.max(0, to - from);
1326
- if (len === 0)
1327
- return Buffer.alloc(0);
1328
- let fd;
1329
- try {
1330
- fd = fs.openSync(p, "r");
1331
- const buf = Buffer.alloc(len);
1332
- const n = fs.readSync(fd, buf, 0, len, from);
1333
- return n === len ? buf : buf.subarray(0, Math.max(0, n));
1334
- }
1335
- catch {
1336
- return Buffer.alloc(0);
1337
- }
1338
- finally {
1339
- if (fd !== undefined) {
1340
- try {
1341
- fs.closeSync(fd);
1342
- }
1343
- catch {
1344
- /* noop */
1345
- }
1346
- }
1347
- }
1348
- }
1349
- function writeJson0600(p, v) {
1350
- // truncate-in-place はクラッシュ/ENOSPC の窓で空・途中 JSON を残すので、temp→rename の原子的置換にする
1351
- const tmp = `${p}.${randomBytes(6).toString("hex")}.tmp`;
1352
- fs.writeFileSync(tmp, JSON.stringify(v, null, 2) + "\n", { mode: 0o600, flag: "wx" });
1353
- try {
1354
- fs.chmodSync(tmp, 0o600);
1355
- }
1356
- catch {
1357
- /* noop */
1358
- }
1359
- try {
1360
- fs.renameSync(tmp, p);
1361
- }
1362
- catch (e) {
1363
- try {
1364
- fs.unlinkSync(tmp);
1365
- }
1366
- catch {
1367
- /* noop */
1368
- }
1369
- throw e;
1370
- }
1371
- }
1372
- function writeText0600(p, text) {
1373
- fs.writeFileSync(p, text, { mode: 0o600 });
1374
- try {
1375
- fs.chmodSync(p, 0o600);
1376
- }
1377
- catch {
1378
- /* noop */
1379
- }
1380
- }
1381
- function createEmpty0600(p) {
1382
- // O_EXCL が「既存 path なら失敗」を保証するため、新規作成の一意性はこれで足りる。
1383
- // O_NOFOLLOW は撤去した(オーナー裁定 2026-08-19)。
1384
- const fd = fs.openSync(p, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o600);
1385
- fs.closeSync(fd);
1386
- }
1387
- function realCodexHome() {
1388
- return process.env.CODEX_HOME || path.join(process.env.HOME ?? os.homedir(), ".codex");
1389
- }
1390
- function readCodexConfigPins(configPath) {
1391
- let body;
1392
- try {
1393
- body = fs.readFileSync(configPath, "utf8");
1394
- }
1395
- catch {
1396
- return { model: { present: false, value: null }, effort: { present: false, value: null } };
1397
- }
1398
- const rows = body.split(/\r?\n/);
1399
- let firstTable = rows.findIndex((l) => /^\s*\[/.test(l));
1400
- if (firstTable === -1)
1401
- firstTable = rows.length;
1402
- const pick = (key) => {
1403
- for (const l of rows.slice(0, firstTable)) {
1404
- const m = l.match(new RegExp(`^\\s*(?:"${key}"|'${key}'|${key})\\s*=\\s*(.*)$`));
1405
- if (m) {
1406
- const v = m[1].trim().match(/^"([^"\\]*)"\s*(?:#.*)?$/);
1407
- return { present: true, value: v ? v[1] : null };
1408
- }
1409
- }
1410
- return { present: false, value: null };
1411
- };
1412
- return { model: pick("model"), effort: pick("model_reasoning_effort") };
1413
- }
1414
- function codexConfigSummary(configPath) {
1415
- let body;
1416
- try {
1417
- body = fs.readFileSync(configPath, "utf8");
1418
- }
1419
- catch {
1420
- return "";
1421
- }
1422
- const rows = body.split(/\r?\n/);
1423
- const mcpServers = rows.filter((line) => /^\s*\[mcp_servers\./.test(line)).length;
1424
- const firstTable = rows.findIndex((line) => /^\s*\[/.test(line));
1425
- const topLevel = rows.slice(0, firstTable === -1 ? rows.length : firstTable);
1426
- const valueOf = (key) => {
1427
- const row = topLevel.find((line) => new RegExp(`^\\s*(?:"${key}"|'${key}'|${key})\\s*=\\s*(.+?)\\s*(?:#.*)?$`).test(line));
1428
- if (!row)
1429
- return null;
1430
- const raw = row.match(/=\s*(.+?)(?:\s+#.*)?$/)?.[1].trim() ?? null;
1431
- if (!raw)
1432
- return null;
1433
- const quoted = raw.match(/^(?:"([^"\\]*)"|'([^'\\]*)')$/);
1434
- return quoted ? quoted[1] ?? quoted[2] : raw;
1435
- };
1436
- const bits = [`mcp_servers ${mcpServers} 個継承`];
1437
- const approvalPolicy = valueOf("approval_policy");
1438
- const sandboxMode = valueOf("sandbox_mode");
1439
- if (approvalPolicy)
1440
- bits.push(`approval_policy=${approvalPolicy}`);
1441
- if (sandboxMode)
1442
- bits.push(`sandbox_mode=${sandboxMode}`);
1443
- return `共有 config: ${bits.join(" / ")}`;
1444
- }
1445
- function createClaudeCorrelationSettings(name, launchId, model, effort) {
1446
- const hookScript = claudeHookScriptPath();
1447
- if (!fs.existsSync(hookScript)) {
1448
- throw new AitermError(`Claude Stop hook wrapper が見つかりません。npm run build を実行してください: ${hookScript}`, 2);
1449
- }
1450
- const settings = agentManagedClaudeSettingsPath(name, launchId);
1451
- writeJson0600(settings, {
1452
- ...(model ? { model } : {}),
1453
- ...(effort ? { effortLevel: effort } : {}),
1454
- hooks: {
1455
- Stop: [
1456
- {
1457
- hooks: [
1458
- {
1459
- type: "command",
1460
- command: nodeHookCommand(hookScript),
1461
- timeout: 10,
1462
- },
1463
- ],
1464
- },
1465
- ],
1466
- },
1467
- });
1468
- return settings;
1469
- }
1470
- function realGrokHome() {
1471
- return path.resolve(process.env.GROK_HOME || path.join(process.env.HOME ?? os.homedir(), ".grok"));
1472
- }
1473
- function resolveAndValidateGrokAuth(srcHome) {
1474
- const inheritedSet = Object.prototype.hasOwnProperty.call(process.env, "GROK_AUTH_PATH");
1475
- const inherited = process.env.GROK_AUTH_PATH;
1476
- if (inheritedSet && (!inherited || !path.isAbsolute(inherited)))
1477
- throw new AitermError("GROK_AUTH_PATH は空でない絶対パスで指定してください", 2);
1478
- const authPath = inherited ?? path.join(srcHome, "auth.json");
1479
- let fd;
1480
- try {
1481
- fd = fs.openSync(authPath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
1482
- const st = fs.fstatSync(fd);
1483
- // Windows の fs.Stats.mode は POSIX permission bit を持たず、常に 666/777 相当を報告する
1484
- // (NTFS ACL は別体系)。currentUid と同じ既知制約の明示的受容として、Windows では
1485
- // group/other bit 検証を行わない。isFile・nlink・owner・size・O_NOFOLLOW・realpath 検証は共通に維持する。
1486
- const worldAccessible = !isWin && (st.mode & 0o077) !== 0;
1487
- if (!st.isFile() || st.nlink !== 1 || st.uid !== currentUid() || worldAccessible || st.size > GROK_AUTH_MAX_BYTES) {
1488
- throw new AitermError("Grok 認証正本の安全検証に失敗しました", 2);
1489
- }
1490
- const value = JSON.parse(fs.readFileSync(fd, "utf8"));
1491
- if (!value || typeof value !== "object" || Array.isArray(value))
1492
- throw new AitermError("Grok 認証正本のJSONが不正です", 2);
1493
- // auth file 自体は O_NOFOLLOW で開いているが、中間 directory の symlink は辿り得る。
1494
- // vendor に渡す正本を path swap の入口にしないため、字句正規化した絶対 path と realpath を
1495
- // 一致させ、canonical な祖先も root まで検証する。same-UID race の排他は vendor lock の責務。
1496
- const lexicalPath = path.resolve(authPath);
1497
- const canonicalPath = fs.realpathSync(authPath);
1498
- if (lexicalPath !== canonicalPath)
1499
- throw new AitermError("Grok 認証正本の path に symlink を含められません", 2);
1500
- for (let dir = path.dirname(canonicalPath);; dir = path.dirname(dir)) {
1501
- const dirSt = fs.lstatSync(dir);
1502
- // /tmp のような root 所有 + sticky の共有 directory は、本人所有の private な
1503
- // 直下 directory を他 UID が rename/unlink できないため許可する。sticky 無しの
1504
- // group/other writable 祖先は path swap が可能なので従来どおり拒否する。
1505
- // Windows は directory も mode bit を持たない(常に 777 相当)ため、同じ受容で除外する。
1506
- const writableByOthers = !isWin && (dirSt.mode & 0o022) !== 0;
1507
- const protectedSharedRoot = dirSt.uid === 0 && (dirSt.mode & 0o1000) !== 0;
1508
- if (!dirSt.isDirectory()
1509
- || dirSt.isSymbolicLink()
1510
- || (dirSt.uid !== currentUid() && dirSt.uid !== 0)
1511
- || (writableByOthers && !protectedSharedRoot)) {
1512
- throw new AitermError("Grok 認証正本の祖先 directory が安全ではありません", 2);
1513
- }
1514
- const parent = path.dirname(dir);
1515
- if (parent === dir)
1516
- break;
1517
- }
1518
- return canonicalPath;
1519
- }
1520
- catch (e) {
1521
- if (e.code === "ENOENT" && !inheritedSet && process.env.XAI_API_KEY)
1522
- return null;
1523
- if (e instanceof AitermError)
1524
- throw e;
1525
- throw new AitermError(e.code === "ENOENT" ? "Grok 認証正本が見つかりません。先に grok login が必要です" : "Grok 認証正本を安全に開けません", 2);
1526
- }
1527
- finally {
1528
- if (fd !== undefined)
1529
- fs.closeSync(fd);
1530
- }
1531
- }
1532
- function grokModelCatalog(bin, cwd) {
1533
- const result = spawnAgentControlCommand(bin, ["models"], cwd, {
1534
- cwd,
1535
- encoding: "utf8",
1536
- env: process.env,
1537
- timeout: GROK_MODELS_TIMEOUT_MS,
1538
- maxBuffer: GROK_MODELS_MAX_BYTES,
1539
- windowsHide: true,
1540
- });
1541
- if (result.error || result.status !== 0) {
1542
- const detail = result.error?.message || result.stderr?.trim() || `exit=${result.status ?? "unknown"}`;
1543
- throw new AitermError(`Grok model catalog を取得できません: ${detail}`, 2);
1544
- }
1545
- const text = result.stdout.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
1546
- const marker = "Available models:";
1547
- const start = text.indexOf(marker);
1548
- if (start < 0)
1549
- throw new AitermError("Grok model catalog の出力形式が不正です(Available models がありません)", 2);
1550
- const models = [];
1551
- for (const line of text.slice(start + marker.length).split(/\r?\n/)) {
1552
- const match = line.match(/^\s{2}[-*]\s+(.+?)(?:\s+\(default\))?\s*$/);
1553
- if (match?.[1])
1554
- models.push(match[1]);
1555
- }
1556
- if (models.length === 0)
1557
- throw new AitermError("Grok model catalog に利用可能なmodelがありません", 2);
1558
- return models;
1559
- }
1560
- function assertGrokModelAvailable(bin, cwd, model) {
1561
- const models = grokModelCatalog(bin, cwd);
1562
- if (!models.includes(model)) {
1563
- throw new AitermError(`Grok model catalog に ${JSON.stringify(model)} がありません。利用可能: ${models.join(", ")}。` +
1564
- "別modelへfallbackせず起動を中止しました", 2);
1565
- }
1566
- }
1567
- function writeAgentMetadata(meta) {
1568
- writeJson0600(agentMetadataPath(meta.aiterm_session, meta.launch_id), meta);
1569
- }
1570
- function validateOperationId(operationId) {
1571
- if (typeof operationId !== "string" || !OPERATION_ID_RE.test(operationId)) {
1572
- throw new AitermError("operation_id は sha256:<64 lowercase hex> で指定してください", 2);
1573
- }
1574
- return operationId;
1575
- }
1576
1115
  function writeClaudeOperationMarker(meta, operationId) {
1577
1116
  if (meta.kind !== "claude")
1578
1117
  throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
@@ -1997,82 +1536,6 @@ function liveWaitLocks(name) {
1997
1536
  }
1998
1537
  return out;
1999
1538
  }
2000
- function createClaudeAgentMetadata(name, cwd, initialPrompt, launchOperationId, launchRequestDigest, lineageContext, model, effort) {
2001
- const launchId = randomBytes(16).toString("hex");
2002
- const eventFile = agentEventPath(name, launchId);
2003
- const resultFile = agentClaudeResultPath(name, launchId);
2004
- createEmpty0600(eventFile);
2005
- createEmpty0600(resultFile);
2006
- const claudeSettings = createClaudeCorrelationSettings(name, launchId, model, effort);
2007
- const meta = {
2008
- kind: "claude",
2009
- aiterm_session: name,
2010
- launch_id: launchId,
2011
- event_file: eventFile,
2012
- created_at: new Date().toISOString(),
2013
- cwd,
2014
- vendor_session_id: randomUUID(),
2015
- initial_prompt: initialPrompt,
2016
- launch_operation_id: launchOperationId,
2017
- launch_request_digest: launchRequestDigest,
2018
- hook_route: "shared_claude_settings",
2019
- ...agentLineageFields(lineageContext),
2020
- node_platform: process.platform,
2021
- claude_settings: claudeSettings,
2022
- result_file: resultFile,
2023
- };
2024
- writeAgentMetadata(meta);
2025
- return meta;
2026
- }
2027
- function createCodexAgentMetadata(name, cwd, initialPrompt, overrides = {}, writeScope, lineageContext) {
2028
- const launchId = randomBytes(16).toString("hex");
2029
- const eventFile = agentEventPath(name, launchId);
2030
- createEmpty0600(eventFile);
2031
- const codexHome = realCodexHome();
2032
- const meta = {
2033
- kind: "codex",
2034
- aiterm_session: name,
2035
- launch_id: launchId,
2036
- event_file: eventFile,
2037
- created_at: new Date().toISOString(),
2038
- cwd,
2039
- ...(writeScope === undefined ? {} : { write_scope: writeScope }),
2040
- vendor_session_id: null,
2041
- initial_prompt: initialPrompt,
2042
- hook_route: "shared_codex_home",
2043
- completion_route: "codex_transcript",
2044
- ...(lineageContext ? agentLineageFields(lineageContext) : {}),
2045
- node_platform: process.platform,
2046
- codex_home: codexHome,
2047
- };
2048
- writeAgentMetadata(meta);
2049
- return meta;
2050
- }
2051
- function createGrokAgentMetadata(kind, name, cwd, initialPrompt, authPath, writeScope, lineageContext) {
2052
- const launchId = randomBytes(16).toString("hex");
2053
- const eventFile = agentEventPath(name, launchId);
2054
- createEmpty0600(eventFile);
2055
- const grokHome = realGrokHome();
2056
- const meta = {
2057
- kind,
2058
- aiterm_session: name,
2059
- launch_id: launchId,
2060
- event_file: eventFile,
2061
- created_at: new Date().toISOString(),
2062
- cwd,
2063
- ...(writeScope === undefined ? {} : { write_scope: writeScope }),
2064
- vendor_session_id: randomUUID(),
2065
- initial_prompt: initialPrompt,
2066
- hook_route: "shared_grok_home",
2067
- completion_route: "grok_transcript",
2068
- ...(lineageContext ? agentLineageFields(lineageContext) : {}),
2069
- node_platform: process.platform,
2070
- grok_home: grokHome,
2071
- grok_auth_path: authPath,
2072
- };
2073
- writeAgentMetadata(meta);
2074
- return meta;
2075
- }
2076
1539
  function loadAgentLineageFields(m, required) {
2077
1540
  const present = m.agent_role !== undefined ||
2078
1541
  m.parent_session_id !== undefined ||
@@ -2319,7 +1782,7 @@ function bindCompletedInitialPrompt(meta) {
2319
1782
  if (meta.initial_prompt !== "pending" && meta.initial_prompt !== "sent")
2320
1783
  return;
2321
1784
  if (meta.kind === "codex") {
2322
- const done = latestCodexCompletion(meta);
1785
+ const done = latestCodexCompletion(meta, readTranscriptLines);
2323
1786
  if (!done) {
2324
1787
  throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。${agentWaitGuide(meta.aiterm_session)}`, 2);
2325
1788
  }
@@ -2328,7 +1791,7 @@ function bindCompletedInitialPrompt(meta) {
2328
1791
  return;
2329
1792
  }
2330
1793
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
2331
- if (!latestGrokCompletion(meta)) {
1794
+ if (!latestGrokCompletion(meta, readTranscriptLines)) {
2332
1795
  throw new AitermError(`agent session '${meta.aiterm_session}' は起動時 prompt の完了待ちです。${agentWaitGuide(meta.aiterm_session)}`, 2);
2333
1796
  }
2334
1797
  setInitialPromptState(meta, "done");
@@ -2363,9 +1826,9 @@ function tryLoadAgentMetadata(name) {
2363
1826
  }
2364
1827
  function latestAgentDoneEvent(meta, expectedOperationId = null) {
2365
1828
  if (meta.kind === "codex")
2366
- return latestCodexCompletion(meta);
1829
+ return latestCodexCompletion(meta, readTranscriptLines);
2367
1830
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
2368
- return latestGrokCompletion(meta);
1831
+ return latestGrokCompletion(meta, readTranscriptLines);
2369
1832
  }
2370
1833
  const size = safeStatSize(meta.event_file);
2371
1834
  if (size === 0)
@@ -2425,7 +1888,7 @@ function recoverAgentVendorSession(meta) {
2425
1888
  const transcript = bindCodexTranscriptSession(meta);
2426
1889
  if (!transcript)
2427
1890
  return;
2428
- if (meta.initial_prompt === "pending" && latestCodexCompletion(meta))
1891
+ if (meta.initial_prompt === "pending" && latestCodexCompletion(meta, readTranscriptLines))
2429
1892
  setInitialPromptState(meta, "done");
2430
1893
  return;
2431
1894
  }
@@ -2447,263 +1910,6 @@ function recoverAgentVendorSession(meta) {
2447
1910
  bindAgentVendorSession(meta, scanned.event);
2448
1911
  writeAgentMetadata(meta);
2449
1912
  }
2450
- function findLatestCodexTranscript(codexHome, vendorSessionId) {
2451
- const sessionsDir = path.join(codexHome, "sessions");
2452
- let latestFile = null;
2453
- let latestMtime = -Infinity;
2454
- const visit = (dir) => {
2455
- let entries;
2456
- try {
2457
- entries = fs.readdirSync(dir, { withFileTypes: true });
2458
- }
2459
- catch {
2460
- return;
2461
- }
2462
- for (const entry of entries) {
2463
- const file = path.join(dir, entry.name);
2464
- if (entry.isDirectory()) {
2465
- visit(file);
2466
- continue;
2467
- }
2468
- if (!entry.isFile() || !entry.name.startsWith("rollout-") || !entry.name.endsWith(".jsonl") || !entry.name.includes(vendorSessionId))
2469
- continue;
2470
- try {
2471
- const mtimeMs = fs.statSync(file).mtimeMs;
2472
- if (mtimeMs > latestMtime) {
2473
- latestFile = file;
2474
- latestMtime = mtimeMs;
2475
- }
2476
- }
2477
- catch {
2478
- // 探索中に消えた transcript は候補にしない。候補が無ければ明示エラーにする。
2479
- }
2480
- }
2481
- };
2482
- visit(sessionsDir);
2483
- return latestFile;
2484
- }
2485
- function listCodexTranscripts(codexHome) {
2486
- const sessionsDir = path.join(codexHome, "sessions");
2487
- const files = [];
2488
- const visit = (dir) => {
2489
- let entries;
2490
- try {
2491
- entries = fs.readdirSync(dir, { withFileTypes: true });
2492
- }
2493
- catch {
2494
- return;
2495
- }
2496
- for (const entry of entries) {
2497
- const file = path.join(dir, entry.name);
2498
- if (entry.isDirectory())
2499
- visit(file);
2500
- else if (entry.isFile() && entry.name.startsWith("rollout-") && entry.name.endsWith(".jsonl"))
2501
- files.push(file);
2502
- }
2503
- };
2504
- visit(sessionsDir);
2505
- return files.sort();
2506
- }
2507
- function codexTranscriptMatchesLaunch(file, meta) {
2508
- const createdAt = Date.parse(meta.created_at);
2509
- try {
2510
- const st = fs.statSync(file);
2511
- if (Number.isFinite(createdAt) && st.mtimeMs + 5_000 < createdAt)
2512
- return false;
2513
- }
2514
- catch {
2515
- return false;
2516
- }
2517
- const size = Math.min(safeStatSize(file), 1024 * 1024);
2518
- if (size === 0)
2519
- return false;
2520
- const marker = `AITERM_AGENT_LAUNCH_ID=${meta.launch_id}`;
2521
- let rootCli = false;
2522
- let launchMarker = false;
2523
- for (const line of readFileRange(file, 0, size).toString("utf8").split("\n")) {
2524
- if (!line.trim())
2525
- continue;
2526
- let record;
2527
- try {
2528
- record = JSON.parse(line);
2529
- }
2530
- catch {
2531
- continue;
2532
- }
2533
- if (record?.type === "session_meta") {
2534
- rootCli = record?.payload?.originator === "codex-tui" && record?.payload?.source === "cli";
2535
- if (!rootCli)
2536
- return false;
2537
- }
2538
- if (record?.type === "response_item" &&
2539
- record?.payload?.type === "message" &&
2540
- record?.payload?.role === "developer" &&
2541
- Array.isArray(record?.payload?.content)) {
2542
- launchMarker ||= record.payload.content.some((item) => (item?.type === "input_text" || item?.type === "output_text") &&
2543
- typeof item?.text === "string" &&
2544
- item.text.includes(marker));
2545
- }
2546
- if (rootCli && launchMarker)
2547
- return true;
2548
- }
2549
- return false;
2550
- }
2551
- function codexTranscriptSessionId(file) {
2552
- const size = Math.min(safeStatSize(file), AGENT_EVENT_TAIL_BYTES);
2553
- if (size === 0)
2554
- return null;
2555
- const text = readFileRange(file, 0, size).toString("utf8");
2556
- for (const line of text.split("\n")) {
2557
- if (!line.trim())
2558
- continue;
2559
- try {
2560
- const record = JSON.parse(line);
2561
- if (record?.type === "session_meta" && typeof record?.payload?.id === "string" && record.payload.id) {
2562
- return record.payload.id;
2563
- }
2564
- }
2565
- catch {
2566
- // startup中の未完結行は後のpollで読み直す。
2567
- }
2568
- }
2569
- return null;
2570
- }
2571
- function codexRootTranscript(meta) {
2572
- if (meta.kind !== "codex" || !meta.codex_home)
2573
- return null;
2574
- if (meta.vendor_session_id)
2575
- return findLatestCodexTranscript(meta.codex_home, meta.vendor_session_id);
2576
- if (meta.hook_route === "shared_codex_home") {
2577
- const matches = listCodexTranscripts(meta.codex_home).filter((file) => codexTranscriptMatchesLaunch(file, meta));
2578
- if (matches.length > 1) {
2579
- throw new AitermError("共有CODEX_HOMEに同じlaunch markerのroot rolloutが複数あります。sessionを閉じて起動し直してください。", 2);
2580
- }
2581
- return matches[0] ?? null;
2582
- }
2583
- return listCodexTranscripts(meta.codex_home)[0] ?? null;
2584
- }
2585
- function bindCodexTranscriptSession(meta) {
2586
- const transcript = codexRootTranscript(meta);
2587
- if (!transcript)
2588
- return null;
2589
- const vendorSessionId = codexTranscriptSessionId(transcript);
2590
- if (vendorSessionId && !meta.vendor_session_id) {
2591
- meta.vendor_session_id = vendorSessionId;
2592
- writeAgentMetadata(meta);
2593
- }
2594
- return transcript;
2595
- }
2596
- function codexCompletionEvent(meta, vendorSessionId, record) {
2597
- if (record?.type !== "event_msg" ||
2598
- record?.payload?.type !== "task_complete" ||
2599
- typeof record?.payload?.turn_id !== "string" ||
2600
- !record.payload.turn_id)
2601
- return null;
2602
- return {
2603
- type: "agent_done",
2604
- vendor: "codex",
2605
- aiterm_session: meta.aiterm_session,
2606
- launch_id: meta.launch_id,
2607
- vendor_session_id: vendorSessionId,
2608
- turn_id: record.payload.turn_id,
2609
- operation_id: null,
2610
- reason: "Codex transcript task_complete",
2611
- done_status: "turn_done",
2612
- stop_hook_active: false,
2613
- at: typeof record.timestamp === "string" ? record.timestamp : new Date().toISOString(),
2614
- };
2615
- }
2616
- function latestCodexCompletion(meta) {
2617
- const transcript = codexRootTranscript(meta);
2618
- if (!transcript)
2619
- return null;
2620
- const vendorSessionId = meta.vendor_session_id ?? codexTranscriptSessionId(transcript);
2621
- let latest = null;
2622
- for (const line of readTranscriptLines(transcript)) {
2623
- if (!line.trim())
2624
- continue;
2625
- try {
2626
- latest = codexCompletionEvent(meta, vendorSessionId, JSON.parse(line)) ?? latest;
2627
- }
2628
- catch {
2629
- // Codexが末尾を書込み中なら、その行は次の観測で完結してから読む。
2630
- }
2631
- }
2632
- return latest;
2633
- }
2634
- function grokSessionDirectory(meta) {
2635
- if ((meta.kind !== "grok" && meta.kind !== "composer") || !meta.grok_home || !meta.vendor_session_id)
2636
- return null;
2637
- const cwd = meta.cwd ?? process.cwd();
2638
- return path.join(meta.grok_home, "sessions", encodeURIComponent(cwd), meta.vendor_session_id);
2639
- }
2640
- function grokEventsTranscript(meta) {
2641
- const dir = grokSessionDirectory(meta);
2642
- return dir ? path.join(dir, "events.jsonl") : null;
2643
- }
2644
- function grokCompletionEvent(meta, record) {
2645
- if ((meta.kind !== "grok" && meta.kind !== "composer") ||
2646
- record?.type !== "turn_ended" ||
2647
- (record?.outcome !== "completed" && record?.outcome !== "cancelled"))
2648
- return null;
2649
- const turnId = typeof record?.ts === "string" || typeof record?.ts === "number" ? String(record.ts) : null;
2650
- return {
2651
- type: "agent_done",
2652
- vendor: meta.kind,
2653
- aiterm_session: meta.aiterm_session,
2654
- launch_id: meta.launch_id,
2655
- vendor_session_id: meta.vendor_session_id,
2656
- turn_id: turnId,
2657
- operation_id: null,
2658
- reason: `Grok transcript turn_ended:${record.outcome}`,
2659
- done_status: "turn_done",
2660
- stop_hook_active: false,
2661
- at: typeof record?.ts === "string" ? record.ts : new Date().toISOString(),
2662
- };
2663
- }
2664
- function latestGrokCompletion(meta) {
2665
- const transcript = grokEventsTranscript(meta);
2666
- if (!transcript || !fs.existsSync(transcript))
2667
- return null;
2668
- let latest = null;
2669
- for (const line of readTranscriptLines(transcript)) {
2670
- if (!line.trim())
2671
- continue;
2672
- try {
2673
- latest = grokCompletionEvent(meta, JSON.parse(line)) ?? latest;
2674
- }
2675
- catch {
2676
- // 末尾書込み中のlineは次の観測で完結してから読む。
2677
- }
2678
- }
2679
- return latest;
2680
- }
2681
- function grokInitializationComplete(meta) {
2682
- const transcript = grokEventsTranscript(meta);
2683
- if (!transcript || !fs.existsSync(transcript))
2684
- return false;
2685
- const size = safeStatSize(transcript);
2686
- const from = Math.max(0, size - GROK_TRANSCRIPT_INCREMENT_MAX_BYTES);
2687
- const lines = readFileRange(transcript, from, size).toString("utf8").split("\n");
2688
- if (from > 0)
2689
- lines.shift();
2690
- for (let index = lines.length - 1; index >= 0; index -= 1) {
2691
- const line = lines[index].trim();
2692
- if (!line)
2693
- continue;
2694
- try {
2695
- const record = JSON.parse(line);
2696
- if (record?.type === "mcp_init_completed")
2697
- return true;
2698
- if (record?.type === "mcp_init_started")
2699
- return false;
2700
- }
2701
- catch {
2702
- // 末尾書込み中や無関係な破損lineはready判定を進めない。
2703
- }
2704
- }
2705
- return false;
2706
- }
2707
1913
  function agentCompletionCursor(meta) {
2708
1914
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
2709
1915
  const transcript = grokEventsTranscript(meta);
@@ -2734,47 +1940,6 @@ function readTranscriptLines(file) {
2734
1940
  throw new AitermError(`transcript を読めません: ${e.message}`, 2);
2735
1941
  }
2736
1942
  }
2737
- function readClaudeResultText(meta, done, operationId) {
2738
- if (meta.kind !== "claude" || !meta.result_file || !done.result_digest || done.result_bytes == null) {
2739
- transcriptUnavailable();
2740
- }
2741
- let st;
2742
- try {
2743
- st = fs.lstatSync(meta.result_file);
2744
- }
2745
- catch {
2746
- transcriptUnavailable();
2747
- }
2748
- if (!st.isFile() ||
2749
- st.isSymbolicLink() ||
2750
- st.uid !== currentUid() ||
2751
- st.nlink !== 1 ||
2752
- (!isWin && (st.mode & 0o077) !== 0) ||
2753
- st.size > CLAUDE_RESULT_MAX_BYTES + 4096) {
2754
- throw new AitermError("Claude result file の安全検証に失敗しました", 2);
2755
- }
2756
- let result;
2757
- try {
2758
- result = JSON.parse(fs.readFileSync(meta.result_file, "utf8"));
2759
- }
2760
- catch {
2761
- throw new AitermError("Claude result file を読めません", 2);
2762
- }
2763
- const keys = result && typeof result === "object" && !Array.isArray(result) ? Object.keys(result).sort() : [];
2764
- if (keys.join(",") !== "operation_id,result_bytes,result_digest,schema,text,vendor_session_id" ||
2765
- result.schema !== "aiterm.claude-turn-result.v2" ||
2766
- result.operation_id !== done.operation_id ||
2767
- (operationId !== null && result.operation_id !== operationId) ||
2768
- result.vendor_session_id !== meta.vendor_session_id ||
2769
- result.result_digest !== done.result_digest ||
2770
- result.result_bytes !== done.result_bytes ||
2771
- typeof result.text !== "string" ||
2772
- Buffer.byteLength(result.text, "utf8") !== done.result_bytes ||
2773
- createHash("sha256").update(result.text, "utf8").digest("hex") !== done.result_digest) {
2774
- throw new AitermError("Claude result file が完了eventと一致しません", 2);
2775
- }
2776
- return result.text;
2777
- }
2778
1943
  const CLAUDE_COMPLETION_MARKER_SETTLE_TIMEOUT_MS = 1_000;
2779
1944
  function claudeCompletionWasPublishedAfterMarker(meta, marker) {
2780
1945
  let markerStat;
@@ -2833,75 +1998,13 @@ export async function readAgentTranscript(name, o = {}) {
2833
1998
  if (meta.kind === "claude") {
2834
1999
  if (!done)
2835
2000
  transcriptUnavailable();
2836
- text = readClaudeResultText(meta, done, operationId);
2001
+ text = readClaudeResultText(meta, done, operationId, transcriptUnavailable);
2837
2002
  }
2838
2003
  else if (meta.kind === "codex") {
2839
- if (!meta.codex_home)
2840
- transcriptUnavailable();
2841
- const transcript = findLatestCodexTranscript(meta.codex_home, meta.vendor_session_id);
2842
- if (!transcript)
2843
- transcriptUnavailable();
2844
- const lines = readTranscriptLines(transcript);
2845
- const matching = [];
2846
- let finalAnswer = "";
2847
- for (const line of lines) {
2848
- if (!line.trim())
2849
- continue;
2850
- let record;
2851
- try {
2852
- record = JSON.parse(line);
2853
- }
2854
- catch {
2855
- continue;
2856
- }
2857
- const payload = record?.payload;
2858
- if (record?.type === "response_item" &&
2859
- payload?.type === "message" &&
2860
- payload?.role === "assistant" &&
2861
- payload?.internal_chat_message_metadata_passthrough?.turn_id === turnId &&
2862
- Array.isArray(payload?.content)) {
2863
- for (const item of payload.content) {
2864
- if (item?.type === "output_text" && typeof item.text === "string")
2865
- matching.push(item.text);
2866
- }
2867
- }
2868
- if (record?.type === "event_msg" &&
2869
- payload?.type === "agent_message" &&
2870
- payload?.phase === "final_answer" &&
2871
- typeof payload?.message === "string") {
2872
- finalAnswer = payload.message;
2873
- }
2874
- }
2875
- text = matching.join("\n") || finalAnswer;
2004
+ text = codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable);
2876
2005
  }
2877
2006
  else {
2878
- if (!meta.grok_home)
2879
- transcriptUnavailable();
2880
- // cwd 未指定で起動した TUI はサーバープロセスの cwd を継承する。metadata に null が残る既存
2881
- // launch との互換のため、その実際の起動 cwd を path 導出に使う(launch 側は変更しない)。
2882
- const cwd = meta.cwd ?? process.cwd();
2883
- const transcript = path.join(meta.grok_home, "sessions", encodeURIComponent(cwd), meta.vendor_session_id, "chat_history.jsonl");
2884
- const lines = readTranscriptLines(transcript);
2885
- let lastUser = -1;
2886
- const records = [];
2887
- for (const line of lines) {
2888
- if (!line.trim())
2889
- continue;
2890
- try {
2891
- const record = JSON.parse(line);
2892
- records.push(record);
2893
- if (record?.type === "user" && !("synthetic_reason" in record))
2894
- lastUser = records.length - 1;
2895
- }
2896
- catch {
2897
- // 外部 transcript の壊れた1行は残りの完結行を読む妨げにしない。
2898
- }
2899
- }
2900
- text = records
2901
- .slice(lastUser + 1)
2902
- .filter((record) => record?.type === "assistant" && typeof record?.content === "string")
2903
- .map((record) => record.content)
2904
- .join("\n");
2007
+ text = grokTranscriptText(meta, readTranscriptLines, transcriptUnavailable);
2905
2008
  }
2906
2009
  if (!text.trim())
2907
2010
  transcriptNotFound(meta.kind);
@@ -2998,153 +2101,47 @@ export function agentWaitGuide(session) {
2998
2101
  const cmd = `aiterm-wait --session ${session ?? "<session_id>"} --cursor 0`;
2999
2102
  return `完了通知は ${agentWaitLaunchForm(cmd)} で受ける(親はここで待たない・polling 不要)。receipt の outcome=done を確認してから再取得する。`;
3000
2103
  }
3001
- async function observeCodexDone(meta, timeout, requestedCursor) {
3002
- const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
3003
- let transcript = codexRootTranscript(meta);
3004
- const startOffset = requestedCursor ?? (transcript ? safeStatSize(transcript) : 0);
3005
- let cursor = startOffset;
3006
- let carry = "";
3007
- let malformedEvents = 0;
3008
- let discardLeadingFragment = false;
3009
- let initializedBoundary = false;
3010
- const deadline = performance.now() + timeout * 1000;
3011
- const observation = (outcome, ev = null) => ({
3012
- schema: "aiterm.agent-wait-result.v1",
3013
- session_id: meta.aiterm_session,
3014
- launch_id: meta.launch_id,
3015
- vendor: "codex",
3016
- outcome,
3017
- operation_id: null,
3018
- vendor_session_id: ev?.vendor_session_id ?? meta.vendor_session_id ?? null,
3019
- turn_id: ev?.turn_id ?? null,
3020
- malformed_events: malformedEvents,
3021
- at: ev?.at ?? null,
3022
- });
3023
- for (;;) {
3024
- if (!fs.existsSync(metadataFile))
3025
- return observation("closed");
3026
- transcript ??= codexRootTranscript(meta);
3027
- if (transcript) {
3028
- if (!initializedBoundary) {
3029
- if (cursor > 0) {
3030
- const previous = readFileRange(transcript, cursor - 1, cursor).toString("utf8");
3031
- discardLeadingFragment = previous !== "\n";
3032
- }
3033
- initializedBoundary = true;
3034
- }
3035
- const size = safeStatSize(transcript);
3036
- if (size < cursor) {
3037
- throw new AitermError("Codex transcript が完了待機中に短くなりました。該当セッションを閉じて起動し直してください。", 2);
3038
- }
3039
- if (size - startOffset > CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES) {
3040
- throw new AitermError("Codex transcript のturn増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
3041
- }
3042
- if (size > cursor) {
3043
- carry += readFileRange(transcript, cursor, size).toString("utf8");
3044
- cursor = size;
3045
- const parts = carry.split("\n");
3046
- carry = parts.pop() ?? "";
3047
- if (discardLeadingFragment && parts.length > 0) {
3048
- parts.shift();
3049
- discardLeadingFragment = false;
3050
- }
3051
- const vendorSessionId = meta.vendor_session_id ?? codexTranscriptSessionId(transcript);
3052
- for (const line of parts) {
3053
- if (!line.trim())
3054
- continue;
3055
- if (Buffer.byteLength(line, "utf8") > AGENT_EVENT_MAX_BYTES) {
3056
- malformedEvents++;
3057
- continue;
3058
- }
3059
- try {
3060
- const done = codexCompletionEvent(meta, vendorSessionId, JSON.parse(line));
3061
- if (done)
3062
- return observation("done", done);
3063
- }
3064
- catch {
3065
- malformedEvents++;
3066
- }
3067
- }
3068
- }
3069
- }
3070
- if (performance.now() >= deadline)
3071
- return observation(timeout === 0 ? "running" : "timeout");
3072
- await sleep(AGENT_DONE_POLL_MS);
2104
+ // vendor 別の利用上限バナー。検知は「報告」専用で、完了判定や自動復旧には使わない。
2105
+ // 出典(2026-08-22): grok は live 実バナーで検証、codex/claude はインストール済み実バイナリの
2106
+ // 埋込文字列から抽出(codex: "You've hit your usage limit for" / claude: "Usage limit reached ·
2107
+ // continuing automatically when it resets"。Claude Code はリセット時に自動継続する設計なので、
2108
+ // この報告は「今は上限で止まっている」の観測であり恒久停止を意味しない)。
2109
+ const AGENT_RATE_LIMIT_PATTERNS = {
2110
+ grok: [/You hit your weekly limit/i, /Weekly limit left:\s*0%/i],
2111
+ composer: [/You hit your weekly limit/i, /Weekly limit left:\s*0%/i],
2112
+ codex: [/You'?ve hit your usage limit/i],
2113
+ claude: [/Usage limit reached/i],
2114
+ };
2115
+ const AGENT_RATE_LIMIT_SCAN_BYTES = 16 * 1024;
2116
+ // pane log の末尾から上限バナーを探す。読めない・無い・対象 vendor でないは全て null(誤検知より取りこぼし側へ倒す)。
2117
+ export function detectAgentRateLimit(kind, aitermSession) {
2118
+ const patterns = AGENT_RATE_LIMIT_PATTERNS[kind];
2119
+ if (!patterns)
2120
+ return null;
2121
+ const file = logpath(aitermSession);
2122
+ let size;
2123
+ try {
2124
+ size = fs.statSync(file).size;
3073
2125
  }
3074
- }
3075
- async function observeGrokDone(meta, timeout, requestedCursor) {
3076
- const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
3077
- const transcript = grokEventsTranscript(meta);
3078
- if (!transcript)
3079
- throw new AitermError("Grok transcriptのsession相関情報がありません", 2);
3080
- const startOffset = requestedCursor ?? safeStatSize(transcript);
3081
- let cursor = startOffset;
3082
- let carry = "";
3083
- let malformedEvents = 0;
3084
- let discardLeadingFragment = false;
3085
- let initializedBoundary = false;
3086
- const deadline = performance.now() + timeout * 1000;
3087
- const observation = (outcome, ev = null) => ({
3088
- schema: "aiterm.agent-wait-result.v1",
3089
- session_id: meta.aiterm_session,
3090
- launch_id: meta.launch_id,
3091
- vendor: meta.kind,
3092
- outcome,
3093
- operation_id: null,
3094
- vendor_session_id: meta.vendor_session_id,
3095
- turn_id: ev?.turn_id ?? null,
3096
- malformed_events: malformedEvents,
3097
- at: ev?.at ?? null,
3098
- });
3099
- for (;;) {
3100
- if (!fs.existsSync(metadataFile))
3101
- return observation("closed");
3102
- if (fs.existsSync(transcript)) {
3103
- if (!initializedBoundary) {
3104
- if (cursor > 0) {
3105
- const previous = readFileRange(transcript, cursor - 1, cursor).toString("utf8");
3106
- discardLeadingFragment = previous !== "\n";
3107
- }
3108
- initializedBoundary = true;
3109
- }
3110
- const size = safeStatSize(transcript);
3111
- if (size < cursor) {
3112
- throw new AitermError("Grok transcript が完了待機中に短くなりました。該当セッションを閉じて起動し直してください。", 2);
3113
- }
3114
- if (size - startOffset > GROK_TRANSCRIPT_INCREMENT_MAX_BYTES) {
3115
- throw new AitermError("Grok transcript のturn増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
3116
- }
3117
- if (size > cursor) {
3118
- carry += readFileRange(transcript, cursor, size).toString("utf8");
3119
- cursor = size;
3120
- const parts = carry.split("\n");
3121
- carry = parts.pop() ?? "";
3122
- if (discardLeadingFragment && parts.length > 0) {
3123
- parts.shift();
3124
- discardLeadingFragment = false;
3125
- }
3126
- for (const line of parts) {
3127
- if (!line.trim())
3128
- continue;
3129
- if (Buffer.byteLength(line, "utf8") > AGENT_EVENT_MAX_BYTES) {
3130
- malformedEvents++;
3131
- continue;
3132
- }
3133
- try {
3134
- const done = grokCompletionEvent(meta, JSON.parse(line));
3135
- if (done)
3136
- return observation("done", done);
3137
- }
3138
- catch {
3139
- malformedEvents++;
3140
- }
3141
- }
3142
- }
3143
- }
3144
- if (performance.now() >= deadline)
3145
- return observation(timeout === 0 ? "running" : "timeout");
3146
- await sleep(AGENT_DONE_POLL_MS);
2126
+ catch {
2127
+ return null;
2128
+ }
2129
+ if (size === 0)
2130
+ return null;
2131
+ let text;
2132
+ try {
2133
+ text = readFileRange(file, Math.max(0, size - AGENT_RATE_LIMIT_SCAN_BYTES), size).toString("utf8");
2134
+ }
2135
+ catch {
2136
+ return null;
2137
+ }
2138
+ const clean = text.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
2139
+ for (const re of patterns) {
2140
+ const match = clean.match(re);
2141
+ if (match)
2142
+ return match[0];
3147
2143
  }
2144
+ return null;
3148
2145
  }
3149
2146
  // 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
3150
2147
  // Codexはrollout transcript、他vendorはevent fileを増分走査し、vendor_session_idのbind永続化を
@@ -3160,10 +2157,10 @@ export async function observeAgentDone(name, o = {}) {
3160
2157
  }
3161
2158
  const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
3162
2159
  if (meta.kind === "codex" && meta.completion_route === "codex_transcript") {
3163
- return observeCodexDone(meta, timeout, o.cursor);
2160
+ return observeCodexDone(meta, timeout, o.cursor, detectAgentRateLimit);
3164
2161
  }
3165
2162
  if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
3166
- return observeGrokDone(meta, timeout, o.cursor);
2163
+ return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit);
3167
2164
  }
3168
2165
  const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
3169
2166
  // 境界の優先順: dispatch receipt の event_cursor(起動順序に依存しない)→ operation相関
@@ -3173,7 +2170,7 @@ export async function observeAgentDone(name, o = {}) {
3173
2170
  let cursor = startOffset;
3174
2171
  let carry = "";
3175
2172
  let malformedEvents = 0;
3176
- const observation = (outcome, ev = null) => ({
2173
+ const observation = (outcome, ev = null, rateLimit = null) => ({
3177
2174
  schema: "aiterm.agent-wait-result.v1",
3178
2175
  session_id: meta.aiterm_session,
3179
2176
  launch_id: meta.launch_id,
@@ -3184,6 +2181,7 @@ export async function observeAgentDone(name, o = {}) {
3184
2181
  turn_id: ev?.turn_id ?? null,
3185
2182
  malformed_events: malformedEvents,
3186
2183
  at: ev?.at ?? null,
2184
+ rate_limit: rateLimit,
3187
2185
  });
3188
2186
  for (;;) {
3189
2187
  if (!fs.existsSync(metadataFile))
@@ -3211,27 +2209,22 @@ export async function observeAgentDone(name, o = {}) {
3211
2209
  }
3212
2210
  // timeout=0 は「待たずに一度だけ見る」照会=未完了は失敗ではなく running。
3213
2211
  // 1秒以上を指定した待機の未完了は従来どおり timeout で、待ち方の意味は変えない。
2212
+ {
2213
+ const limited = detectAgentRateLimit(meta.kind, meta.aiterm_session);
2214
+ if (limited)
2215
+ return observation("rate_limited", null, limited);
2216
+ }
3214
2217
  if (performance.now() >= deadline)
3215
2218
  return observation(timeout === 0 ? "running" : "timeout");
3216
2219
  await sleep(AGENT_DONE_POLL_MS);
3217
2220
  }
3218
2221
  }
3219
2222
  function isAgentTuiReady(kind, screen) {
3220
- if (kind === "claude") {
3221
- return screen.includes("Claude Code") && /(^|\n)\s*❯/.test(screen);
3222
- }
3223
- if (kind === "codex") {
3224
- // 起動直後は製品header、長寿命sessionでは常駐footerがCodex TUIの識別子になる。
3225
- // capture-paneは直近45行だけなので、会話が進むとheaderは正常に画面外へ流れる。
3226
- const codexFrontend = screen.includes("OpenAI Codex")
3227
- || /(^|\n)\s*\S+\s+(?:low|medium|high|xhigh|max|ultra)(?:\s+fast)?\s+·\s+\S.*$/m.test(screen);
3228
- return codexFrontend && /(^|\n)\s*[›>]/.test(screen);
3229
- }
3230
- // Grok Build 0.2.117 は起動完了後に製品名を消し、model footerだけを残す。
3231
- // Composerも同じfrontendでmodel名だけが異なるため、両方をvendor UIの根拠にする。
3232
- // Windows native grok.exe(1.0.4 実測)は入力欄markerを `❯` でなく `>` で描画するため両方を受ける。
3233
- const grokFrontend = screen.includes("Grok Build") || /\b(?:Grok|Composer)\s+[\w.()-]+/.test(screen);
3234
- return grokFrontend && /(^|\n|\s)[❯>]/.test(screen);
2223
+ if (kind === "claude")
2224
+ return claudeTuiReady(screen);
2225
+ if (kind === "codex")
2226
+ return codexTuiReady(screen);
2227
+ return grokTuiReady(screen);
3235
2228
  }
3236
2229
  // Codex/Claude は実行中に「(esc to interrupt)」を表示する(実機採取)。startup 側の処理
3237
2230
  // (MCP initialize 等)が走ったまま composer だけ描画されている画面は入力受付とみなさない。
@@ -3296,7 +2289,7 @@ function agentSubmitResidueOnScreen(kind, screen, tail) {
3296
2289
  // 入力欄マーカーは ready 判定と同じ記号を行頭基準で探す。submit 済みの transcript echo は
3297
2290
  // マーカー行より上に出るため、最後のマーカー行以降だけを composer 領域として見る。
3298
2291
  // grok/composer は Windows native 描画(`>`・実測 1.0.4)も ready 判定と同様に受ける。
3299
- const markerRe = kind === "codex" ? /^\s*[›>]/ : kind === "claude" ? /^\s*❯/ : /(^|\s)[❯>]/;
2292
+ const markerRe = kind === "codex" ? CODEX_COMPOSER_MARKER_RE : kind === "claude" ? CLAUDE_COMPOSER_MARKER_RE : GROK_COMPOSER_MARKER_RE;
3300
2293
  let markerIdx = -1;
3301
2294
  for (let i = lines.length - 1; i >= 0; i--) {
3302
2295
  if (markerRe.test(lines[i])) {
@@ -3484,21 +2477,6 @@ function lineCounts(screen) {
3484
2477
  }
3485
2478
  return counts;
3486
2479
  }
3487
- function grokFooterHasConfiguration(screen, model, effort) {
3488
- const modelMatch = model?.match(/^grok-(\d+(?:\.\d+)*)$/) ?? null;
3489
- if (model && !modelMatch)
3490
- return false;
3491
- const modelLabel = modelMatch ? `Grok ${modelMatch[1]}` : null;
3492
- return screen.split("\n").some((line) => {
3493
- if (!line.includes("·"))
3494
- return false;
3495
- if (modelLabel && !line.includes(`${modelLabel} (`))
3496
- return false;
3497
- if (effort && !line.includes(`(${effort})`))
3498
- return false;
3499
- return modelLabel !== null || effort !== null;
3500
- });
3501
- }
3502
2480
  async function waitForGrokConfigurationResult(name, before, model, effort, timeoutMs = 3_000) {
3503
2481
  const beforeCounts = lineCounts(before);
3504
2482
  const footerAlreadyMatched = grokFooterHasConfiguration(before, model, effort);
@@ -3532,41 +2510,6 @@ function sendMenuChoice(name, choice) {
3532
2510
  throw new AitermError(`agent設定の選択を送れませんでした: ${sent.stderr.trim() || `code=${sent.code}`}`, 2);
3533
2511
  }
3534
2512
  }
3535
- function codexModelChoice(screen, model) {
3536
- for (const line of screen.slice(screen.lastIndexOf("Select Model and Effort")).split("\n")) {
3537
- const match = line.match(/^\s*(?:›\s*)?(\d+)\.\s+(\S+)/);
3538
- if (match?.[2] === model)
3539
- return match[1];
3540
- }
3541
- return null;
3542
- }
3543
- function codexEffortChoice(screen, effort) {
3544
- const labels = {
3545
- low: /^Low\b/i,
3546
- medium: /^Medium\b/i,
3547
- high: /^High\b/i,
3548
- xhigh: /^Extra high\b/i,
3549
- max: /^Max\b/i,
3550
- ultra: /^Ultra\b/i,
3551
- };
3552
- const wanted = labels[effort.toLowerCase()];
3553
- if (!wanted)
3554
- return null;
3555
- for (const line of screen.split("\n")) {
3556
- const match = line.match(/^\s*(?:›\s*)?(\d+)\.\s+(.+?)\s{2,}/);
3557
- if (match && wanted.test(match[2]))
3558
- return match[1];
3559
- }
3560
- return null;
3561
- }
3562
- function codexMoreReasoningChoice(screen) {
3563
- for (const line of screen.split("\n")) {
3564
- const match = line.match(/^\s*(?:›\s*)?(\d+)\.\s+More reasoning/);
3565
- if (match)
3566
- return match[1];
3567
- }
3568
- return null;
3569
- }
3570
2513
  /** 同じ対話sessionを保ったまま、vendor標準の操作でmodel/effortを変更する。 */
3571
2514
  export async function configureAgent(name, opts) {
3572
2515
  assertSessionName(name);
@@ -3779,47 +2722,13 @@ function inspectClaudeOperation(meta, operationId, action) {
3779
2722
  bindAgentVendorSession(meta, done);
3780
2723
  writeAgentMetadata(meta);
3781
2724
  }
3782
- const rawOutput = readClaudeResultText(meta, done, operationId);
2725
+ const rawOutput = readClaudeResultText(meta, done, operationId, transcriptUnavailable);
3783
2726
  return { ...base, status: "completed", raw_output: rawOutput, reason: null };
3784
2727
  }
3785
2728
  // ── 対話型エージェント起動(Claude / Codex / Grok Build(Grok) / Grok Build(Composer))──────
3786
2729
  // aiterm の永続端末に、指定モデルの対話エージェント TUI を起動する。以後は pty_read で画面を
3787
2730
  // 読み、pty_send で操作する=aiterm の対話パラダイムそのもの。モデルはツールごとに固定し、
3788
2731
  // reasoning effort は引数で渡す。CLI 未導入環境は明示エラー(動くフリをしない)。
3789
- const CLAUDE_AUTH_STATUS_TIMEOUT_MS = 5_000;
3790
- function assertClaudeAuthenticationReady(bin) {
3791
- const result = spawnAgentControlCommand(bin, ["auth", "status", "--json"], process.cwd(), {
3792
- encoding: "utf8",
3793
- timeout: CLAUDE_AUTH_STATUS_TIMEOUT_MS,
3794
- maxBuffer: 64 * 1024,
3795
- });
3796
- let status = null;
3797
- try {
3798
- status = JSON.parse((result.stdout ?? "").trim());
3799
- }
3800
- catch {
3801
- status = null;
3802
- }
3803
- if (result.error == null &&
3804
- result.status === 0 &&
3805
- status !== null &&
3806
- typeof status === "object" &&
3807
- !Array.isArray(status) &&
3808
- status.loggedIn === true) {
3809
- return;
3810
- }
3811
- if (status !== null &&
3812
- typeof status === "object" &&
3813
- !Array.isArray(status) &&
3814
- status.loggedIn === false) {
3815
- throw new AitermError("Claude Codeの認証を利用できません。sessionは作成していません。" +
3816
- "通常端末で `claude doctor` を実行し、Keychain/credential storeを直してから一度だけ `claude auth login` を実行してください。" +
3817
- "aiterm相関付きClaude session内で /login を繰り返さないでください。", 2);
3818
- }
3819
- const timedOut = result.error && result.error.code === "ETIMEDOUT";
3820
- throw new AitermError(`Claude Codeの認証状態を起動前に確認できません${timedOut ? "(5秒でtimeout)" : ""}。sessionは作成していません。` +
3821
- "`claude auth status --json` と `claude doctor` が成功することを通常端末で確認してください。", 2);
3822
- }
3823
2732
  const THROUGHLINE_HANDOFF_CONTEXT_SCHEMA = "throughline.handoff_context.v1";
3824
2733
  const PORTABLE_FORK_MISSION_SEPARATOR = "\n\n---\n\n## Portable fork mission\n\n";
3825
2734
  export function composePortableForkPrompt(context, mission) {
@@ -3862,66 +2771,12 @@ export function vendorLauncherDiagnostic(kind) {
3862
2771
  return "unverified";
3863
2772
  }
3864
2773
  }
3865
- // 単一引用符で安全に包む(' は '\'' で脱出)。send は raw:true で送るため自前で quote する。
3866
- function shq(s) {
3867
- return `'${s.replace(/'/g, "'\\''")}'`;
3868
- }
3869
- // grok CLI はモデル未指定だと端末側 default に従うため、ツール契約として既定 slug を固定する。
3870
- // codex は既定 slug を持たず端末 config/CLI 既定に委ねる(起動応答で実効値を報告する)。
3871
- // grok 既定は dotagents 規範(docs/02_models.md: xAI 旗艦 = grok-4.6)に従う。
3872
- const GROK_MODEL_DEFAULTS = {
3873
- grok: "grok-4.6",
3874
- composer: "grok-composer-2.5-fast",
3875
- };
3876
- const CLAUDE_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
3877
2774
  function buildAgentCmd(kind, bin, model, effort, prompt, meta = null) {
3878
- const parts = [shq(bin)];
3879
- if (kind === "claude") {
3880
- if (meta?.kind === "claude") {
3881
- parts.push("--setting-sources", shq("user,project,local"), "--settings", shq(meta.claude_settings ?? ""), "--session-id", shq(meta.vendor_session_id ?? ""), "--append-system-prompt", shq(subagentInstruction(meta)));
3882
- }
3883
- if (model)
3884
- parts.push("--model", shq(model));
3885
- if (effort)
3886
- parts.push("--effort", shq(effort));
3887
- }
3888
- else if (kind === "codex") {
3889
- // `codex --help` で確認した実在フラグ。read-only 宣言だけはCLI sandboxへ落とし、
3890
- // launcher自身が実効能力壁を作る。パス説明はCodex CLIに同等のallowlist引数がないため宣言のまま残す。
3891
- if (meta?.kind === "codex" && meta.write_scope === "read-only")
3892
- parts.push("--sandbox", "read-only");
3893
- // model/effort は共有configを書き換えず、CLI引数で明示して起動単位に優先する。
3894
- if (model)
3895
- parts.push("-m", shq(model));
3896
- if (effort)
3897
- parts.push("-c", `model_reasoning_effort=${shq(effort)}`);
3898
- if (meta?.kind === "codex" && meta.hook_route === "shared_codex_home") {
3899
- parts.push("-c", `developer_instructions=${shq(subagentInstruction(meta))}`);
3900
- }
3901
- }
3902
- else {
3903
- // grok / composer は同じ grok CLI をモデル違いで起動する。
3904
- parts.push("--no-auto-update");
3905
- if (meta?.kind === "grok" || meta?.kind === "composer")
3906
- parts.push("--no-alt-screen");
3907
- parts.push("--model", shq(model ?? GROK_MODEL_DEFAULTS[kind]));
3908
- if (effort)
3909
- parts.push("--reasoning-effort", shq(effort));
3910
- if ((meta?.kind === "grok" || meta?.kind === "composer") && meta.write_scope === "read-only") {
3911
- // read-only は sandbox が実効書込み禁止を作るため、MCP ツール許可ダイアログの自動承認を
3912
- // 付けても能力は増えない。無人 subagent が初回 MCP 使用の許可待ちで停止する実障害への対処。
3913
- // read-only 以外の launch には付けない=権限拡大しない。
3914
- parts.push("--sandbox", "read-only", "--always-approve");
3915
- }
3916
- if ((meta?.kind === "grok" || meta?.kind === "composer") && meta.hook_route === "shared_grok_home") {
3917
- parts.push("--session-id", shq(meta.vendor_session_id ?? ""), "--rules", shq(subagentInstruction(meta)));
3918
- }
3919
- if ((meta?.kind === "grok" || meta?.kind === "composer") && prompt)
3920
- parts.push("--verbatim");
3921
- }
3922
- if (prompt)
3923
- parts.push(shq(prompt)); // 初手プロンプト(任意)
3924
- return parts.join(" ");
2775
+ if (kind === "claude")
2776
+ return buildClaudeAgentCmd(bin, model, effort, prompt, meta);
2777
+ if (kind === "codex")
2778
+ return buildCodexAgentCmd(bin, model, effort, prompt, meta);
2779
+ return buildGrokAgentCmd(kind, bin, model, effort, prompt, meta);
3925
2780
  }
3926
2781
  function agentEnvPrefix(meta, sid, envVars = []) {
3927
2782
  const inherited = envVars.flatMap((name) => {
@@ -3945,60 +2800,16 @@ function agentEnvPrefix(meta, sid, envVars = []) {
3945
2800
  ];
3946
2801
  if (meta.kind === "claude" || meta.kind === "codex")
3947
2802
  return common;
3948
- // grok/composer: 検証済み auth 正本をそのままの path 形で渡す。Windows では native 強制により
3949
- // vendor は Windows process なので、Windows ドライブパスが正しい形(WSL 形への変換はしない)。
3950
- return [
3951
- ...(meta.grok_auth_path ? [`GROK_AUTH_PATH=${shq(meta.grok_auth_path)}`] : []),
3952
- "GROK_DISABLE_AUTOUPDATER=1",
3953
- ...common,
3954
- ];
2803
+ return [...grokEnvTokens(meta), ...common];
3955
2804
  })();
3956
2805
  return tokens.length ? tokens.join(" ") + " " : "";
3957
2806
  }
3958
- function agentLabel(kind) {
3959
- return kind === "claude"
3960
- ? "Claude Code"
3961
- : kind === "composer"
3962
- ? "Grok Build(Composer)"
3963
- : kind === "grok"
3964
- ? "Grok Build(Grok)"
3965
- : "Codex";
3966
- }
3967
- // 起動応答にモデル/effort の実効値と出所を明示する。codex は端末 config のピン(model /
3968
- // model_reasoning_effort)が対話子へ波及する構造のため、引数・端末config継承・CLI既定の
3969
- // どれで起動したかを起動時点で可視化し、実効 effort=ultra は proactive 自動委譲 ON を警告する。
3970
2807
  function buildAgentLaunchNote(kind, model, effort, meta) {
3971
- const writeScopeNote = meta?.write_scope === undefined
3972
- ? ""
3973
- : (kind === "codex" || kind === "grok" || kind === "composer") && meta.write_scope === "read-only"
3974
- ? `\n能力宣言: write_scope=${JSON.stringify(meta.write_scope)}。${agentLabel(kind)} CLIへ --sandbox read-only を付与し、書込みを実効禁止。` +
3975
- (kind === "codex" ? "" : "MCPツール許可は --always-approve で自動承認(sandbox内のため能力拡大なし)。")
3976
- : `\n能力宣言: write_scope=${JSON.stringify(meta.write_scope)}。パス単位のsandbox allowlistに対応するCLI引数がないため宣言の記録のみ(構造的unsupported)。`;
3977
- if (kind === "claude") {
3978
- return `起動設定: model=${model ?? "CLI既定"} effort=${effort ?? "CLI既定"}。${writeScopeNote}`;
3979
- }
3980
- if (kind !== "codex") {
3981
- return (`起動設定: model=${model ?? GROK_MODEL_DEFAULTS[kind]}(${model ? "引数" : "ツール既定"})。` +
3982
- `effort=${effort ?? "CLI/model既定"}。` + writeScopeNote);
3983
- }
3984
- const configPath = meta?.kind === "codex" && meta.codex_home
3985
- ? path.join(meta.codex_home, "config.toml")
3986
- : path.join(realCodexHome(), "config.toml");
3987
- const pins = readCodexConfigPins(configPath);
3988
- const describePin = (arg, pin) => arg
3989
- ? `${arg}(引数)`
3990
- : pin.present
3991
- ? pin.value
3992
- ? `${pin.value}(端末config継承)`
3993
- : "端末config継承(値未解析)"
3994
- : "CLI既定";
3995
- const effectiveEffort = effort ?? (pins.effort.present ? pins.effort.value : null);
3996
- const launch = `起動設定: model=${describePin(model, pins.model)} effort=${describePin(effort, pins.effort)}。` +
3997
- (effectiveEffort === "ultra"
3998
- ? "⚠ effort=ultra は max 推論+proactive 自動委譲 ON(子エージェント自動生成・使用量急増に注意)。"
3999
- : "");
4000
- const summary = meta?.kind === "codex" && meta.codex_home ? codexConfigSummary(configPath) : "";
4001
- return (summary ? `${launch}\n${summary}\n` : launch) + writeScopeNote;
2808
+ if (kind === "claude")
2809
+ return claudeLaunchNote(model, effort, meta);
2810
+ if (kind !== "codex")
2811
+ return grokLaunchNote(kind, model, effort, meta);
2812
+ return codexLaunchNote(model, effort, meta);
4002
2813
  }
4003
2814
  function claudeLaunchRequestDigest({ sessionName, model, effort, cwd, agentDone, }) {
4004
2815
  const canonical = JSON.stringify({
@@ -4133,9 +2944,7 @@ export function openAgent(kind, opts = {}) {
4133
2944
  "Windows 版 Grok CLI を導入するか、GROK_BIN に grok.exe の絶対パスを指定してください。", 2), 2);
4134
2945
  }
4135
2946
  const binForCmd = agentBinForPaneShell(bin);
4136
- // Windows native pane(psmux + Git Bash)は Windows パスをそのまま扱える。
4137
- // Git Bash の cd はドライブパスを forward slash 形で受けるのが安全。
4138
- const cwdForCmd = cwd && isWin ? cwd.replace(/\\/g, "/") : cwd;
2947
+ const cwdForCmd = cwd ? paneCwdArgument(cwd) : cwd;
4139
2948
  const grokAuthPath = agentDone && (kind === "grok" || kind === "composer") ? resolveAndValidateGrokAuth(realGrokHome()) : null;
4140
2949
  if (kind === "grok" || kind === "composer") {
4141
2950
  const requestedModel = model ?? (kind === "composer" ? GROK_MODEL_DEFAULTS.composer : null);