aiterm-mcp 0.31.2 → 0.33.0
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 +31 -1
- package/README.ja.md +68 -16
- package/README.md +71 -17
- package/dist/core.js +393 -56
- package/dist/harnesses/claude.js +34 -1
- package/dist/harnesses/codex.js +105 -1
- package/dist/harnesses/cursor.js +8 -0
- package/dist/harnesses/grok.js +27 -7
- package/dist/harnesses/pane-tokens.js +11 -0
- package/dist/index.js +102 -7
- package/dist/process-runtime.js +101 -0
- package/dist/setup-cli.js +15 -0
- package/dist/setup-integrations.js +107 -0
- package/dist/setup-platform.js +77 -0
- package/dist/setup.js +88 -0
- package/dist/tmux-runtime.js +34 -5
- package/dist/windows-powershell.js +2 -3
- package/docs/00_overview.md +1 -1
- package/docs/DESIGN.md +34 -2
- package/docs/RELEASE.md +19 -8
- package/package.json +2 -1
package/dist/core.js
CHANGED
|
@@ -13,13 +13,15 @@ import * as path from "node:path";
|
|
|
13
13
|
import { createHash, randomBytes } from "node:crypto";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
import * as rtk from "./rtk.js";
|
|
16
|
+
import { paneTokenHint } from "./harnesses/pane-tokens.js";
|
|
17
|
+
import { readRuntimeProcesses, processSubtree, processIdentity, backgroundProcesses } from "./process-runtime.js";
|
|
16
18
|
import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
|
|
17
|
-
import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, settlePaneLog, paneCwdArgument, } from "./tmux-runtime.js";
|
|
19
|
+
import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
|
|
18
20
|
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, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
|
|
19
|
-
import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
|
|
20
|
-
import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady,
|
|
21
|
-
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, claudeSessionTranscriptPath, claudeApiErrorFromLine, } from "./harnesses/claude.js";
|
|
22
|
-
import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
|
|
21
|
+
import { GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
|
|
22
|
+
import { bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, } from "./harnesses/codex.js";
|
|
23
|
+
import { OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeStartupAction, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, } from "./harnesses/claude.js";
|
|
24
|
+
import { bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, buildCursorAgentCmd, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
|
|
23
25
|
import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
|
|
24
26
|
export { AitermError } from "./errors.js";
|
|
25
27
|
export { tmuxSpawnEnv } from "./tmux-runtime.js";
|
|
@@ -651,19 +653,21 @@ function rtkRewrite(text) {
|
|
|
651
653
|
// Windows native pane の既定 shell 解決。裸の "bash" は PATH 上で System32 の
|
|
652
654
|
// WSL launcher (bash.exe) に解決されてしまうため、Git for Windows の bash.exe を
|
|
653
655
|
// 明示解決する(WSL 非依存の裁定に従う)。AITERM_BASH で上書き可。
|
|
654
|
-
export function openSession(name, shell = "bash") {
|
|
656
|
+
export function openSession(name, shell = "bash", envVars = []) {
|
|
655
657
|
shell = resolveWinPaneShell(shell);
|
|
658
|
+
for (const key of envVars) {
|
|
659
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
660
|
+
throw new AitermError("env_varsの環境変数名が不正です", 2);
|
|
661
|
+
}
|
|
656
662
|
try {
|
|
657
663
|
fs.mkdirSync(SOCKDIR, { recursive: true });
|
|
658
664
|
}
|
|
659
665
|
catch (error) {
|
|
660
666
|
ownTelemetryFailure("AITERM.PERSISTENCE_WRITE_FAILED", error);
|
|
661
667
|
}
|
|
662
|
-
// macOS
|
|
663
|
-
// ときだけ -e で環境変数を渡して抑止する。-e は tmux>=3.2 が必要だが macOS の Homebrew tmux は常に該当。
|
|
664
|
-
// (古い tmux<3.2 が残る Linux/WSL で -e を渡すと new-session が落ちるため、darwin 限定にする。)
|
|
668
|
+
// macOS標準bashの移行bannerを抑止する。環境変数の注入方法はterminal runtimeが所有する。
|
|
665
669
|
const banner = process.platform === "darwin" && path.basename(shell) === "bash"
|
|
666
|
-
? ["
|
|
670
|
+
? ["BASH_SILENCE_DEPRECATION_WARNING=1"]
|
|
667
671
|
: [];
|
|
668
672
|
// 並行性対策: 複数エージェントが同時に名前なし open すると autoName が同じ t{i} を返し得る(TOCTOU)。
|
|
669
673
|
// 自動採番は初回のみ読みやすい t{i}、衝突したら乱数 nonce 名でリトライする(線形リトライは高並行で
|
|
@@ -680,9 +684,22 @@ export function openSession(name, shell = "bash") {
|
|
|
680
684
|
}
|
|
681
685
|
else {
|
|
682
686
|
// -f は端末個人の設定ファイルを読まないための空 config。
|
|
683
|
-
const
|
|
684
|
-
|
|
687
|
+
const environment = [...banner, ...envVars.filter(key => key !== "AITERM_SESSION_ID" && process.env[key] !== undefined)
|
|
688
|
+
.map(key => `${key}=${process.env[key]}`), `AITERM_SESSION_ID=${nm}`];
|
|
689
|
+
const launch = sessionEnvironmentLaunch(shell, environment);
|
|
690
|
+
const r = tmux("new-session", "-d", "-s", nm, ...launch.args, "-f", TMUX_EMPTY_CONFIG, launch.shell);
|
|
691
|
+
if (r.code === 0) {
|
|
692
|
+
if (launch.register_after_start)
|
|
693
|
+
for (const entry of environment) {
|
|
694
|
+
const at = entry.indexOf("=");
|
|
695
|
+
const registered = tmux("set-environment", "-t", nm, entry.slice(0, at), entry.slice(at + 1));
|
|
696
|
+
if (registered.code !== 0) {
|
|
697
|
+
tmux("kill-session", "-t", nm);
|
|
698
|
+
throw new AitermError("session環境変数を登録できません", 2);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
685
701
|
break;
|
|
702
|
+
}
|
|
686
703
|
// 自動採番かつ「重複名」由来の失敗(他エージェントが同名を先に取った)なら次名でリトライ。
|
|
687
704
|
const dup = /duplicate|already exists/i.test(r.stderr);
|
|
688
705
|
if (explicit || !dup)
|
|
@@ -1021,14 +1038,172 @@ export async function readOutput(name, o = {}) {
|
|
|
1021
1038
|
return body + "\n" + meta + agentMeta + completionSuffix(status);
|
|
1022
1039
|
return body + "\n" + meta + agentMeta;
|
|
1023
1040
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1041
|
+
function decodeActivityCursor(cursor, name) {
|
|
1042
|
+
let value;
|
|
1043
|
+
try {
|
|
1044
|
+
value = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
1045
|
+
}
|
|
1046
|
+
catch {
|
|
1047
|
+
throw new AitermError("活動観測cursorを読めません", 2);
|
|
1048
|
+
}
|
|
1049
|
+
if (value?.session_id !== name || typeof value.pane_identity !== "string"
|
|
1050
|
+
|| typeof value.screen_digest !== "string" || !value.processes || typeof value.processes !== "object"
|
|
1051
|
+
|| Array.isArray(value.processes) || !Object.values(value.processes).every(cpu => typeof cpu === "number" && Number.isFinite(cpu) && cpu >= 0)
|
|
1052
|
+
|| !value.background_processes || typeof value.background_processes !== "object" || Array.isArray(value.background_processes)
|
|
1053
|
+
|| !Object.values(value.background_processes).every(cpu => typeof cpu === "number" && Number.isFinite(cpu) && cpu >= 0))
|
|
1054
|
+
throw new AitermError("活動観測cursorが対象sessionの形式と一致しません", 2);
|
|
1055
|
+
return value;
|
|
1056
|
+
}
|
|
1057
|
+
function selectHarnessProcesses(meta, rows, subtree) {
|
|
1058
|
+
const matches = (row) => {
|
|
1059
|
+
const first = /^(?:"([^"]+)"|(\S+))/.exec(row.command);
|
|
1060
|
+
const executable = path.posix.basename((first?.[1] ?? first?.[2] ?? "").replace(/\\/g, "/"))
|
|
1061
|
+
.replace(/\.exe$/i, "").replace(/^-/, "").toLowerCase();
|
|
1062
|
+
const command = row.command.replace(/\\/g, "/").replace(/\.exe(?=["\s]|$)/gi, "").replace(/"/g, "");
|
|
1063
|
+
const tokens = row.command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
1064
|
+
const argument = tokens[1]?.replace(/^["']|["']$/g, "").replace(/\\/g, "/");
|
|
1065
|
+
const launchedScript = meta.agent_executable !== undefined && argument === meta.agent_executable.replace(/\\/g, "/");
|
|
1066
|
+
return launchedScript || (!SHELLS.has(executable) && executable !== "pwsh" && AGENT_COMMAND_PATTERNS[meta.kind].test(command));
|
|
1067
|
+
};
|
|
1068
|
+
// WindowsのMSYS execでnative親子関係が切れる場合も、launch固有引数で相関する。
|
|
1069
|
+
const correlated = rows.filter(row => matches(row) && row.command.includes(meta.launch_id));
|
|
1070
|
+
const candidates = correlated.length ? correlated : subtree.filter(matches);
|
|
1071
|
+
const roots = candidates.filter(row => !candidates.some(parent => row.parent_pid === parent.pid));
|
|
1072
|
+
return roots;
|
|
1073
|
+
}
|
|
1074
|
+
export function observeSession(name, cursor) {
|
|
1075
|
+
assertSessionName(name);
|
|
1076
|
+
const previous = cursor === undefined ? null : decodeActivityCursor(cursor, name);
|
|
1077
|
+
const listed = listSessionsResult().sessions.find(session => session.session_id === name);
|
|
1078
|
+
const result = {
|
|
1079
|
+
schema: "aiterm.pty-observe-result.v1", session_id: name, observed_at: new Date().toISOString(),
|
|
1080
|
+
exists: !!listed, harness: null, launch_id: null, state: "missing", reason: "session_missing",
|
|
1081
|
+
pane_alive: false, harness_alive: null, pane_process: null, harness_process: null, process_identity: null,
|
|
1082
|
+
token_hint: null,
|
|
1083
|
+
activity: { cursor: null, output_changed: null, cpu_seconds: null, cpu_delta_seconds: null, cpu_delta_complete: null,
|
|
1084
|
+
background_cpu_seconds: null, background_cpu_delta_seconds: null, background_cpu_delta_complete: null },
|
|
1085
|
+
};
|
|
1086
|
+
if (!listed)
|
|
1087
|
+
return result;
|
|
1088
|
+
const pane = tmux("display-message", "-p", "-t", name, "#{pane_pid}\t#{pane_dead}");
|
|
1089
|
+
if (pane.code !== 0)
|
|
1090
|
+
throw new AitermError("paneの生存情報を取得できません", 2);
|
|
1091
|
+
const [pidText, dead] = pane.stdout.trim().split("\t");
|
|
1092
|
+
const panePid = Number(pidText);
|
|
1093
|
+
if (!Number.isSafeInteger(panePid) || panePid < 1 || !["0", "1"].includes(dead))
|
|
1094
|
+
throw new AitermError("paneの生存情報の形式を認識できません", 2);
|
|
1095
|
+
const meta = tryLoadAgentMetadata(name);
|
|
1096
|
+
result.harness = meta ? agentHarness(meta.kind) : null;
|
|
1097
|
+
result.launch_id = meta?.launch_id ?? null;
|
|
1098
|
+
if (dead === "1") {
|
|
1099
|
+
result.state = "dead";
|
|
1100
|
+
result.reason = "pane_dead";
|
|
1101
|
+
result.harness_alive = meta ? false : null;
|
|
1102
|
+
return result;
|
|
1103
|
+
}
|
|
1104
|
+
const rows = readRuntimeProcesses();
|
|
1105
|
+
const root = rows.find(row => row.pid === panePid);
|
|
1106
|
+
result.pane_alive = root ? true : null;
|
|
1107
|
+
result.state = "unknown";
|
|
1108
|
+
result.reason = root ? "ordinary_pty" : "native_pane_process_unresolved";
|
|
1109
|
+
if (!root)
|
|
1110
|
+
return result;
|
|
1111
|
+
result.pane_process = processIdentity(root);
|
|
1112
|
+
const subtree = processSubtree(rows, root.pid);
|
|
1113
|
+
const candidates = meta ? selectHarnessProcesses(meta, rows, subtree) : [];
|
|
1114
|
+
const agent = candidates.length === 1 ? candidates[0] : null;
|
|
1115
|
+
if (meta) {
|
|
1116
|
+
result.harness_process = agent ? processIdentity(agent) : null;
|
|
1117
|
+
// Cursorにはlaunch固有argvがない。Windowsの切れた親子関係だけで死亡を断定しない。
|
|
1118
|
+
result.harness_alive = candidates.length ? true : isWin && meta.kind === "cursor" ? null : false;
|
|
1119
|
+
if (result.harness_alive === false) {
|
|
1120
|
+
result.state = "dead";
|
|
1121
|
+
result.reason = "harness_exited";
|
|
1122
|
+
}
|
|
1123
|
+
else if (result.harness_alive === null)
|
|
1124
|
+
result.reason = "harness_process_unresolved";
|
|
1125
|
+
else if (!agent)
|
|
1126
|
+
result.reason = "harness_process_ambiguous";
|
|
1127
|
+
result.process_identity = result.harness_process;
|
|
1128
|
+
}
|
|
1129
|
+
else {
|
|
1130
|
+
const leaders = subtree.filter(row => row.parent_pid === root.pid && (isWin || row.process_group_id === row.pid));
|
|
1131
|
+
result.process_identity = leaders.length > 1 ? null : processIdentity(leaders[0] ?? root);
|
|
1132
|
+
if (leaders.length > 1)
|
|
1133
|
+
result.reason = "process_identity_ambiguous";
|
|
1134
|
+
}
|
|
1135
|
+
const captured = tmux("capture-pane", "-p", "-J", "-t", name, "-S", "-200");
|
|
1136
|
+
if (captured.code !== 0)
|
|
1137
|
+
throw new AitermError("paneの活動観測を取得できません", 2);
|
|
1138
|
+
const screen = captured.stdout;
|
|
1139
|
+
result.token_hint = meta ? paneTokenHint(screen) : null;
|
|
1140
|
+
if (meta && agent) {
|
|
1141
|
+
const observation = meta.kind === "grok" || meta.kind === "composer" ? grokPaneObservation(screen)
|
|
1142
|
+
: meta.kind === "codex" ? codexPaneObservation(screen)
|
|
1143
|
+
: meta.kind === "claude" ? claudePaneObservation(screen) : cursorPaneObservation(screen);
|
|
1144
|
+
result.state = observation.state;
|
|
1145
|
+
result.reason = observation.reason;
|
|
1146
|
+
if (agent.stopped === true || processSubtree(rows, agent.pid).some(row => row.process_group_id === agent.process_group_id && row.stopped === true)) {
|
|
1147
|
+
result.state = "blocked";
|
|
1148
|
+
result.reason = "harness_stopped";
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
const activityRows = agent && !subtree.some(row => row.pid === agent.pid)
|
|
1152
|
+
? [...subtree, ...processSubtree(rows, agent.pid)] : subtree;
|
|
1153
|
+
const processes = Object.fromEntries(activityRows.map(row => [`${row.pid}:${row.started_identity}`, row.cpu_seconds]));
|
|
1154
|
+
const backgroundCpu = Object.fromEntries(backgroundProcesses(activityRows, root)
|
|
1155
|
+
.map(row => [`${row.pid}:${row.started_identity}`, row.cpu_seconds]));
|
|
1156
|
+
const sample = {
|
|
1157
|
+
session_id: name, pane_identity: `${root.pid}:${root.started_identity}`,
|
|
1158
|
+
screen_digest: createHash("sha256").update(screen).digest("hex"), processes, background_processes: backgroundCpu,
|
|
1159
|
+
};
|
|
1160
|
+
const comparable = previous?.pane_identity === sample.pane_identity;
|
|
1161
|
+
result.activity = {
|
|
1162
|
+
cursor: Buffer.from(JSON.stringify(sample)).toString("base64url"),
|
|
1163
|
+
output_changed: comparable ? previous.screen_digest !== sample.screen_digest : null,
|
|
1164
|
+
cpu_seconds: Object.values(processes).reduce((sum, cpu) => sum + cpu, 0),
|
|
1165
|
+
cpu_delta_seconds: comparable ? Object.entries(processes).reduce((sum, [identity, cpu]) => sum + cpu - (previous.processes[identity] ?? 0), 0) : null,
|
|
1166
|
+
cpu_delta_complete: comparable ? Object.keys(previous.processes).every(identity => identity in processes) : null,
|
|
1167
|
+
background_cpu_seconds: Object.values(backgroundCpu).reduce((sum, cpu) => sum + cpu, 0),
|
|
1168
|
+
background_cpu_delta_seconds: comparable ? Object.entries(backgroundCpu).reduce((sum, [identity, cpu]) => sum + cpu - (previous.background_processes[identity] ?? 0), 0) : null,
|
|
1169
|
+
background_cpu_delta_complete: comparable ? Object.keys(previous.background_processes).every(identity => identity in backgroundCpu) : null,
|
|
1170
|
+
};
|
|
1171
|
+
return result;
|
|
1172
|
+
}
|
|
1173
|
+
export function listSessionsResult(envKeys = []) {
|
|
1174
|
+
for (const key of envKeys) {
|
|
1175
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
1176
|
+
throw new AitermError("env_keysの環境変数名が不正です", 2);
|
|
1177
|
+
}
|
|
1178
|
+
const r = tmux("list-sessions", "-F", "#{session_name}\t#{pane_current_command}\t#{session_attached}\t#{window_width}\t#{window_height}");
|
|
1179
|
+
const sessions = [];
|
|
1180
|
+
if (r.code !== 0 && !/no server running|No such file or directory/i.test(r.stderr))
|
|
1181
|
+
throw new AitermError("セッション一覧を取得できません: " + r.stderr.trim(), 2);
|
|
1182
|
+
if (r.code === 0)
|
|
1183
|
+
for (const line of r.stdout.trim().split("\n").filter(Boolean)) {
|
|
1184
|
+
const [name, command, attached, width, height] = line.split("\t");
|
|
1185
|
+
assertSessionName(name);
|
|
1186
|
+
const environment = {};
|
|
1187
|
+
for (const key of envKeys) {
|
|
1188
|
+
const value = tmux("show-environment", "-t", name, key);
|
|
1189
|
+
if (value.code !== 0 && !/unknown variable|not found|not set/i.test(value.stderr))
|
|
1190
|
+
throw new AitermError("session環境変数を照会できません", 2);
|
|
1191
|
+
// psmuxは指定以外の内部キーも出すため、要求されたキーと完全一致した行だけを返す。
|
|
1192
|
+
const selected = value.stdout.split(/\r?\n/).find(entry => entry.startsWith(`${key}=`));
|
|
1193
|
+
environment[key] = selected === undefined ? null : selected.slice(key.length + 1);
|
|
1194
|
+
}
|
|
1195
|
+
const meta = tryLoadAgentMetadata(name);
|
|
1196
|
+
sessions.push({ session_id: name, current_command: normalizePaneCommand(command), attached: Number(attached) > 0,
|
|
1197
|
+
width: Number(width), height: Number(height), harness: meta ? agentHarness(meta.kind) : null, environment });
|
|
1198
|
+
}
|
|
1199
|
+
return { schema: "aiterm.pty-list-result.v1", observed_at: new Date().toISOString(), sessions };
|
|
1200
|
+
}
|
|
1201
|
+
export function listSessions(result = listSessionsResult()) {
|
|
1202
|
+
if (result.sessions.length) {
|
|
1203
|
+
return result.sessions
|
|
1204
|
+
.map((session) => {
|
|
1205
|
+
const name = session.session_id;
|
|
1206
|
+
const line = `${name}\t${session.current_command}\t${session.attached ? "attached" : "detached"}\t${session.width}x${session.height}`;
|
|
1032
1207
|
const meta = tryLoadAgentMetadata(name);
|
|
1033
1208
|
if (!meta)
|
|
1034
1209
|
return line;
|
|
@@ -1222,6 +1397,72 @@ function createAgentLineageContext(kind, sessionId, seed) {
|
|
|
1222
1397
|
delegationAllowed: true,
|
|
1223
1398
|
};
|
|
1224
1399
|
}
|
|
1400
|
+
export function runAgentApproval(options) {
|
|
1401
|
+
const { action, session_id: name, approval_choice: selected, observed_prompt_digest: expected } = options;
|
|
1402
|
+
assertSessionName(name);
|
|
1403
|
+
if (action === "inspect" && (selected !== undefined || expected !== undefined))
|
|
1404
|
+
throw new AitermError("inspectでは承認選択とdigestを指定できません", 2);
|
|
1405
|
+
if (action === "respond" && (selected === undefined || expected === undefined))
|
|
1406
|
+
throw new AitermError("respondにはapproval_choiceとobserved_prompt_digestが必要です", 2);
|
|
1407
|
+
const release = action === "respond" ? acquireSessionSendFileLock(name) : () => { };
|
|
1408
|
+
try {
|
|
1409
|
+
const meta = loadAgentMetadata(name);
|
|
1410
|
+
const result = {
|
|
1411
|
+
schema: "aiterm.agent-approval-result.v1", action, status: "blocked", session_id: name,
|
|
1412
|
+
harness: agentHarness(meta.kind), launch_id: meta.launch_id, reason: "harness_unsupported",
|
|
1413
|
+
kind: null, prompt: null, prompt_digest: null, choices: [], selected_choice: null, at: new Date().toISOString(),
|
|
1414
|
+
};
|
|
1415
|
+
if (meta.kind !== "codex")
|
|
1416
|
+
return result;
|
|
1417
|
+
const captured = tmux("capture-pane", "-p", "-J", "-t", name, "-S", "-200");
|
|
1418
|
+
if (captured.code !== 0)
|
|
1419
|
+
throw new AitermError("現在の承認画面を取得できません", 2);
|
|
1420
|
+
const screen = captured.stdout;
|
|
1421
|
+
const state = codexPaneObservation(screen);
|
|
1422
|
+
const dialog = codexApprovalDialog(screen);
|
|
1423
|
+
if (!dialog) {
|
|
1424
|
+
result.status = state.state === "blocked" || state.state === "unknown" || action === "respond" ? "blocked" : "none";
|
|
1425
|
+
result.reason = state.state === "blocked" ? "unknown_dialog" : state.state === "unknown" ? "unrecognized_screen" : "no_current_dialog";
|
|
1426
|
+
return result;
|
|
1427
|
+
}
|
|
1428
|
+
result.kind = dialog.kind;
|
|
1429
|
+
result.prompt = dialog.prompt;
|
|
1430
|
+
result.prompt_digest = `sha256:${createHash("sha256").update(`${meta.launch_id}\n${dialog.canonical}`).digest("hex")}`;
|
|
1431
|
+
result.choices = dialog.choices.map(({ decision, label }) => ({ decision, label }));
|
|
1432
|
+
result.reason = "approval_required";
|
|
1433
|
+
result.status = "approval_required";
|
|
1434
|
+
if (dialog.selected_index === null || dialog.choices.length === 0) {
|
|
1435
|
+
result.status = "blocked";
|
|
1436
|
+
result.reason = "unsupported_choices";
|
|
1437
|
+
return result;
|
|
1438
|
+
}
|
|
1439
|
+
if (action === "inspect")
|
|
1440
|
+
return result;
|
|
1441
|
+
if (expected !== result.prompt_digest) {
|
|
1442
|
+
result.status = "blocked";
|
|
1443
|
+
result.reason = "dialog_changed";
|
|
1444
|
+
return result;
|
|
1445
|
+
}
|
|
1446
|
+
const choice = dialog.choices.find(entry => entry.decision === selected);
|
|
1447
|
+
if (!choice) {
|
|
1448
|
+
result.status = "blocked";
|
|
1449
|
+
result.reason = "choice_unavailable";
|
|
1450
|
+
return result;
|
|
1451
|
+
}
|
|
1452
|
+
const distance = choice.index - dialog.selected_index;
|
|
1453
|
+
const keys = [...Array(Math.abs(distance)).fill(distance > 0 ? "Down" : "Up"), "Enter"];
|
|
1454
|
+
const sent = tmux("send-keys", "-t", name, ...keys);
|
|
1455
|
+
if (sent.code !== 0)
|
|
1456
|
+
throw new AitermError("Codex承認入力を送れませんでした", 2);
|
|
1457
|
+
result.status = "submitted";
|
|
1458
|
+
result.reason = "choice_submitted";
|
|
1459
|
+
result.selected_choice = selected ?? null;
|
|
1460
|
+
return result;
|
|
1461
|
+
}
|
|
1462
|
+
finally {
|
|
1463
|
+
release();
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1225
1466
|
const DEFAULT_AGENT_DONE_TIMEOUT = 600;
|
|
1226
1467
|
const agentMetadataNegativeCache = new Map();
|
|
1227
1468
|
let agentTuiReadyStableSamplesTestOverride = null;
|
|
@@ -1706,6 +1947,15 @@ function loadAgentMetadata(name) {
|
|
|
1706
1947
|
if (m.event_file !== expectedEvent) {
|
|
1707
1948
|
throw new AitermError("agent metadata の path が現在の secure state root と一致しません", 2);
|
|
1708
1949
|
}
|
|
1950
|
+
if (m.initial_prompt_delivery !== undefined && (!["not_requested", "not_sent", "submitted_unconfirmed", "started"].includes(m.initial_prompt_delivery.status)
|
|
1951
|
+
|| typeof m.initial_prompt_delivery.reason !== "string"
|
|
1952
|
+
|| ![true, false, null].includes(m.initial_prompt_delivery.turn_started)
|
|
1953
|
+
|| (m.initial_prompt_cursor !== null && !Number.isSafeInteger(m.initial_prompt_cursor))))
|
|
1954
|
+
throw new AitermError("初手delivery metadataが不正です", 2);
|
|
1955
|
+
const deliveryFields = m.initial_prompt_delivery === undefined ? {} : {
|
|
1956
|
+
initial_prompt_delivery: m.initial_prompt_delivery, initial_prompt_cursor: m.initial_prompt_cursor,
|
|
1957
|
+
};
|
|
1958
|
+
const executableFields = m.agent_executable === undefined ? {} : { agent_executable: m.agent_executable };
|
|
1709
1959
|
if (m.kind === "claude") {
|
|
1710
1960
|
const expectedSettings = agentManagedClaudeSettingsPath(name, m.launch_id);
|
|
1711
1961
|
const expectedResult = agentClaudeResultPath(name, m.launch_id);
|
|
@@ -1731,6 +1981,8 @@ function loadAgentMetadata(name) {
|
|
|
1731
1981
|
...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
|
|
1732
1982
|
vendor_session_id: m.vendor_session_id,
|
|
1733
1983
|
initial_prompt: normalizeInitialPromptState(m.initial_prompt),
|
|
1984
|
+
...deliveryFields,
|
|
1985
|
+
...executableFields,
|
|
1734
1986
|
launch_operation_id: launchOperationId,
|
|
1735
1987
|
launch_request_digest: launchRequestDigest,
|
|
1736
1988
|
hook_route: "shared_claude_settings",
|
|
@@ -1760,6 +2012,8 @@ function loadAgentMetadata(name) {
|
|
|
1760
2012
|
...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
|
|
1761
2013
|
vendor_session_id: typeof m.vendor_session_id === "string" ? m.vendor_session_id : null,
|
|
1762
2014
|
initial_prompt: normalizeInitialPromptState(m.initial_prompt),
|
|
2015
|
+
...deliveryFields,
|
|
2016
|
+
...executableFields,
|
|
1763
2017
|
hook_route: "shared_codex_home",
|
|
1764
2018
|
completion_route: "codex_transcript",
|
|
1765
2019
|
...loadAgentLineageFields(m, true),
|
|
@@ -1785,6 +2039,8 @@ function loadAgentMetadata(name) {
|
|
|
1785
2039
|
...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
|
|
1786
2040
|
vendor_session_id: typeof m.vendor_session_id === "string" ? m.vendor_session_id : null,
|
|
1787
2041
|
initial_prompt: normalizeInitialPromptState(m.initial_prompt),
|
|
2042
|
+
...deliveryFields,
|
|
2043
|
+
...executableFields,
|
|
1788
2044
|
hook_route: "shared_cursor_home",
|
|
1789
2045
|
completion_route: "cursor_transcript",
|
|
1790
2046
|
...loadAgentLineageFields(m, true),
|
|
@@ -1818,6 +2074,8 @@ function loadAgentMetadata(name) {
|
|
|
1818
2074
|
...(typeof m.write_scope === "string" ? { write_scope: m.write_scope } : {}),
|
|
1819
2075
|
vendor_session_id: m.vendor_session_id,
|
|
1820
2076
|
initial_prompt: normalizeInitialPromptState(m.initial_prompt),
|
|
2077
|
+
...deliveryFields,
|
|
2078
|
+
...executableFields,
|
|
1821
2079
|
hook_route: "shared_grok_home",
|
|
1822
2080
|
completion_route: "grok_transcript",
|
|
1823
2081
|
...loadAgentLineageFields(m, true),
|
|
@@ -2169,6 +2427,8 @@ export async function readAgentTranscriptResult(name, o = {}) {
|
|
|
2169
2427
|
text = codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable);
|
|
2170
2428
|
}
|
|
2171
2429
|
else {
|
|
2430
|
+
if (!done)
|
|
2431
|
+
transcriptUnavailable();
|
|
2172
2432
|
text = grokTranscriptText(meta, readTranscriptLines, transcriptUnavailable);
|
|
2173
2433
|
}
|
|
2174
2434
|
if (!text.trim())
|
|
@@ -2493,9 +2753,10 @@ function isAgentTuiBusy(kind, screen) {
|
|
|
2493
2753
|
// ready gate 用: 入力欄マーカーがあっても busy 表示中は ready と数えない。
|
|
2494
2754
|
// frontend 推定(inferAgentFrontend)は「agent TUI が前面か」を見るだけなので isAgentTuiReady のまま。
|
|
2495
2755
|
function isAgentTuiIdleReady(kind, screen) {
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2756
|
+
const observation = kind === "grok" || kind === "composer" ? grokPaneObservation(screen)
|
|
2757
|
+
: kind === "codex" ? codexPaneObservation(screen)
|
|
2758
|
+
: kind === "claude" ? claudePaneObservation(screen) : cursorPaneObservation(screen);
|
|
2759
|
+
return observation.state === "idle";
|
|
2499
2760
|
}
|
|
2500
2761
|
// 起動側が明示応答すべき既知UI。ここで自動承認せず、ready timeoutを待たずに
|
|
2501
2762
|
// `initial_prompt=not_sent`を返してsessionを生かしたままcallerへ制御を戻す。
|
|
@@ -2503,10 +2764,7 @@ function isAgentTuiActionRequired(kind, screen) {
|
|
|
2503
2764
|
if (kind === "grok" || kind === "composer")
|
|
2504
2765
|
return grokLaunchBlockingDialog(screen) !== null;
|
|
2505
2766
|
if (kind === "codex") {
|
|
2506
|
-
return
|
|
2507
|
-
|| screen.includes("Hooks need review")
|
|
2508
|
-
|| screen.includes("Allow the room MCP server to run tool")
|
|
2509
|
-
|| screen.includes("Would you like to run the following command?");
|
|
2767
|
+
return codexPaneObservation(screen).state === "blocked";
|
|
2510
2768
|
}
|
|
2511
2769
|
if (kind === "claude") {
|
|
2512
2770
|
return /new MCP servers? found in this project/iu.test(screen)
|
|
@@ -2795,6 +3053,39 @@ async function sendAgentPromptText(name, text, kind) {
|
|
|
2795
3053
|
}
|
|
2796
3054
|
sendKey(name, "Enter", { preserveAgentOperation: true });
|
|
2797
3055
|
}
|
|
3056
|
+
function setInitialDelivery(meta, value, cursor) {
|
|
3057
|
+
meta.initial_prompt_delivery = value;
|
|
3058
|
+
meta.initial_prompt_cursor = cursor;
|
|
3059
|
+
writeAgentMetadata(meta);
|
|
3060
|
+
}
|
|
3061
|
+
async function prepareAgentInput(name, meta, options) {
|
|
3062
|
+
await ensureAgentOwnsPaneInput(name, meta.kind);
|
|
3063
|
+
let ready = await waitAgentTuiReady(name, meta, options.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
3064
|
+
const handled = new Set();
|
|
3065
|
+
while (!ready.ready) {
|
|
3066
|
+
const action = meta.kind === "codex" ? codexStartupAction(ready.lastScreen, options.trust_project === true)
|
|
3067
|
+
: meta.kind === "claude" ? claudeStartupAction(ready.lastScreen, options.trust_project === true)
|
|
3068
|
+
: meta.kind === "grok" || meta.kind === "composer" ? grokStartupAction(ready.lastScreen, options.trust_project === true) : null;
|
|
3069
|
+
if (!action || handled.has(action.kind))
|
|
3070
|
+
break;
|
|
3071
|
+
handled.add(action.kind);
|
|
3072
|
+
for (const key of action.keys) {
|
|
3073
|
+
sendKey(name, key, { preserveAgentOperation: true });
|
|
3074
|
+
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
3075
|
+
}
|
|
3076
|
+
ready = await waitAgentTuiReady(name, meta, options.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
3077
|
+
}
|
|
3078
|
+
if (!ready.ready) {
|
|
3079
|
+
const state = meta.kind === "grok" || meta.kind === "composer" ? grokPaneObservation(ready.lastScreen)
|
|
3080
|
+
: meta.kind === "codex" ? codexPaneObservation(ready.lastScreen)
|
|
3081
|
+
: meta.kind === "claude" ? claudePaneObservation(ready.lastScreen) : cursorPaneObservation(ready.lastScreen);
|
|
3082
|
+
return { status: "blocked", reason: state.reason };
|
|
3083
|
+
}
|
|
3084
|
+
const live = observeSession(name);
|
|
3085
|
+
if (live.harness_alive !== true || live.harness_process === null || live.state !== "idle")
|
|
3086
|
+
return { status: "blocked", reason: live.reason };
|
|
3087
|
+
return { status: "ready", reason: "composer_ready" };
|
|
3088
|
+
}
|
|
2798
3089
|
export async function sendInitialAgentPrompt(name, text, o = {}) {
|
|
2799
3090
|
assertSessionName(name);
|
|
2800
3091
|
const meta = loadAgentMetadata(name);
|
|
@@ -2805,29 +3096,12 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
|
|
|
2805
3096
|
throw new AitermError(`agent session '${name}' は起動時 prompt の完了待ちです。初回応答完了後に再度操作してください。`, 2);
|
|
2806
3097
|
}
|
|
2807
3098
|
setInitialPromptState(meta, "not_sent");
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
for (let confirmations = 0; !ready.ready && meta.kind === "claude" && confirmations < 3 && isClaudeManagedLaunchConfirmation(ready.lastScreen); confirmations++) {
|
|
2815
|
-
sendKey(name, "Down", { preserveAgentOperation: true });
|
|
2816
|
-
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
2817
|
-
sendKey(name, "Enter", { preserveAgentOperation: true });
|
|
2818
|
-
await sleep(AGENT_SUBMIT_DELAY_MS);
|
|
2819
|
-
ready = await waitAgentTuiReady(name, meta, o.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
|
|
2820
|
-
}
|
|
2821
|
-
if (!ready.ready) {
|
|
2822
|
-
// ready失敗は成功形で返さず明示エラーにする(実被弾 2026-08-25: Codexのupdate確認ダイアログで
|
|
2823
|
-
// 未送信のまま成功形receiptが返り、呼び出し側が40分気づけなかった)。sessionは調査/復旧用に残る。
|
|
2824
|
-
const dialog = meta.kind === "codex" ? codexLaunchBlockingDialog(ready.lastScreen)
|
|
2825
|
-
: meta.kind === "grok" || meta.kind === "composer" ? grokLaunchBlockingDialog(ready.lastScreen) : null;
|
|
2826
|
-
const causeNote = dialog
|
|
2827
|
-
? `${dialog}が入力を塞いでいます。pty_read(screen:true)で画面を確認し、pty_keyでダイアログに応答してから、pty_sendでpromptを送ってください。`
|
|
2828
|
-
: `pty_read(screen:true)で画面を確認し、入力受付になってからpty_sendでpromptを送ってください。`;
|
|
2829
|
-
throw new AitermError(`initial_prompt=not_sent vendor=${meta.kind} ready=false samples=${ready.samples} harness=${agentHarness(meta.kind)}\n` +
|
|
2830
|
-
`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態にならず、prompt は送信していません。${causeNote}`, 2);
|
|
3099
|
+
setInitialDelivery(meta, { status: "not_sent", reason: "preparing", turn_started: false }, null);
|
|
3100
|
+
const startup = await prepareAgentInput(name, meta, o);
|
|
3101
|
+
if (startup.status !== "ready") {
|
|
3102
|
+
setInitialDelivery(meta, { status: "not_sent", reason: startup.reason, turn_started: false }, null);
|
|
3103
|
+
throw new AitermError(`initial_prompt=not_sent vendor=${meta.kind} ready=false harness=${agentHarness(meta.kind)}\n` +
|
|
3104
|
+
`agent session '${name}' の起動準備が完了していないため、promptは送信していません。reason=${startup.reason}`, 2);
|
|
2831
3105
|
}
|
|
2832
3106
|
const promptText = meta.kind === "cursor" ? cursorPromptWithLineage(meta, text) : text;
|
|
2833
3107
|
const startOffset = agentCompletionCursor(meta);
|
|
@@ -2836,6 +3110,7 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
|
|
|
2836
3110
|
prepareSendText(text, { raw: false });
|
|
2837
3111
|
reserveAnonymousClaudeTurn(meta);
|
|
2838
3112
|
}
|
|
3113
|
+
setInitialDelivery(meta, { status: "submitted_unconfirmed", reason: "dispatch_in_progress", turn_started: null }, startOffset);
|
|
2839
3114
|
await sendAgentPromptText(name, promptText, meta.kind);
|
|
2840
3115
|
setInitialPromptState(meta, "pending");
|
|
2841
3116
|
}
|
|
@@ -2863,12 +3138,40 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
|
|
|
2863
3138
|
setInitialPromptState(meta, "failed");
|
|
2864
3139
|
throw e;
|
|
2865
3140
|
}
|
|
3141
|
+
let delivery = { status: "submitted_unconfirmed", reason: "start_unconfirmed", turn_started: null };
|
|
3142
|
+
const deadline = performance.now() + 3000;
|
|
3143
|
+
do {
|
|
3144
|
+
const screen = captureScreen(name, AGENT_TUI_READY_LINES);
|
|
3145
|
+
const state = meta.kind === "grok" || meta.kind === "composer" ? grokPaneObservation(screen)
|
|
3146
|
+
: meta.kind === "codex" ? codexPaneObservation(screen)
|
|
3147
|
+
: meta.kind === "claude" ? claudePaneObservation(screen) : cursorPaneObservation(screen);
|
|
3148
|
+
if (state.state === "busy") {
|
|
3149
|
+
delivery = { status: "started", reason: "turn_running", turn_started: true };
|
|
3150
|
+
break;
|
|
3151
|
+
}
|
|
3152
|
+
if (state.state === "blocked" && ["command_approval", "mcp_approval", "tool_approval"].includes(state.reason)) {
|
|
3153
|
+
delivery = { status: "started", reason: "approval_required", turn_started: true };
|
|
3154
|
+
break;
|
|
3155
|
+
}
|
|
3156
|
+
const completed = await observeAgentDone(name, { cursor: startOffset, timeout: 0 });
|
|
3157
|
+
if (completed.outcome === "done") {
|
|
3158
|
+
delivery = { status: "started", reason: "turn_completed", turn_started: true };
|
|
3159
|
+
break;
|
|
3160
|
+
}
|
|
3161
|
+
if (state.state === "blocked" || completed.outcome === "error" || completed.outcome === "rate_limited") {
|
|
3162
|
+
delivery.reason = state.state === "blocked" ? state.reason : completed.outcome;
|
|
3163
|
+
break;
|
|
3164
|
+
}
|
|
3165
|
+
await sleep(100);
|
|
3166
|
+
} while (performance.now() < deadline);
|
|
3167
|
+
setInitialDelivery(meta, delivery, startOffset);
|
|
2866
3168
|
return {
|
|
2867
3169
|
text: `initial_prompt=pending vendor=${meta.kind} event_cursor=${startOffset} harness=${agentHarness(meta.kind)}\n` +
|
|
2868
3170
|
`起動時 prompt を送信した。${agentDispatchGuide(name, startOffset)}` +
|
|
2869
3171
|
agentSubmitResidueWarning(name, residue.residue),
|
|
2870
3172
|
event_cursor: startOffset,
|
|
2871
3173
|
submit_residue: residue.residue,
|
|
3174
|
+
initial_prompt: delivery,
|
|
2872
3175
|
};
|
|
2873
3176
|
}
|
|
2874
3177
|
export function isAgentSession(name) {
|
|
@@ -3520,7 +3823,7 @@ export function openAgent(kind, opts = {}) {
|
|
|
3520
3823
|
let sid;
|
|
3521
3824
|
let hint;
|
|
3522
3825
|
try {
|
|
3523
|
-
[sid, hint] = openSession(opts.session_name ?? null, "bash");
|
|
3826
|
+
[sid, hint] = openSession(opts.session_name ?? null, "bash", envVars);
|
|
3524
3827
|
}
|
|
3525
3828
|
catch (error) {
|
|
3526
3829
|
if (launchOperationId !== null && sessionExists(opts.session_name)) {
|
|
@@ -3541,8 +3844,11 @@ export function openAgent(kind, opts = {}) {
|
|
|
3541
3844
|
? createCursorAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", writeScope, lineageContext)
|
|
3542
3845
|
: createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope, lineageContext)
|
|
3543
3846
|
: null;
|
|
3544
|
-
if (meta)
|
|
3847
|
+
if (meta) {
|
|
3848
|
+
meta.agent_executable = bin;
|
|
3849
|
+
writeAgentMetadata(meta);
|
|
3545
3850
|
agentMetadataNegativeCache.delete(sid);
|
|
3851
|
+
}
|
|
3546
3852
|
launchNote = buildAgentLaunchNote(kind, model, effort, meta);
|
|
3547
3853
|
const cmd = buildAgentCmd(kind, binForCmd, model, effort, opts.prompt ?? null, meta);
|
|
3548
3854
|
const envPrefix = agentEnvPrefix(meta, sid, envVars);
|
|
@@ -3582,6 +3888,19 @@ export function openAgent(kind, opts = {}) {
|
|
|
3582
3888
|
`起動直後に増分 pty_read すると空/半描画になり得るので screen:true を使う。`,
|
|
3583
3889
|
];
|
|
3584
3890
|
}
|
|
3891
|
+
export class AgentLaunchPromptError extends AitermError {
|
|
3892
|
+
session_id;
|
|
3893
|
+
initial_prompt;
|
|
3894
|
+
event_cursor;
|
|
3895
|
+
startup;
|
|
3896
|
+
constructor(message, code, session_id, initial_prompt, event_cursor, startup = { status: "blocked", reason: initial_prompt.reason }) {
|
|
3897
|
+
super(message, code);
|
|
3898
|
+
this.session_id = session_id;
|
|
3899
|
+
this.initial_prompt = initial_prompt;
|
|
3900
|
+
this.event_cursor = event_cursor;
|
|
3901
|
+
this.startup = startup;
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3585
3904
|
export async function openAgentWithInitialPrompt(kind, opts = {}) {
|
|
3586
3905
|
const mission = opts.prompt ?? null;
|
|
3587
3906
|
const sourceSessionId = opts.throughline_source_session ?? null;
|
|
@@ -3624,7 +3943,21 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
|
|
|
3624
3943
|
write_scope: opts.write_scope,
|
|
3625
3944
|
env_vars: opts.env_vars,
|
|
3626
3945
|
});
|
|
3627
|
-
|
|
3946
|
+
const delivery = { status: "not_requested", reason: "not_requested", turn_started: false };
|
|
3947
|
+
let startup = { status: "not_checked", reason: "startup_not_requested" };
|
|
3948
|
+
if (opts.trust_project === true) {
|
|
3949
|
+
try {
|
|
3950
|
+
startup = await prepareAgentInput(sid, loadAgentMetadata(sid), {
|
|
3951
|
+
trust_project: true, ready_timeout: opts.ready_timeout ?? undefined,
|
|
3952
|
+
});
|
|
3953
|
+
}
|
|
3954
|
+
catch (error) {
|
|
3955
|
+
throw new AgentLaunchPromptError(`session_id: ${sid}\n起動準備中に失敗しました。${error instanceof Error ? error.message : String(error)}`, error instanceof AitermError ? error.code : 1, sid, delivery, null, { status: "blocked", reason: "startup_failed" });
|
|
3956
|
+
}
|
|
3957
|
+
if (startup.status !== "ready")
|
|
3958
|
+
throw new AgentLaunchPromptError(`session_id: ${sid}\n起動準備を完了できませんでした。reason=${startup.reason}`, 2, sid, delivery, null, startup);
|
|
3959
|
+
}
|
|
3960
|
+
return [sid, hint, null, null, delivery, startup];
|
|
3628
3961
|
}
|
|
3629
3962
|
const [sid, hint] = openAgent(kind, {
|
|
3630
3963
|
session_name: opts.session_name ?? null,
|
|
@@ -3640,13 +3973,17 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
|
|
|
3640
3973
|
try {
|
|
3641
3974
|
const initial = await sendInitialAgentPrompt(sid, prompt, {
|
|
3642
3975
|
ready_timeout: opts.ready_timeout ?? undefined,
|
|
3976
|
+
trust_project: opts.trust_project,
|
|
3643
3977
|
});
|
|
3644
|
-
return [sid, `${hint}\n${initial.text}`, initial.event_cursor, initial.submit_residue
|
|
3978
|
+
return [sid, `${hint}\n${initial.text}`, initial.event_cursor, initial.submit_residue, initial.initial_prompt,
|
|
3979
|
+
{ status: "ready", reason: "composer_ready" }];
|
|
3645
3980
|
}
|
|
3646
3981
|
catch (e) {
|
|
3647
3982
|
const code = e instanceof AitermError ? e.code : 1;
|
|
3648
3983
|
const message = e instanceof Error ? e.message : String(e);
|
|
3649
|
-
|
|
3650
|
-
|
|
3984
|
+
const meta = loadAgentMetadata(sid);
|
|
3985
|
+
throw new AgentLaunchPromptError(`session_id: ${sid}\n` +
|
|
3986
|
+
`起動後の初回 prompt 処理で失敗しました。session は調査/復旧用に残しています。\n${message}`, code, sid, meta.initial_prompt_delivery ?? { status: "not_sent", reason: "startup_failed", turn_started: false }, meta.initial_prompt_cursor ?? null, { status: meta.initial_prompt_delivery?.status === "submitted_unconfirmed" ? "ready" : "blocked",
|
|
3987
|
+
reason: meta.initial_prompt_delivery?.reason ?? "startup_failed" });
|
|
3651
3988
|
}
|
|
3652
3989
|
}
|