aiterm-mcp 0.58.4 → 0.59.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -1
- package/README.ja.md +15 -5
- package/README.md +14 -4
- package/dist/agent-shared.js +48 -1
- package/dist/core.js +868 -106
- package/dist/cursor-parent-receive.js +2 -1
- package/dist/fast-mode.js +22 -0
- package/dist/harnesses/claude.js +137 -19
- package/dist/harnesses/codex.js +126 -22
- package/dist/harnesses/cursor.js +96 -12
- package/dist/harnesses/grok.js +159 -2
- package/dist/index.js +41 -7
- package/dist/live-node.js +17 -0
- package/dist/model-catalog.js +1 -0
- package/dist/parent-delivery-cli.js +3 -2
- package/dist/runtime-error-report.js +2 -1
- package/dist/runtime-error-store.js +3 -2
- package/dist/user-config.js +138 -0
- package/docs/DESIGN.md +11 -0
- package/docs/RELEASE.md +5 -0
- package/package.json +2 -2
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import * as steer from "aiterm-steer-delivery";
|
|
5
5
|
import { cursorHookRoots } from "./cursor-parent-receiver.js";
|
|
6
|
-
|
|
6
|
+
import { liveNodeExecutable } from "./live-node.js";
|
|
7
|
+
export function cursorReceiveProcess(deliveryId, executable = liveNodeExecutable()) {
|
|
7
8
|
return steer.cursorReceiveProcess(fileURLToPath(import.meta.url), deliveryId, executable);
|
|
8
9
|
}
|
|
9
10
|
/** 背景processの起動情報を、親のshellへ書ける1行にする(Windowsの既定shellはPowerShell)。 */
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// ファスト(高速の応答)の切り替えの、harness中立の部分(ADR 0114)。
|
|
2
|
+
// harnessごとの切り替え方は src/harnesses/ と core が持ち、ここは断りの符号と、起動時の扱いだけを持つ。
|
|
3
|
+
import { AitermError } from "./errors.js";
|
|
4
|
+
export class FastModeError extends AitermError {
|
|
5
|
+
fast_code;
|
|
6
|
+
constructor(code, detail) {
|
|
7
|
+
super(`${code}: ${detail}`, 2);
|
|
8
|
+
this.name = "FastModeError";
|
|
9
|
+
this.fast_code = code;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* 起動時にfastへ出来なかった時、起動を断らずに通常の速度で起こす符号か。
|
|
14
|
+
* アカウントの都合(credits切れ等)や確かめられない時に起動まで断ると、呼び手の席が起きず、届いた文が落ちる。
|
|
15
|
+
* 設定の誤り(対応していないmodel、IDとの食い違い)は、起動の前に断る。
|
|
16
|
+
*/
|
|
17
|
+
export function launchContinuesWithoutFast(code) {
|
|
18
|
+
return code === "FAST_MODE_UNAVAILABLE" || code === "FAST_MODE_UNCONFIRMED";
|
|
19
|
+
}
|
|
20
|
+
export function fastModeFailure(error) {
|
|
21
|
+
return error instanceof FastModeError ? { code: error.fast_code, message: error.message } : null;
|
|
22
|
+
}
|
package/dist/harnesses/claude.js
CHANGED
|
@@ -11,7 +11,7 @@ import { AitermError } from "../errors.js";
|
|
|
11
11
|
import { modeBitsWorldAccessible } from "../tmux-runtime.js";
|
|
12
12
|
import { runAgentProtocolCommand, spawnAgentControlCommand } from "../agent-resolver.js";
|
|
13
13
|
import { catalogInvalid, catalogUnavailable, checkedCatalog, findJsonLine, processSummary } from "../model-catalog.js";
|
|
14
|
-
import { currentUid, writeJson0600, shq, subagentInstruction, writeScopeLaunchNote, agentEventPath, createEmpty0600, writeAgentMetadata, agentLineageFields, assertSessionName, agentsDir, LAUNCH_ID_RE, AGENT_EVENT_TAIL_BYTES, readFileRange, } from "../agent-shared.js";
|
|
14
|
+
import { currentUid, writeJson0600, shq, subagentInstruction, AGENT_DEFAULT_PREFACE, writeScopeLaunchNote, agentEventPath, createEmpty0600, writeAgentMetadata, agentLineageFields, assertSessionName, agentsDir, LAUNCH_ID_RE, AGENT_EVENT_TAIL_BYTES, readFileRange, } from "../agent-shared.js";
|
|
15
15
|
export const OPERATION_ID_RE = /^sha256:[0-9a-f]{64}$/;
|
|
16
16
|
export const CLAUDE_RESULT_MAX_BYTES = 4 * 1024 * 1024;
|
|
17
17
|
export const CLAUDE_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
@@ -72,6 +72,9 @@ export function createClaudeCorrelationSettings(name, launchId, model, effort) {
|
|
|
72
72
|
writeJson0600(settings, {
|
|
73
73
|
...(model ? { model } : {}),
|
|
74
74
|
...(effort ? { effortLevel: effort } : {}),
|
|
75
|
+
// 席は必ず通常の速度で始める(ADR 0114)。--settingsは利用者の設定より強いので、共有の設定にfastModeが残っていても効かない。
|
|
76
|
+
// ここへtrueは入れない: 後で/fast offにしても、機種を替えた時にClaude Codeがこの値を見てfastへ戻す。
|
|
77
|
+
fastMode: false,
|
|
75
78
|
hooks: {
|
|
76
79
|
Stop: [
|
|
77
80
|
{
|
|
@@ -151,29 +154,70 @@ export async function claudeModelChoices(bin, cwd) {
|
|
|
151
154
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "aiterm-claude-models-"));
|
|
152
155
|
try {
|
|
153
156
|
const settings = path.join(dir, "settings.json");
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
157
|
+
// fastMode: true を付けて聞くと、応答の fast_mode_disabled_reason が「このアカウントで使えない訳」になる
|
|
158
|
+
// (付けないと sdk_opt_in_required が返るだけ)。promptは送らないので、fastで流れる番は無い。
|
|
159
|
+
fs.writeFileSync(settings, JSON.stringify({ disableAllHooks: true, fastMode: true }), { mode: 0o600 });
|
|
160
|
+
// Claude Codeは、起きてすぐは「fastの可否を確かめている最中(pending)」と答える事がある(その端末で初めて聞いた時。Windowsで見た)。
|
|
161
|
+
// 少し置いて聞き直すと、アカウントの答え(使える、または使えない訳)が返る。
|
|
162
|
+
for (let attempt = 0;; attempt++) {
|
|
163
|
+
const requestId = `aiterm-models-${randomUUID()}`;
|
|
164
|
+
const request = { type: "control_request", request_id: requestId, request: { subtype: "initialize" } };
|
|
165
|
+
const result = await runAgentProtocolCommand(bin, [
|
|
166
|
+
"-p", "--output-format", "stream-json", "--verbose", "--input-format", "stream-json",
|
|
167
|
+
"--no-session-persistence", "--strict-mcp-config", "--settings", settings,
|
|
168
|
+
], {
|
|
169
|
+
cwd,
|
|
170
|
+
env: process.env,
|
|
171
|
+
input: JSON.stringify(request) + "\n",
|
|
172
|
+
until: requestId,
|
|
173
|
+
timeout: CLAUDE_MODELS_TIMEOUT_MS,
|
|
174
|
+
maxBuffer: CLAUDE_MODELS_MAX_BYTES,
|
|
175
|
+
});
|
|
176
|
+
if (result.error || result.status !== 0) {
|
|
177
|
+
throw catalogUnavailable("Claude Code", result.error?.message || result.stderr?.trim() || `exit=${result.status ?? "unknown"}`);
|
|
178
|
+
}
|
|
179
|
+
if (attempt < CLAUDE_FAST_PENDING_RETRIES && claudeFastStatePending(result.stdout, requestId)) {
|
|
180
|
+
await new Promise((resolve) => setTimeout(resolve, CLAUDE_FAST_PENDING_WAIT_MS));
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
return claudeCatalogFromInitialize(result.stdout, requestId, processSummary(result));
|
|
170
184
|
}
|
|
171
|
-
return claudeCatalogFromInitialize(result.stdout, requestId, processSummary(result));
|
|
172
185
|
}
|
|
173
186
|
finally {
|
|
174
187
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
175
188
|
}
|
|
176
189
|
}
|
|
190
|
+
const CLAUDE_FAST_PENDING_RETRIES = 2;
|
|
191
|
+
const CLAUDE_FAST_PENDING_WAIT_MS = 1_500;
|
|
192
|
+
/** initializeの応答が、fastの可否を「確かめている最中」と答えているか。 */
|
|
193
|
+
export function claudeFastStatePending(stdout, requestId) {
|
|
194
|
+
const response = findJsonLine(stdout, (value) => value?.type === "control_response" && value.response?.request_id === requestId);
|
|
195
|
+
return response?.response?.response?.fast_mode_disabled_reason === "pending";
|
|
196
|
+
}
|
|
197
|
+
// initializeの応答の fast_mode_disabled_reason と、Claude Codeが画面に出す文(2.1.296)。
|
|
198
|
+
const CLAUDE_FAST_DISABLED_TEXT = {
|
|
199
|
+
extra_usage_disabled: "Fast mode requires usage credits",
|
|
200
|
+
free: "Fast mode requires a paid subscription",
|
|
201
|
+
preference: "Fast mode has been disabled by your organization",
|
|
202
|
+
network_error: "Fast mode unavailable due to network connectivity issues",
|
|
203
|
+
not_first_party: "Fast mode is only available when using the Anthropic API directly",
|
|
204
|
+
disabled_by_env: "Fast mode is not available",
|
|
205
|
+
model_not_allowed: "Opus is not in your organization's allowed models",
|
|
206
|
+
unknown: "Fast mode is currently unavailable",
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* initializeの応答の理由から、fastを使えない時の断りを作る。
|
|
210
|
+
* 使える時(理由なし)と、まだ分からない時(pending)・聞き方の都合(sdk_opt_in_required)はnull。
|
|
211
|
+
*/
|
|
212
|
+
export function claudeFastUnavailable(reason) {
|
|
213
|
+
if (typeof reason !== "string" || !reason || reason === "pending" || reason === "sdk_opt_in_required")
|
|
214
|
+
return null;
|
|
215
|
+
const text = CLAUDE_FAST_DISABLED_TEXT[reason];
|
|
216
|
+
return {
|
|
217
|
+
code: "FAST_MODE_UNAVAILABLE",
|
|
218
|
+
message: `FAST_MODE_UNAVAILABLE: Claude Codeが、このアカウントではファストを使えないと答えました(${reason}${text ? `: ${text}` : ""})。`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
177
221
|
/** stream-jsonのcontrol_response(initialize)からmodel候補を作る。 */
|
|
178
222
|
export function claudeCatalogFromInitialize(stdout, requestId, summary = "") {
|
|
179
223
|
const response = findJsonLine(stdout, (value) => value?.type === "control_response" && value.response?.request_id === requestId);
|
|
@@ -198,14 +242,26 @@ export function claudeCatalogFromInitialize(stdout, requestId, summary = "") {
|
|
|
198
242
|
efforts: levels.length > 0 ? [...levels, "ultracode"] : [],
|
|
199
243
|
default_effort: null,
|
|
200
244
|
hidden: false,
|
|
245
|
+
fast: model.supportsFastMode === true,
|
|
201
246
|
};
|
|
202
247
|
});
|
|
248
|
+
// fast対応はModelInfoのsupportsFastMode。一覧の値(opus等)に加えて、実体のID(resolvedModel)でも引けるようにする。
|
|
249
|
+
const fastIds = {};
|
|
250
|
+
for (const model of models) {
|
|
251
|
+
if (model?.supportsFastMode !== true)
|
|
252
|
+
continue;
|
|
253
|
+
fastIds[model.value] = model.value;
|
|
254
|
+
if (typeof model.resolvedModel === "string" && model.resolvedModel)
|
|
255
|
+
fastIds[model.resolvedModel] ??= model.resolvedModel;
|
|
256
|
+
}
|
|
203
257
|
return checkedCatalog("Claude Code", {
|
|
204
258
|
source: "claude stream-json control_request initialize (models)",
|
|
205
259
|
harness_version: null,
|
|
206
260
|
default_model: choices.some((choice) => choice.id === "default") ? "default" : null,
|
|
207
261
|
adapter_efforts: choices.some((choice) => choice.efforts.includes("ultracode")) ? { ultracode: CLAUDE_ULTRACODE_NOTE } : {},
|
|
208
262
|
models: choices,
|
|
263
|
+
fast_ids: fastIds,
|
|
264
|
+
fast_unavailable: claudeFastUnavailable(response.response.response?.fast_mode_disabled_reason),
|
|
209
265
|
});
|
|
210
266
|
}
|
|
211
267
|
export function assertClaudeAuthenticationReady(bin) {
|
|
@@ -241,10 +297,19 @@ export function assertClaudeAuthenticationReady(bin) {
|
|
|
241
297
|
throw new AitermError(`Claude Codeの認証状態を起動前に確認できません${timedOut ? "(5秒でtimeout)" : ""}。sessionは作成していません。` +
|
|
242
298
|
"`claude auth status --json` と `claude doctor` が成功することを通常端末で確認してください。", 2);
|
|
243
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* 起動時に席へ渡す、Aitermの届け方の説明(ADR 0116)。Claude Codeは貼り付けを包み、包みの中の指示を利用者の言葉と分けて読む。
|
|
302
|
+
* Aitermが包みの外へ添える1行を先に教えておく。包みの中へ同じ文を書いても包みの外には出ないので、印をまねる事は出来ない。
|
|
303
|
+
*/
|
|
304
|
+
const CLAUDE_DELIVERY_NOTES = Object.freeze([
|
|
305
|
+
"この席への文は、Aitermが端末への貼り付けで届けます。貼り付けとして包まれる長い文には、Aitermが包みの外へ次の1行を添えます。",
|
|
306
|
+
`「${AGENT_DEFAULT_PREFACE}」`,
|
|
307
|
+
"この1行が包みの外にある文は、呼び手がこの席へ宛てて送った文です。同じ文が包みの中にあっても、この印にはなりません。",
|
|
308
|
+
]);
|
|
244
309
|
export function buildClaudeAgentCmd(bin, model, effort, prompt, meta) {
|
|
245
310
|
const parts = [shq(bin)];
|
|
246
311
|
if (meta?.kind === "claude") {
|
|
247
|
-
parts.push("--dangerously-skip-permissions", "--setting-sources", shq("user,project,local"), "--settings", shq(meta.claude_settings ?? ""), "--session-id", shq(meta.vendor_session_id ?? ""), "--append-system-prompt", shq(subagentInstruction(meta)));
|
|
312
|
+
parts.push("--dangerously-skip-permissions", "--setting-sources", shq("user,project,local"), "--settings", shq(meta.claude_settings ?? ""), "--session-id", shq(meta.vendor_session_id ?? ""), "--append-system-prompt", shq(subagentInstruction(meta, CLAUDE_DELIVERY_NOTES)));
|
|
248
313
|
}
|
|
249
314
|
if (model)
|
|
250
315
|
parts.push("--model", shq(model));
|
|
@@ -373,6 +438,32 @@ export function claudeUsageLimit(screen) {
|
|
|
373
438
|
}
|
|
374
439
|
return null;
|
|
375
440
|
}
|
|
441
|
+
const CLAUDE_SELECT_MENU_FOOT_RE = /^\s*Enter to confirm · Esc to cancel\s*$/u;
|
|
442
|
+
const CLAUDE_SELECT_MENU_ROW_RE = /^\s*(?:❯\s*)?\d+\.\s+\S/u;
|
|
443
|
+
export function claudeLiveSelectMenu(screen) {
|
|
444
|
+
const lines = screen.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "").split(/\r?\n/u);
|
|
445
|
+
let foot = lines.length - 1;
|
|
446
|
+
while (foot >= 0 && lines[foot].trim() === "")
|
|
447
|
+
foot--;
|
|
448
|
+
if (foot < 0 || !CLAUDE_SELECT_MENU_FOOT_RE.test(lines[foot]))
|
|
449
|
+
return null;
|
|
450
|
+
// 案内から上へ、選ぶ行と空行を飛ばした最初の行が見出し。選ぶ行が1つも無い物は選ぶ画面と数えない。
|
|
451
|
+
let rows = 0;
|
|
452
|
+
let head = foot - 1;
|
|
453
|
+
for (; head >= 0; head--) {
|
|
454
|
+
if (lines[head].trim() === "")
|
|
455
|
+
continue;
|
|
456
|
+
if (!CLAUDE_SELECT_MENU_ROW_RE.test(lines[head]))
|
|
457
|
+
break;
|
|
458
|
+
rows++;
|
|
459
|
+
}
|
|
460
|
+
if (rows === 0)
|
|
461
|
+
return null;
|
|
462
|
+
const heading = head >= 0 ? lines[head].trim() : null;
|
|
463
|
+
const rateLimit = heading === "What do you want to do?"
|
|
464
|
+
&& lines.slice(head + 1, foot).some((line) => /\bStop and wait for limit to reset\b/u.test(line));
|
|
465
|
+
return { kind: rateLimit ? "rate_limit_options" : "unknown", heading };
|
|
466
|
+
}
|
|
376
467
|
function lastLineIndex(lines, re) {
|
|
377
468
|
for (let i = lines.length - 1; i >= 0; i--)
|
|
378
469
|
if (re.test(lines[i]))
|
|
@@ -615,3 +706,30 @@ export function claudeAuthPane(screen, onboarding = false) {
|
|
|
615
706
|
message: complete ? null : input ? "Claude Codeの公式画面で入力または初回案内の選択を完了してください。" : null,
|
|
616
707
|
onboarding_complete: complete };
|
|
617
708
|
}
|
|
709
|
+
/** `/fast on|off` を送る前の画面と後の画面から、増えた結果の行を読む。まだ出ていなければnull。 */
|
|
710
|
+
export function claudeFastResult(before, after) {
|
|
711
|
+
const count = (screen, test) => screen.split("\n").filter((line) => test.test(line)).length;
|
|
712
|
+
const kinds = [
|
|
713
|
+
["unavailable", /Fast mode unavailable:|Fast mode is (?:not available|only available|currently unavailable)|Fast mode requires/],
|
|
714
|
+
["unchanged", /Fast mode (?:was not |unchanged)|fast mode may still have been/],
|
|
715
|
+
["on", /Fast mode ON/],
|
|
716
|
+
["off", /Fast mode OFF/],
|
|
717
|
+
];
|
|
718
|
+
for (const [state, test] of kinds) {
|
|
719
|
+
if (count(after, test) <= count(before, test))
|
|
720
|
+
continue;
|
|
721
|
+
const lines = after.split("\n");
|
|
722
|
+
let index = lines.length - 1;
|
|
723
|
+
while (index > 0 && !test.test(lines[index]))
|
|
724
|
+
index--;
|
|
725
|
+
// 使えない訳は次の行へ折り返す事がある(「… /usage-credits to」「turn them on」)。続きの行を1つだけ足す。
|
|
726
|
+
const next = lines[index + 1]?.trim() ?? "";
|
|
727
|
+
const tail = state === "unavailable" && next && !/^[❯>─╭╰│]/u.test(next) ? ` ${next}` : "";
|
|
728
|
+
return { state, line: `${lines[index].replace(/^\s*⎿\s*/u, "").trim()}${tail}` };
|
|
729
|
+
}
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
/** 入力欄の近くに、ファストが入っている印(↯)が出ているか。 */
|
|
733
|
+
export function claudeFastIndicatorShown(screen) {
|
|
734
|
+
return screen.split("\n").slice(-8).some((line) => line.includes("\u21AF"));
|
|
735
|
+
}
|
package/dist/harnesses/codex.js
CHANGED
|
@@ -11,7 +11,7 @@ import { spawnAgentControlCommand } from "../agent-resolver.js";
|
|
|
11
11
|
import * as steer from "aiterm-steer-delivery";
|
|
12
12
|
import { AITERM_PROFILE } from "../steer-profile.js";
|
|
13
13
|
import { catalogInvalid, catalogUnavailable, checkedCatalog } from "../model-catalog.js";
|
|
14
|
-
import { shq, subagentInstruction, writeScopeLaunchNote, safeStatSize, readFileRange, sleep, agentMetadataPath, writeAgentMetadata, agentEventPath, createEmpty0600, agentLineageFields, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, AGENT_EVENT_TAIL_BYTES, AGENT_TURN_BOUNDARY_TAIL_BYTES, readTailLinesNewestFirst, CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES, agentHarness, } from "../agent-shared.js";
|
|
14
|
+
import { shq, subagentInstruction, writeScopeLaunchNote, safeStatSize, readFileRange, sleep, agentMetadataPath, writeAgentMetadata, agentEventPath, createEmpty0600, agentLineageFields, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, AGENT_EVENT_TAIL_BYTES, AGENT_TURN_BOUNDARY_TAIL_BYTES, readTailLinesNewestFirst, scanLinesNewestFirst, CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES, agentHarness, } from "../agent-shared.js";
|
|
15
15
|
export function realCodexHome() {
|
|
16
16
|
return process.env.CODEX_HOME || path.join(process.env.HOME ?? os.homedir(), ".codex");
|
|
17
17
|
}
|
|
@@ -21,7 +21,7 @@ export function readCodexConfigPins(configPath) {
|
|
|
21
21
|
body = fs.readFileSync(configPath, "utf8");
|
|
22
22
|
}
|
|
23
23
|
catch {
|
|
24
|
-
return { model: { present: false, value: null }, effort: { present: false, value: null } };
|
|
24
|
+
return { model: { present: false, value: null }, effort: { present: false, value: null }, service_tier: { present: false, value: null } };
|
|
25
25
|
}
|
|
26
26
|
const rows = body.split(/\r?\n/);
|
|
27
27
|
let firstTable = rows.findIndex((l) => /^\s*\[/.test(l));
|
|
@@ -37,7 +37,7 @@ export function readCodexConfigPins(configPath) {
|
|
|
37
37
|
}
|
|
38
38
|
return { present: false, value: null };
|
|
39
39
|
};
|
|
40
|
-
return { model: pick("model"), effort: pick("model_reasoning_effort") };
|
|
40
|
+
return { model: pick("model"), effort: pick("model_reasoning_effort"), service_tier: pick("service_tier") };
|
|
41
41
|
}
|
|
42
42
|
export function codexConfigSummary(configPath) {
|
|
43
43
|
let body;
|
|
@@ -383,12 +383,31 @@ export function codexUsageLimit(record) {
|
|
|
383
383
|
}
|
|
384
384
|
// 完了待ちが、会話の切り替えを確かめる間隔。送った文はすぐ記録へ書かれるので、普通のturnはここへ来ない。
|
|
385
385
|
const CODEX_SWITCH_CHECK_MS = 3_000;
|
|
386
|
+
/** 1行がturnの境界ならその境界、境界でなければundefined、境界の語を含むのに読めない行ならnull。 */
|
|
387
|
+
function codexTurnBoundaryLine(line) {
|
|
388
|
+
if (!line.includes("task_started") && !line.includes("task_complete") && !line.includes("turn_aborted"))
|
|
389
|
+
return undefined;
|
|
390
|
+
let record;
|
|
391
|
+
try {
|
|
392
|
+
record = JSON.parse(line);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
if (record?.type !== "event_msg")
|
|
398
|
+
return undefined;
|
|
399
|
+
const type = record?.payload?.type;
|
|
400
|
+
return type === "task_started" || type === "task_complete" || type === "turn_aborted"
|
|
401
|
+
? { type, turn_id: record.payload.turn_id, reason: record.payload.reason } : undefined;
|
|
402
|
+
}
|
|
386
403
|
/**
|
|
387
404
|
* この起動のroot rolloutの、末尾から見て最初のturnの境界。
|
|
388
405
|
* Codexはturnの開始でtask_started、終わりでtask_complete、止めた時はturn_aborted(reasonはinterrupted)を書く(0.160.1の実記録)。
|
|
389
406
|
* rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はnull。
|
|
407
|
+
* 読む範囲は、`reach`が"tail"なら末尾の1MiB(終わった番の境界は末尾の近くにある。見回りで何度も呼ぶ所はこちら)、
|
|
408
|
+
* "whole"なら境界に当たるまで先頭へさかのぼる(動いている番の開始は、番が長いと末尾の1MiBより前にある)。
|
|
390
409
|
*/
|
|
391
|
-
function codexLatestTurnBoundary(meta) {
|
|
410
|
+
function codexLatestTurnBoundary(meta, reach) {
|
|
392
411
|
let transcript;
|
|
393
412
|
try {
|
|
394
413
|
transcript = codexRootTranscript(meta);
|
|
@@ -398,24 +417,15 @@ function codexLatestTurnBoundary(meta) {
|
|
|
398
417
|
}
|
|
399
418
|
if (!transcript)
|
|
400
419
|
return null;
|
|
420
|
+
if (reach === "whole")
|
|
421
|
+
return scanLinesNewestFirst(transcript, codexTurnBoundaryLine, AGENT_TURN_BOUNDARY_TAIL_BYTES) ?? null;
|
|
401
422
|
const lines = readTailLinesNewestFirst(transcript, AGENT_TURN_BOUNDARY_TAIL_BYTES);
|
|
402
423
|
if (!lines)
|
|
403
424
|
return null;
|
|
404
425
|
for (const line of lines) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
try {
|
|
409
|
-
record = JSON.parse(line);
|
|
410
|
-
}
|
|
411
|
-
catch {
|
|
412
|
-
return null;
|
|
413
|
-
}
|
|
414
|
-
if (record?.type !== "event_msg")
|
|
415
|
-
continue;
|
|
416
|
-
const type = record?.payload?.type;
|
|
417
|
-
if (type === "task_started" || type === "task_complete" || type === "turn_aborted")
|
|
418
|
-
return { type, turn_id: record.payload.turn_id, reason: record.payload.reason };
|
|
426
|
+
const boundary = codexTurnBoundaryLine(line);
|
|
427
|
+
if (boundary !== undefined)
|
|
428
|
+
return boundary;
|
|
419
429
|
}
|
|
420
430
|
return null;
|
|
421
431
|
}
|
|
@@ -425,7 +435,7 @@ function codexLatestTurnBoundary(meta) {
|
|
|
425
435
|
* rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はfalse(分からない時は「終わった」と数えない)。
|
|
426
436
|
*/
|
|
427
437
|
export function codexTurnSettled(meta) {
|
|
428
|
-
const boundary = codexLatestTurnBoundary(meta);
|
|
438
|
+
const boundary = codexLatestTurnBoundary(meta, "tail");
|
|
429
439
|
if (boundary?.type === "task_complete")
|
|
430
440
|
return typeof boundary.turn_id === "string" && boundary.turn_id !== "";
|
|
431
441
|
// 見たのはEscで止めた時の形だけ。他の理由(別のturnへの置き換え等)は、次のturnが続くかも知れないので数えない。
|
|
@@ -433,11 +443,13 @@ export function codexTurnSettled(meta) {
|
|
|
433
443
|
}
|
|
434
444
|
/**
|
|
435
445
|
* この起動のroot rolloutの上で動いているturnのID。末尾から見て最初の境界がtask_startedの時だけ返す。
|
|
436
|
-
* rollout
|
|
446
|
+
* rolloutが無い、この起動に結び付かない、書いている途中、境界が1つも無い時はnull(分からない時は「動いている」と数えない)。
|
|
447
|
+
* 開始の印は、境界に当たるまで記録をさかのぼって探す。末尾の1MiBだけを読むと、長い番(開始から1MiBより多く書いた番)を
|
|
448
|
+
* 動いていると読めず、回答を流している間に届いた文が、新しいturnの道で入力待ちを30秒待って断られる(実被弾 2026-10-13)。
|
|
437
449
|
* Codexは、processが生きている間、turnの終わりを必ず書く。書かずに終わるのは落ちた時なので、呼ぶ側はprocessの生死も見る。
|
|
438
450
|
*/
|
|
439
451
|
export function codexRunningTurn(meta) {
|
|
440
|
-
const boundary = codexLatestTurnBoundary(meta);
|
|
452
|
+
const boundary = codexLatestTurnBoundary(meta, "whole");
|
|
441
453
|
return boundary?.type === "task_started" && typeof boundary.turn_id === "string" && boundary.turn_id !== "" ? boundary.turn_id : null;
|
|
442
454
|
}
|
|
443
455
|
export function latestCodexCompletion(meta, readTranscriptLines) {
|
|
@@ -596,6 +608,10 @@ export function buildCodexAgentCmd(bin, model, effort, prompt, meta) {
|
|
|
596
608
|
parts.push("--sandbox", "read-only");
|
|
597
609
|
if (meta?.kind === "codex")
|
|
598
610
|
parts.push("-c", "check_for_update_on_startup=false");
|
|
611
|
+
// 席は必ず通常の速度で始める(ADR 0114)。何も渡さないと、利用者のconfigの値や、modelの既定の段(Fastのmodelがある)で流れる。
|
|
612
|
+
// ここへfastは渡さない: 後で/fastで戻しても、番ごとの補助の問い合わせが起動時の値のまま残る(0.162.1で確かめた)。
|
|
613
|
+
if (meta?.kind === "codex")
|
|
614
|
+
parts.push("-c", "service_tier=default");
|
|
599
615
|
// model/effort は共有configを書き換えず、CLI引数で明示して起動単位に優先する。
|
|
600
616
|
if (model)
|
|
601
617
|
parts.push("-m", shq(model));
|
|
@@ -828,7 +844,8 @@ export function codexLaunchBlockingDialog(screen) {
|
|
|
828
844
|
export function codexModelChoice(screen, model) {
|
|
829
845
|
for (const line of screen.slice(screen.lastIndexOf("Select Model and Effort")).split("\n")) {
|
|
830
846
|
const match = line.match(/^\s*(?:›\s*)?(\d+)\.\s+(\S+)/);
|
|
831
|
-
|
|
847
|
+
// 選ぶ画面は表示名(GPT-6.1-Sol)で並ぶ。一覧のID(gpt-6.1-sol)とは大文字と小文字だけが違う(0.162.1)。
|
|
848
|
+
if (match?.[2].toLowerCase() === model.toLowerCase())
|
|
832
849
|
return match[1];
|
|
833
850
|
}
|
|
834
851
|
return null;
|
|
@@ -1007,6 +1024,11 @@ export async function codexModelChoices(bin, includeHidden) {
|
|
|
1007
1024
|
}
|
|
1008
1025
|
return codexCatalogFromPages(pages);
|
|
1009
1026
|
}
|
|
1027
|
+
/** CodexのTUIの `/fast` と同じ見方: modelの速度の段(serviceTiers)に、名前が Fast の段がある。 */
|
|
1028
|
+
function codexModelHasFastTier(model) {
|
|
1029
|
+
return Array.isArray(model?.serviceTiers)
|
|
1030
|
+
&& model.serviceTiers.some((tier) => typeof tier?.name === "string" && tier.name.toLowerCase() === "fast");
|
|
1031
|
+
}
|
|
1010
1032
|
/** `model/list` の応答(全ページ)からmodel候補を作る。 */
|
|
1011
1033
|
export function codexCatalogFromPages(pages) {
|
|
1012
1034
|
let defaultModel = null;
|
|
@@ -1031,6 +1053,7 @@ export function codexCatalogFromPages(pages) {
|
|
|
1031
1053
|
efforts,
|
|
1032
1054
|
default_effort: typeof model.defaultReasoningEffort === "string" ? model.defaultReasoningEffort : null,
|
|
1033
1055
|
hidden: model.hidden === true,
|
|
1056
|
+
fast: codexModelHasFastTier(model),
|
|
1034
1057
|
};
|
|
1035
1058
|
});
|
|
1036
1059
|
});
|
|
@@ -1040,6 +1063,7 @@ export function codexCatalogFromPages(pages) {
|
|
|
1040
1063
|
default_model: defaultModel,
|
|
1041
1064
|
adapter_efforts: {},
|
|
1042
1065
|
models,
|
|
1066
|
+
fast_ids: Object.fromEntries(models.filter((model) => model.fast).map((model) => [model.id, model.id])),
|
|
1043
1067
|
});
|
|
1044
1068
|
}
|
|
1045
1069
|
export function codexAuthPlan() { return { args: ["login", "--device-auth"], env: [] }; }
|
|
@@ -1082,3 +1106,83 @@ export function codexAuthPane(screen) {
|
|
|
1082
1106
|
return { url: authUrl(screen, ["auth.openai.com", "auth0.openai.com", "login.openai.com"]),
|
|
1083
1107
|
user_code: authUserCode(screen), input_required: false, message: null };
|
|
1084
1108
|
}
|
|
1109
|
+
// ---- ファスト(速度の段)。CodexのTUIの `/fast` は入り切りの交互で、切り替えた後に「Service tier set to …」を1行出す。
|
|
1110
|
+
// 足元にも /status にも今の段は出ない(0.162.1)。
|
|
1111
|
+
/** 画面にある「Service tier set to …」の行を、上から順に。priority は fast、default は通常の速度。 */
|
|
1112
|
+
export function codexServiceTierNotices(screen) {
|
|
1113
|
+
return screen.split("\n").flatMap((line) => {
|
|
1114
|
+
const match = line.match(/^\s*[•◦]?\s*Service tier (?:set to (\S+)|cleared)\s*$/);
|
|
1115
|
+
if (!match)
|
|
1116
|
+
return [];
|
|
1117
|
+
return [match[1] === "priority" || match[1] === "fast" ? "fast" : "standard"];
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
/**
|
|
1121
|
+
* 足元のmodelの表示(「GPT-5.6-Sol xhigh fast · ~/proj」)に fast と出ているか。出ていればtrue。
|
|
1122
|
+
* 出ていない時はnull: 通常の速度か、足元に段を出さない構成(独自のprovider等)かを、画面からは区別できない。
|
|
1123
|
+
*/
|
|
1124
|
+
export function codexFooterShowsFast(screen) {
|
|
1125
|
+
return screen.split("\n").slice(-6).some((line) => /^\s*\S+\s+(?:default|none|minimal|low|medium|high|xhigh|max|ultra)\s+fast\s+·\s/.test(line)) ? true : null;
|
|
1126
|
+
}
|
|
1127
|
+
/** 入力欄へ `/fast` と打った時に、命令の候補として `/fast` が出ているか(出ていない時にEnterを押すと、文として送られる)。 */
|
|
1128
|
+
export function codexFastCommandOffered(screen) {
|
|
1129
|
+
return screen.split("\n").slice(-14).some((line) => /^\s*›?\s*\/fast\s{2,}\S/.test(line));
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* 利用者のconfig.tomlの最上位の項目を、公式の口(App Serverの config/batchWrite)で元の値へ戻す。
|
|
1133
|
+
* `/fast` は service_tier を、`/model` の選択は model と model_reasoning_effort を、利用者の既定として保存する。
|
|
1134
|
+
* 戻しても、切り替えた席の中の値は保たれる(0.162.1で確かめた)。
|
|
1135
|
+
*/
|
|
1136
|
+
export async function restoreCodexDefaults(bin, codexHome, before) {
|
|
1137
|
+
// 読めない書き方の値(表や複数行)は、元の値が分からないので触らない。
|
|
1138
|
+
const edits = Object.entries(before).filter(([, pin]) => pin && !(pin.present && pin.value === null))
|
|
1139
|
+
.map(([keyPath, pin]) => ({ keyPath, value: pin.present ? pin.value : null, mergeStrategy: "replace" }));
|
|
1140
|
+
if (edits.length === 0)
|
|
1141
|
+
return;
|
|
1142
|
+
const parent = { thread_id: "00000000-0000-4000-8000-000000000000", codex_home: path.resolve(codexHome) };
|
|
1143
|
+
await steer.withCodexReceiver(AITERM_PROFILE, parent, async (request) => {
|
|
1144
|
+
await request("config/batchWrite", { edits });
|
|
1145
|
+
}, { executable: bin, timeout_ms: CODEX_MODELS_TIMEOUT_MS });
|
|
1146
|
+
}
|
|
1147
|
+
/** 公式の口で戻せなかった時のための戻し。config.tomlの最上位の、その項目の1行だけを、元の値へ直す。 */
|
|
1148
|
+
export function restoreCodexDefaultInFile(configPath, key, before) {
|
|
1149
|
+
if (before.present && before.value === null)
|
|
1150
|
+
return;
|
|
1151
|
+
let body;
|
|
1152
|
+
try {
|
|
1153
|
+
body = fs.readFileSync(configPath, "utf8");
|
|
1154
|
+
}
|
|
1155
|
+
catch {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const eol = body.includes("\r\n") ? "\r\n" : "\n";
|
|
1159
|
+
const rows = body.split(/\r?\n/);
|
|
1160
|
+
let firstTable = rows.findIndex((row) => /^\s*\[/.test(row));
|
|
1161
|
+
if (firstTable === -1)
|
|
1162
|
+
firstTable = rows.length;
|
|
1163
|
+
const line = rows.slice(0, firstTable).findIndex((row) => new RegExp(`^\\s*${key}\\s*=`).test(row));
|
|
1164
|
+
const wanted = before.present ? `${key} = ${JSON.stringify(before.value)}` : null;
|
|
1165
|
+
if (line === -1 && wanted === null)
|
|
1166
|
+
return;
|
|
1167
|
+
if (line !== -1 && wanted !== null && rows[line].trim() === wanted)
|
|
1168
|
+
return;
|
|
1169
|
+
if (line === -1)
|
|
1170
|
+
rows.splice(firstTable, 0, wanted);
|
|
1171
|
+
else if (wanted === null)
|
|
1172
|
+
rows.splice(line, 1);
|
|
1173
|
+
else
|
|
1174
|
+
rows[line] = wanted;
|
|
1175
|
+
const temp = `${configPath}.aiterm-restore-${process.pid}`;
|
|
1176
|
+
fs.writeFileSync(temp, rows.join(eol), { mode: fs.statSync(configPath).mode & 0o777 });
|
|
1177
|
+
try {
|
|
1178
|
+
fs.renameSync(temp, configPath);
|
|
1179
|
+
}
|
|
1180
|
+
catch (error) {
|
|
1181
|
+
// Windowsでは、ほかのprocessがfileを開いている間、置き換えに失敗する事がある。途中のfileを残さず、呼び手に次の回で試させる。
|
|
1182
|
+
try {
|
|
1183
|
+
fs.unlinkSync(temp);
|
|
1184
|
+
}
|
|
1185
|
+
catch { /* 残っても中身は前の値 */ }
|
|
1186
|
+
throw error;
|
|
1187
|
+
}
|
|
1188
|
+
}
|