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/agent-shared.js +209 -0
- package/dist/aiterm-wait-cli.js +32 -7
- package/dist/core.js +93 -1284
- package/dist/tmux-runtime.js +72 -0
- package/package.json +2 -2
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
|
|
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,
|
|
19
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
711
|
-
const r = tmux("new-session", "-d", "-s", nm, ...banner, "-f",
|
|
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
|
-
|
|
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
|
-
// -
|
|
718
|
+
// -Sは対応新版だけでvis(3)制御文字変換を無効化する。
|
|
879
719
|
// send 自身の raw/sanitize 契約だけを真実とし、tmux 側で黙って再変換させない。
|
|
880
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
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
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
const
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
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
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
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" ?
|
|
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
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
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
|
-
|
|
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);
|