aiterm-mcp 0.27.6 → 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/core.js +47 -1298
- package/dist/tmux-runtime.js +72 -0
- package/package.json +1 -1
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);
|
|
@@ -3040,166 +2143,6 @@ export function detectAgentRateLimit(kind, aitermSession) {
|
|
|
3040
2143
|
}
|
|
3041
2144
|
return null;
|
|
3042
2145
|
}
|
|
3043
|
-
async function observeCodexDone(meta, timeout, requestedCursor) {
|
|
3044
|
-
const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
|
|
3045
|
-
let transcript = codexRootTranscript(meta);
|
|
3046
|
-
const startOffset = requestedCursor ?? (transcript ? safeStatSize(transcript) : 0);
|
|
3047
|
-
let cursor = startOffset;
|
|
3048
|
-
let carry = "";
|
|
3049
|
-
let malformedEvents = 0;
|
|
3050
|
-
let discardLeadingFragment = false;
|
|
3051
|
-
let initializedBoundary = false;
|
|
3052
|
-
const deadline = performance.now() + timeout * 1000;
|
|
3053
|
-
const observation = (outcome, ev = null, rateLimit = null) => ({
|
|
3054
|
-
schema: "aiterm.agent-wait-result.v1",
|
|
3055
|
-
session_id: meta.aiterm_session,
|
|
3056
|
-
launch_id: meta.launch_id,
|
|
3057
|
-
vendor: "codex",
|
|
3058
|
-
outcome,
|
|
3059
|
-
operation_id: null,
|
|
3060
|
-
vendor_session_id: ev?.vendor_session_id ?? meta.vendor_session_id ?? null,
|
|
3061
|
-
turn_id: ev?.turn_id ?? null,
|
|
3062
|
-
malformed_events: malformedEvents,
|
|
3063
|
-
at: ev?.at ?? null,
|
|
3064
|
-
rate_limit: rateLimit,
|
|
3065
|
-
});
|
|
3066
|
-
for (;;) {
|
|
3067
|
-
if (!fs.existsSync(metadataFile))
|
|
3068
|
-
return observation("closed");
|
|
3069
|
-
transcript ??= codexRootTranscript(meta);
|
|
3070
|
-
if (transcript) {
|
|
3071
|
-
if (!initializedBoundary) {
|
|
3072
|
-
if (cursor > 0) {
|
|
3073
|
-
const previous = readFileRange(transcript, cursor - 1, cursor).toString("utf8");
|
|
3074
|
-
discardLeadingFragment = previous !== "\n";
|
|
3075
|
-
}
|
|
3076
|
-
initializedBoundary = true;
|
|
3077
|
-
}
|
|
3078
|
-
const size = safeStatSize(transcript);
|
|
3079
|
-
if (size < cursor) {
|
|
3080
|
-
throw new AitermError("Codex transcript が完了待機中に短くなりました。該当セッションを閉じて起動し直してください。", 2);
|
|
3081
|
-
}
|
|
3082
|
-
if (size - startOffset > CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES) {
|
|
3083
|
-
throw new AitermError("Codex transcript のturn増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
|
|
3084
|
-
}
|
|
3085
|
-
if (size > cursor) {
|
|
3086
|
-
carry += readFileRange(transcript, cursor, size).toString("utf8");
|
|
3087
|
-
cursor = size;
|
|
3088
|
-
const parts = carry.split("\n");
|
|
3089
|
-
carry = parts.pop() ?? "";
|
|
3090
|
-
if (discardLeadingFragment && parts.length > 0) {
|
|
3091
|
-
parts.shift();
|
|
3092
|
-
discardLeadingFragment = false;
|
|
3093
|
-
}
|
|
3094
|
-
const vendorSessionId = meta.vendor_session_id ?? codexTranscriptSessionId(transcript);
|
|
3095
|
-
for (const line of parts) {
|
|
3096
|
-
if (!line.trim())
|
|
3097
|
-
continue;
|
|
3098
|
-
if (Buffer.byteLength(line, "utf8") > AGENT_EVENT_MAX_BYTES) {
|
|
3099
|
-
malformedEvents++;
|
|
3100
|
-
continue;
|
|
3101
|
-
}
|
|
3102
|
-
try {
|
|
3103
|
-
const done = codexCompletionEvent(meta, vendorSessionId, JSON.parse(line));
|
|
3104
|
-
if (done)
|
|
3105
|
-
return observation("done", done);
|
|
3106
|
-
}
|
|
3107
|
-
catch {
|
|
3108
|
-
malformedEvents++;
|
|
3109
|
-
}
|
|
3110
|
-
}
|
|
3111
|
-
}
|
|
3112
|
-
}
|
|
3113
|
-
{
|
|
3114
|
-
const limited = detectAgentRateLimit(meta.kind, meta.aiterm_session);
|
|
3115
|
-
if (limited)
|
|
3116
|
-
return observation("rate_limited", null, limited);
|
|
3117
|
-
}
|
|
3118
|
-
if (performance.now() >= deadline)
|
|
3119
|
-
return observation(timeout === 0 ? "running" : "timeout");
|
|
3120
|
-
await sleep(AGENT_DONE_POLL_MS);
|
|
3121
|
-
}
|
|
3122
|
-
}
|
|
3123
|
-
async function observeGrokDone(meta, timeout, requestedCursor) {
|
|
3124
|
-
const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
|
|
3125
|
-
const transcript = grokEventsTranscript(meta);
|
|
3126
|
-
if (!transcript)
|
|
3127
|
-
throw new AitermError("Grok transcriptのsession相関情報がありません", 2);
|
|
3128
|
-
const startOffset = requestedCursor ?? safeStatSize(transcript);
|
|
3129
|
-
let cursor = startOffset;
|
|
3130
|
-
let carry = "";
|
|
3131
|
-
let malformedEvents = 0;
|
|
3132
|
-
let discardLeadingFragment = false;
|
|
3133
|
-
let initializedBoundary = false;
|
|
3134
|
-
const deadline = performance.now() + timeout * 1000;
|
|
3135
|
-
const observation = (outcome, ev = null, rateLimit = null) => ({
|
|
3136
|
-
schema: "aiterm.agent-wait-result.v1",
|
|
3137
|
-
session_id: meta.aiterm_session,
|
|
3138
|
-
launch_id: meta.launch_id,
|
|
3139
|
-
vendor: meta.kind,
|
|
3140
|
-
outcome,
|
|
3141
|
-
operation_id: null,
|
|
3142
|
-
vendor_session_id: meta.vendor_session_id,
|
|
3143
|
-
turn_id: ev?.turn_id ?? null,
|
|
3144
|
-
malformed_events: malformedEvents,
|
|
3145
|
-
at: ev?.at ?? null,
|
|
3146
|
-
rate_limit: rateLimit,
|
|
3147
|
-
});
|
|
3148
|
-
for (;;) {
|
|
3149
|
-
if (!fs.existsSync(metadataFile))
|
|
3150
|
-
return observation("closed");
|
|
3151
|
-
if (fs.existsSync(transcript)) {
|
|
3152
|
-
if (!initializedBoundary) {
|
|
3153
|
-
if (cursor > 0) {
|
|
3154
|
-
const previous = readFileRange(transcript, cursor - 1, cursor).toString("utf8");
|
|
3155
|
-
discardLeadingFragment = previous !== "\n";
|
|
3156
|
-
}
|
|
3157
|
-
initializedBoundary = true;
|
|
3158
|
-
}
|
|
3159
|
-
const size = safeStatSize(transcript);
|
|
3160
|
-
if (size < cursor) {
|
|
3161
|
-
throw new AitermError("Grok transcript が完了待機中に短くなりました。該当セッションを閉じて起動し直してください。", 2);
|
|
3162
|
-
}
|
|
3163
|
-
if (size - startOffset > GROK_TRANSCRIPT_INCREMENT_MAX_BYTES) {
|
|
3164
|
-
throw new AitermError("Grok transcript のturn増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
|
|
3165
|
-
}
|
|
3166
|
-
if (size > cursor) {
|
|
3167
|
-
carry += readFileRange(transcript, cursor, size).toString("utf8");
|
|
3168
|
-
cursor = size;
|
|
3169
|
-
const parts = carry.split("\n");
|
|
3170
|
-
carry = parts.pop() ?? "";
|
|
3171
|
-
if (discardLeadingFragment && parts.length > 0) {
|
|
3172
|
-
parts.shift();
|
|
3173
|
-
discardLeadingFragment = false;
|
|
3174
|
-
}
|
|
3175
|
-
for (const line of parts) {
|
|
3176
|
-
if (!line.trim())
|
|
3177
|
-
continue;
|
|
3178
|
-
if (Buffer.byteLength(line, "utf8") > AGENT_EVENT_MAX_BYTES) {
|
|
3179
|
-
malformedEvents++;
|
|
3180
|
-
continue;
|
|
3181
|
-
}
|
|
3182
|
-
try {
|
|
3183
|
-
const done = grokCompletionEvent(meta, JSON.parse(line));
|
|
3184
|
-
if (done)
|
|
3185
|
-
return observation("done", done);
|
|
3186
|
-
}
|
|
3187
|
-
catch {
|
|
3188
|
-
malformedEvents++;
|
|
3189
|
-
}
|
|
3190
|
-
}
|
|
3191
|
-
}
|
|
3192
|
-
}
|
|
3193
|
-
{
|
|
3194
|
-
const limited = detectAgentRateLimit(meta.kind, meta.aiterm_session);
|
|
3195
|
-
if (limited)
|
|
3196
|
-
return observation("rate_limited", null, limited);
|
|
3197
|
-
}
|
|
3198
|
-
if (performance.now() >= deadline)
|
|
3199
|
-
return observation(timeout === 0 ? "running" : "timeout");
|
|
3200
|
-
await sleep(AGENT_DONE_POLL_MS);
|
|
3201
|
-
}
|
|
3202
|
-
}
|
|
3203
2146
|
// 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
|
|
3204
2147
|
// Codexはrollout transcript、他vendorはevent fileを増分走査し、vendor_session_idのbind永続化を
|
|
3205
2148
|
// 行わない(waiterは観測者であって所有者でない)。
|
|
@@ -3214,10 +2157,10 @@ export async function observeAgentDone(name, o = {}) {
|
|
|
3214
2157
|
}
|
|
3215
2158
|
const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
|
|
3216
2159
|
if (meta.kind === "codex" && meta.completion_route === "codex_transcript") {
|
|
3217
|
-
return observeCodexDone(meta, timeout, o.cursor);
|
|
2160
|
+
return observeCodexDone(meta, timeout, o.cursor, detectAgentRateLimit);
|
|
3218
2161
|
}
|
|
3219
2162
|
if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
|
|
3220
|
-
return observeGrokDone(meta, timeout, o.cursor);
|
|
2163
|
+
return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit);
|
|
3221
2164
|
}
|
|
3222
2165
|
const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
|
|
3223
2166
|
// 境界の優先順: dispatch receipt の event_cursor(起動順序に依存しない)→ operation相関
|
|
@@ -3277,21 +2220,11 @@ export async function observeAgentDone(name, o = {}) {
|
|
|
3277
2220
|
}
|
|
3278
2221
|
}
|
|
3279
2222
|
function isAgentTuiReady(kind, screen) {
|
|
3280
|
-
if (kind === "claude")
|
|
3281
|
-
return
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
// capture-paneは直近45行だけなので、会話が進むとheaderは正常に画面外へ流れる。
|
|
3286
|
-
const codexFrontend = screen.includes("OpenAI Codex")
|
|
3287
|
-
|| /(^|\n)\s*\S+\s+(?:low|medium|high|xhigh|max|ultra)(?:\s+fast)?\s+·\s+\S.*$/m.test(screen);
|
|
3288
|
-
return codexFrontend && /(^|\n)\s*[›>]/.test(screen);
|
|
3289
|
-
}
|
|
3290
|
-
// Grok Build 0.2.117 は起動完了後に製品名を消し、model footerだけを残す。
|
|
3291
|
-
// Composerも同じfrontendでmodel名だけが異なるため、両方をvendor UIの根拠にする。
|
|
3292
|
-
// Windows native grok.exe(1.0.4 実測)は入力欄markerを `❯` でなく `>` で描画するため両方を受ける。
|
|
3293
|
-
const grokFrontend = screen.includes("Grok Build") || /\b(?:Grok|Composer)\s+[\w.()-]+/.test(screen);
|
|
3294
|
-
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);
|
|
3295
2228
|
}
|
|
3296
2229
|
// Codex/Claude は実行中に「(esc to interrupt)」を表示する(実機採取)。startup 側の処理
|
|
3297
2230
|
// (MCP initialize 等)が走ったまま composer だけ描画されている画面は入力受付とみなさない。
|
|
@@ -3356,7 +2289,7 @@ function agentSubmitResidueOnScreen(kind, screen, tail) {
|
|
|
3356
2289
|
// 入力欄マーカーは ready 判定と同じ記号を行頭基準で探す。submit 済みの transcript echo は
|
|
3357
2290
|
// マーカー行より上に出るため、最後のマーカー行以降だけを composer 領域として見る。
|
|
3358
2291
|
// grok/composer は Windows native 描画(`>`・実測 1.0.4)も ready 判定と同様に受ける。
|
|
3359
|
-
const markerRe = kind === "codex" ?
|
|
2292
|
+
const markerRe = kind === "codex" ? CODEX_COMPOSER_MARKER_RE : kind === "claude" ? CLAUDE_COMPOSER_MARKER_RE : GROK_COMPOSER_MARKER_RE;
|
|
3360
2293
|
let markerIdx = -1;
|
|
3361
2294
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3362
2295
|
if (markerRe.test(lines[i])) {
|
|
@@ -3544,21 +2477,6 @@ function lineCounts(screen) {
|
|
|
3544
2477
|
}
|
|
3545
2478
|
return counts;
|
|
3546
2479
|
}
|
|
3547
|
-
function grokFooterHasConfiguration(screen, model, effort) {
|
|
3548
|
-
const modelMatch = model?.match(/^grok-(\d+(?:\.\d+)*)$/) ?? null;
|
|
3549
|
-
if (model && !modelMatch)
|
|
3550
|
-
return false;
|
|
3551
|
-
const modelLabel = modelMatch ? `Grok ${modelMatch[1]}` : null;
|
|
3552
|
-
return screen.split("\n").some((line) => {
|
|
3553
|
-
if (!line.includes("·"))
|
|
3554
|
-
return false;
|
|
3555
|
-
if (modelLabel && !line.includes(`${modelLabel} (`))
|
|
3556
|
-
return false;
|
|
3557
|
-
if (effort && !line.includes(`(${effort})`))
|
|
3558
|
-
return false;
|
|
3559
|
-
return modelLabel !== null || effort !== null;
|
|
3560
|
-
});
|
|
3561
|
-
}
|
|
3562
2480
|
async function waitForGrokConfigurationResult(name, before, model, effort, timeoutMs = 3_000) {
|
|
3563
2481
|
const beforeCounts = lineCounts(before);
|
|
3564
2482
|
const footerAlreadyMatched = grokFooterHasConfiguration(before, model, effort);
|
|
@@ -3592,41 +2510,6 @@ function sendMenuChoice(name, choice) {
|
|
|
3592
2510
|
throw new AitermError(`agent設定の選択を送れませんでした: ${sent.stderr.trim() || `code=${sent.code}`}`, 2);
|
|
3593
2511
|
}
|
|
3594
2512
|
}
|
|
3595
|
-
function codexModelChoice(screen, model) {
|
|
3596
|
-
for (const line of screen.slice(screen.lastIndexOf("Select Model and Effort")).split("\n")) {
|
|
3597
|
-
const match = line.match(/^\s*(?:›\s*)?(\d+)\.\s+(\S+)/);
|
|
3598
|
-
if (match?.[2] === model)
|
|
3599
|
-
return match[1];
|
|
3600
|
-
}
|
|
3601
|
-
return null;
|
|
3602
|
-
}
|
|
3603
|
-
function codexEffortChoice(screen, effort) {
|
|
3604
|
-
const labels = {
|
|
3605
|
-
low: /^Low\b/i,
|
|
3606
|
-
medium: /^Medium\b/i,
|
|
3607
|
-
high: /^High\b/i,
|
|
3608
|
-
xhigh: /^Extra high\b/i,
|
|
3609
|
-
max: /^Max\b/i,
|
|
3610
|
-
ultra: /^Ultra\b/i,
|
|
3611
|
-
};
|
|
3612
|
-
const wanted = labels[effort.toLowerCase()];
|
|
3613
|
-
if (!wanted)
|
|
3614
|
-
return null;
|
|
3615
|
-
for (const line of screen.split("\n")) {
|
|
3616
|
-
const match = line.match(/^\s*(?:›\s*)?(\d+)\.\s+(.+?)\s{2,}/);
|
|
3617
|
-
if (match && wanted.test(match[2]))
|
|
3618
|
-
return match[1];
|
|
3619
|
-
}
|
|
3620
|
-
return null;
|
|
3621
|
-
}
|
|
3622
|
-
function codexMoreReasoningChoice(screen) {
|
|
3623
|
-
for (const line of screen.split("\n")) {
|
|
3624
|
-
const match = line.match(/^\s*(?:›\s*)?(\d+)\.\s+More reasoning/);
|
|
3625
|
-
if (match)
|
|
3626
|
-
return match[1];
|
|
3627
|
-
}
|
|
3628
|
-
return null;
|
|
3629
|
-
}
|
|
3630
2513
|
/** 同じ対話sessionを保ったまま、vendor標準の操作でmodel/effortを変更する。 */
|
|
3631
2514
|
export async function configureAgent(name, opts) {
|
|
3632
2515
|
assertSessionName(name);
|
|
@@ -3839,47 +2722,13 @@ function inspectClaudeOperation(meta, operationId, action) {
|
|
|
3839
2722
|
bindAgentVendorSession(meta, done);
|
|
3840
2723
|
writeAgentMetadata(meta);
|
|
3841
2724
|
}
|
|
3842
|
-
const rawOutput = readClaudeResultText(meta, done, operationId);
|
|
2725
|
+
const rawOutput = readClaudeResultText(meta, done, operationId, transcriptUnavailable);
|
|
3843
2726
|
return { ...base, status: "completed", raw_output: rawOutput, reason: null };
|
|
3844
2727
|
}
|
|
3845
2728
|
// ── 対話型エージェント起動(Claude / Codex / Grok Build(Grok) / Grok Build(Composer))──────
|
|
3846
2729
|
// aiterm の永続端末に、指定モデルの対話エージェント TUI を起動する。以後は pty_read で画面を
|
|
3847
2730
|
// 読み、pty_send で操作する=aiterm の対話パラダイムそのもの。モデルはツールごとに固定し、
|
|
3848
2731
|
// reasoning effort は引数で渡す。CLI 未導入環境は明示エラー(動くフリをしない)。
|
|
3849
|
-
const CLAUDE_AUTH_STATUS_TIMEOUT_MS = 5_000;
|
|
3850
|
-
function assertClaudeAuthenticationReady(bin) {
|
|
3851
|
-
const result = spawnAgentControlCommand(bin, ["auth", "status", "--json"], process.cwd(), {
|
|
3852
|
-
encoding: "utf8",
|
|
3853
|
-
timeout: CLAUDE_AUTH_STATUS_TIMEOUT_MS,
|
|
3854
|
-
maxBuffer: 64 * 1024,
|
|
3855
|
-
});
|
|
3856
|
-
let status = null;
|
|
3857
|
-
try {
|
|
3858
|
-
status = JSON.parse((result.stdout ?? "").trim());
|
|
3859
|
-
}
|
|
3860
|
-
catch {
|
|
3861
|
-
status = null;
|
|
3862
|
-
}
|
|
3863
|
-
if (result.error == null &&
|
|
3864
|
-
result.status === 0 &&
|
|
3865
|
-
status !== null &&
|
|
3866
|
-
typeof status === "object" &&
|
|
3867
|
-
!Array.isArray(status) &&
|
|
3868
|
-
status.loggedIn === true) {
|
|
3869
|
-
return;
|
|
3870
|
-
}
|
|
3871
|
-
if (status !== null &&
|
|
3872
|
-
typeof status === "object" &&
|
|
3873
|
-
!Array.isArray(status) &&
|
|
3874
|
-
status.loggedIn === false) {
|
|
3875
|
-
throw new AitermError("Claude Codeの認証を利用できません。sessionは作成していません。" +
|
|
3876
|
-
"通常端末で `claude doctor` を実行し、Keychain/credential storeを直してから一度だけ `claude auth login` を実行してください。" +
|
|
3877
|
-
"aiterm相関付きClaude session内で /login を繰り返さないでください。", 2);
|
|
3878
|
-
}
|
|
3879
|
-
const timedOut = result.error && result.error.code === "ETIMEDOUT";
|
|
3880
|
-
throw new AitermError(`Claude Codeの認証状態を起動前に確認できません${timedOut ? "(5秒でtimeout)" : ""}。sessionは作成していません。` +
|
|
3881
|
-
"`claude auth status --json` と `claude doctor` が成功することを通常端末で確認してください。", 2);
|
|
3882
|
-
}
|
|
3883
2732
|
const THROUGHLINE_HANDOFF_CONTEXT_SCHEMA = "throughline.handoff_context.v1";
|
|
3884
2733
|
const PORTABLE_FORK_MISSION_SEPARATOR = "\n\n---\n\n## Portable fork mission\n\n";
|
|
3885
2734
|
export function composePortableForkPrompt(context, mission) {
|
|
@@ -3922,66 +2771,12 @@ export function vendorLauncherDiagnostic(kind) {
|
|
|
3922
2771
|
return "unverified";
|
|
3923
2772
|
}
|
|
3924
2773
|
}
|
|
3925
|
-
// 単一引用符で安全に包む(' は '\'' で脱出)。send は raw:true で送るため自前で quote する。
|
|
3926
|
-
function shq(s) {
|
|
3927
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
3928
|
-
}
|
|
3929
|
-
// grok CLI はモデル未指定だと端末側 default に従うため、ツール契約として既定 slug を固定する。
|
|
3930
|
-
// codex は既定 slug を持たず端末 config/CLI 既定に委ねる(起動応答で実効値を報告する)。
|
|
3931
|
-
// grok 既定は dotagents 規範(docs/02_models.md: xAI 旗艦 = grok-4.6)に従う。
|
|
3932
|
-
const GROK_MODEL_DEFAULTS = {
|
|
3933
|
-
grok: "grok-4.6",
|
|
3934
|
-
composer: "grok-composer-2.5-fast",
|
|
3935
|
-
};
|
|
3936
|
-
const CLAUDE_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
3937
2774
|
function buildAgentCmd(kind, bin, model, effort, prompt, meta = null) {
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
if (model)
|
|
3944
|
-
parts.push("--model", shq(model));
|
|
3945
|
-
if (effort)
|
|
3946
|
-
parts.push("--effort", shq(effort));
|
|
3947
|
-
}
|
|
3948
|
-
else if (kind === "codex") {
|
|
3949
|
-
// `codex --help` で確認した実在フラグ。read-only 宣言だけはCLI sandboxへ落とし、
|
|
3950
|
-
// launcher自身が実効能力壁を作る。パス説明はCodex CLIに同等のallowlist引数がないため宣言のまま残す。
|
|
3951
|
-
if (meta?.kind === "codex" && meta.write_scope === "read-only")
|
|
3952
|
-
parts.push("--sandbox", "read-only");
|
|
3953
|
-
// model/effort は共有configを書き換えず、CLI引数で明示して起動単位に優先する。
|
|
3954
|
-
if (model)
|
|
3955
|
-
parts.push("-m", shq(model));
|
|
3956
|
-
if (effort)
|
|
3957
|
-
parts.push("-c", `model_reasoning_effort=${shq(effort)}`);
|
|
3958
|
-
if (meta?.kind === "codex" && meta.hook_route === "shared_codex_home") {
|
|
3959
|
-
parts.push("-c", `developer_instructions=${shq(subagentInstruction(meta))}`);
|
|
3960
|
-
}
|
|
3961
|
-
}
|
|
3962
|
-
else {
|
|
3963
|
-
// grok / composer は同じ grok CLI をモデル違いで起動する。
|
|
3964
|
-
parts.push("--no-auto-update");
|
|
3965
|
-
if (meta?.kind === "grok" || meta?.kind === "composer")
|
|
3966
|
-
parts.push("--no-alt-screen");
|
|
3967
|
-
parts.push("--model", shq(model ?? GROK_MODEL_DEFAULTS[kind]));
|
|
3968
|
-
if (effort)
|
|
3969
|
-
parts.push("--reasoning-effort", shq(effort));
|
|
3970
|
-
if ((meta?.kind === "grok" || meta?.kind === "composer") && meta.write_scope === "read-only") {
|
|
3971
|
-
// read-only は sandbox が実効書込み禁止を作るため、MCP ツール許可ダイアログの自動承認を
|
|
3972
|
-
// 付けても能力は増えない。無人 subagent が初回 MCP 使用の許可待ちで停止する実障害への対処。
|
|
3973
|
-
// read-only 以外の launch には付けない=権限拡大しない。
|
|
3974
|
-
parts.push("--sandbox", "read-only", "--always-approve");
|
|
3975
|
-
}
|
|
3976
|
-
if ((meta?.kind === "grok" || meta?.kind === "composer") && meta.hook_route === "shared_grok_home") {
|
|
3977
|
-
parts.push("--session-id", shq(meta.vendor_session_id ?? ""), "--rules", shq(subagentInstruction(meta)));
|
|
3978
|
-
}
|
|
3979
|
-
if ((meta?.kind === "grok" || meta?.kind === "composer") && prompt)
|
|
3980
|
-
parts.push("--verbatim");
|
|
3981
|
-
}
|
|
3982
|
-
if (prompt)
|
|
3983
|
-
parts.push(shq(prompt)); // 初手プロンプト(任意)
|
|
3984
|
-
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);
|
|
3985
2780
|
}
|
|
3986
2781
|
function agentEnvPrefix(meta, sid, envVars = []) {
|
|
3987
2782
|
const inherited = envVars.flatMap((name) => {
|
|
@@ -4005,60 +2800,16 @@ function agentEnvPrefix(meta, sid, envVars = []) {
|
|
|
4005
2800
|
];
|
|
4006
2801
|
if (meta.kind === "claude" || meta.kind === "codex")
|
|
4007
2802
|
return common;
|
|
4008
|
-
|
|
4009
|
-
// vendor は Windows process なので、Windows ドライブパスが正しい形(WSL 形への変換はしない)。
|
|
4010
|
-
return [
|
|
4011
|
-
...(meta.grok_auth_path ? [`GROK_AUTH_PATH=${shq(meta.grok_auth_path)}`] : []),
|
|
4012
|
-
"GROK_DISABLE_AUTOUPDATER=1",
|
|
4013
|
-
...common,
|
|
4014
|
-
];
|
|
2803
|
+
return [...grokEnvTokens(meta), ...common];
|
|
4015
2804
|
})();
|
|
4016
2805
|
return tokens.length ? tokens.join(" ") + " " : "";
|
|
4017
2806
|
}
|
|
4018
|
-
function agentLabel(kind) {
|
|
4019
|
-
return kind === "claude"
|
|
4020
|
-
? "Claude Code"
|
|
4021
|
-
: kind === "composer"
|
|
4022
|
-
? "Grok Build(Composer)"
|
|
4023
|
-
: kind === "grok"
|
|
4024
|
-
? "Grok Build(Grok)"
|
|
4025
|
-
: "Codex";
|
|
4026
|
-
}
|
|
4027
|
-
// 起動応答にモデル/effort の実効値と出所を明示する。codex は端末 config のピン(model /
|
|
4028
|
-
// model_reasoning_effort)が対話子へ波及する構造のため、引数・端末config継承・CLI既定の
|
|
4029
|
-
// どれで起動したかを起動時点で可視化し、実効 effort=ultra は proactive 自動委譲 ON を警告する。
|
|
4030
2807
|
function buildAgentLaunchNote(kind, model, effort, meta) {
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
: `\n能力宣言: write_scope=${JSON.stringify(meta.write_scope)}。パス単位のsandbox allowlistに対応するCLI引数がないため宣言の記録のみ(構造的unsupported)。`;
|
|
4037
|
-
if (kind === "claude") {
|
|
4038
|
-
return `起動設定: model=${model ?? "CLI既定"} effort=${effort ?? "CLI既定"}。${writeScopeNote}`;
|
|
4039
|
-
}
|
|
4040
|
-
if (kind !== "codex") {
|
|
4041
|
-
return (`起動設定: model=${model ?? GROK_MODEL_DEFAULTS[kind]}(${model ? "引数" : "ツール既定"})。` +
|
|
4042
|
-
`effort=${effort ?? "CLI/model既定"}。` + writeScopeNote);
|
|
4043
|
-
}
|
|
4044
|
-
const configPath = meta?.kind === "codex" && meta.codex_home
|
|
4045
|
-
? path.join(meta.codex_home, "config.toml")
|
|
4046
|
-
: path.join(realCodexHome(), "config.toml");
|
|
4047
|
-
const pins = readCodexConfigPins(configPath);
|
|
4048
|
-
const describePin = (arg, pin) => arg
|
|
4049
|
-
? `${arg}(引数)`
|
|
4050
|
-
: pin.present
|
|
4051
|
-
? pin.value
|
|
4052
|
-
? `${pin.value}(端末config継承)`
|
|
4053
|
-
: "端末config継承(値未解析)"
|
|
4054
|
-
: "CLI既定";
|
|
4055
|
-
const effectiveEffort = effort ?? (pins.effort.present ? pins.effort.value : null);
|
|
4056
|
-
const launch = `起動設定: model=${describePin(model, pins.model)} effort=${describePin(effort, pins.effort)}。` +
|
|
4057
|
-
(effectiveEffort === "ultra"
|
|
4058
|
-
? "⚠ effort=ultra は max 推論+proactive 自動委譲 ON(子エージェント自動生成・使用量急増に注意)。"
|
|
4059
|
-
: "");
|
|
4060
|
-
const summary = meta?.kind === "codex" && meta.codex_home ? codexConfigSummary(configPath) : "";
|
|
4061
|
-
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);
|
|
4062
2813
|
}
|
|
4063
2814
|
function claudeLaunchRequestDigest({ sessionName, model, effort, cwd, agentDone, }) {
|
|
4064
2815
|
const canonical = JSON.stringify({
|
|
@@ -4193,9 +2944,7 @@ export function openAgent(kind, opts = {}) {
|
|
|
4193
2944
|
"Windows 版 Grok CLI を導入するか、GROK_BIN に grok.exe の絶対パスを指定してください。", 2), 2);
|
|
4194
2945
|
}
|
|
4195
2946
|
const binForCmd = agentBinForPaneShell(bin);
|
|
4196
|
-
|
|
4197
|
-
// Git Bash の cd はドライブパスを forward slash 形で受けるのが安全。
|
|
4198
|
-
const cwdForCmd = cwd && isWin ? cwd.replace(/\\/g, "/") : cwd;
|
|
2947
|
+
const cwdForCmd = cwd ? paneCwdArgument(cwd) : cwd;
|
|
4199
2948
|
const grokAuthPath = agentDone && (kind === "grok" || kind === "composer") ? resolveAndValidateGrokAuth(realGrokHome()) : null;
|
|
4200
2949
|
if (kind === "grok" || kind === "composer") {
|
|
4201
2950
|
const requestedModel = model ?? (kind === "composer" ? GROK_MODEL_DEFAULTS.composer : null);
|