aiterm-mcp 0.60.1 → 0.61.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +14 -1
  2. package/README.ja.md +1 -1
  3. package/README.md +1 -1
  4. package/dist/claude-operation-marker.js +11 -0
  5. package/dist/claude-stop-hook.js +5 -9
  6. package/dist/core.js +537 -44
  7. package/dist/execution-checkpoint.js +83 -0
  8. package/dist/execution-close.js +82 -0
  9. package/dist/execution-context.js +19 -0
  10. package/dist/execution-contract.js +263 -0
  11. package/dist/execution-dispatch.js +285 -0
  12. package/dist/execution-effect.js +411 -0
  13. package/dist/execution-immediate-stop.js +68 -0
  14. package/dist/execution-interruption.js +150 -0
  15. package/dist/execution-inventory.js +100 -0
  16. package/dist/execution-lock.js +136 -0
  17. package/dist/execution-native-server.js +140 -0
  18. package/dist/execution-native-setup.js +64 -0
  19. package/dist/execution-parent-alias.js +75 -0
  20. package/dist/execution-parent-claude-records.js +136 -0
  21. package/dist/execution-parent-endpoint.js +92 -0
  22. package/dist/execution-parent-manager.js +89 -0
  23. package/dist/execution-parent-native.js +179 -0
  24. package/dist/execution-parent-process.js +29 -0
  25. package/dist/execution-parent-producer.js +129 -0
  26. package/dist/execution-parent-queue.js +84 -0
  27. package/dist/execution-parent-source.js +200 -0
  28. package/dist/execution-parent-worker.js +89 -0
  29. package/dist/execution-parent.js +345 -0
  30. package/dist/execution-participant-store.js +107 -0
  31. package/dist/execution-participant.js +78 -0
  32. package/dist/execution-runtime.js +182 -0
  33. package/dist/execution-service.js +1334 -0
  34. package/dist/execution-steer.js +37 -0
  35. package/dist/execution-store.js +206 -0
  36. package/dist/execution-tools.js +151 -0
  37. package/dist/execution-wire.js +129 -0
  38. package/dist/harnesses/claude.js +2 -0
  39. package/dist/harnesses/codex.js +111 -0
  40. package/dist/harnesses/cursor.js +2 -0
  41. package/dist/harnesses/grok.js +2 -0
  42. package/dist/index.js +1196 -1122
  43. package/dist/parent-delivery.js +408 -41
  44. package/dist/process-runtime.js +28 -0
  45. package/dist/tmux-runtime.js +30 -4
  46. package/docs/DESIGN.md +60 -0
  47. package/docs/RELEASE.md +19 -1
  48. package/package.json +4 -3
package/dist/core.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { parseClaudeOperationMarker } from "./claude-operation-marker.js";
1
2
  /**
2
3
  * core — CLI/MCP が共有する純粋ロジック層(Node/TS 版)。
3
4
  *
@@ -13,10 +14,15 @@ import * as path from "node:path";
13
14
  import { createHash, randomBytes } from "node:crypto";
14
15
  import { fileURLToPath } from "node:url";
15
16
  import * as rtk from "./rtk.js";
17
+ import { closeExpectedExecution, parseExecutionNativeSessions, executionNativeBindingSchema } from "./execution-close.js";
18
+ import { executionPointSchema } from "./execution-contract.js";
19
+ import { withExecutionInterruptEffectScope, bindExecutionNativeTarget, beforeExecutionEffect, bindExecutionCreatedLaunch, executionCreatedNativeBinding, hasExecutionCreationScope, assertExecutionContext, withExecutionPreparation, captureExecutionSendBoundary, readExecutionSendBoundary } from "./execution-effect.js";
20
+ import { ExecutionError } from "./execution-wire.js";
21
+ import { readExecutionParentProcess } from "./execution-parent-process.js";
16
22
  import { isCursorMcpClient } from "aiterm-steer-delivery";
17
23
  import { paneTokenHint } from "./harnesses/pane-tokens.js";
18
24
  import { unfinishedDeliveriesOwnedBy } from "./parent-delivery-owners.js";
19
- import { readRuntimeProcesses, processSubtree, parentProcess, processIdentity, backgroundProcesses, lazyRelayProcess, aitermProcessProbe, windowsConsoleHost, processElapsedSeconds } from "./process-runtime.js";
25
+ import { readRuntimeProcesses, readProcessIdentities, processSubtree, parentProcess, processIdentity, backgroundProcesses, lazyRelayProcess, aitermProcessProbe, windowsConsoleHost, processElapsedSeconds } from "./process-runtime.js";
20
26
  import { liveNodeExecutable } from "./live-node.js";
21
27
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
22
28
  import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, tmuxNewSession, openPsmuxServerToDesktop, registeredSessionEnvironment, attachCommand, normalizePaneCommand, atomicShellMultiline, appendMarkSentinel, markShellCommand, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
@@ -25,10 +31,10 @@ import { catalogUnavailable } from "./model-catalog.js";
25
31
  import { FastModeError, fastModeFailure } from "./fast-mode.js";
26
32
  import { readFileOrNull, restoreJsonSetting, withUserConfigLock } from "./user-config.js";
27
33
  import { isDeepStrictEqual } from "node:util";
28
- import { grokAuthPlan, grokAuthStatus, grokAuthPane, GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokModelChoices, grokModelFromFooter, grokFooterModelName, grokModelDefaultsFromBody, restoreGrokModelDefaults, grokEventsTranscript, latestGrokCompletion, grokTurnSettled, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokPromptAwaitingAccept, grokUnacceptedPromptRestored, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
29
- import { codexAuthPlan, codexAuthStatus, codexAuthPane, realCodexHome, readCodexConfigPins, bindCodexTranscriptSession, latestCodexCompletion, codexTurnSettled, codexRunningTurn, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexTuiBusy, codexHelperProcess, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, codexFastCommandOffered, codexFooterShowsFast, codexServiceTierNotices, restoreCodexDefaults, restoreCodexDefaultInFile, } from "./harnesses/codex.js";
30
- import { claudeAuthPlan, claudeAuthStatus, claudeAuthPane, OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, claudeModelChoices, claudeConfigDir, claudeFastIndicatorShown, claudeFastResult, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeUsageLimit, claudeLiveSelectMenu, claudeModelPicker, claudeModelPickerRow, claudeModelSwitchConfirm, claudeModelSwitchResult, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, claudeApiErrorAfter, } from "./harnesses/claude.js";
31
- import { cursorAuthPlan, cursorAuthStatus, cursorAuthPane, bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, cursorModelChoices, cursorModelIsFastVariant, cursorFastParameter, cursorHeaderShowsFast, cursorPickerView, cursorEffortSelected, cursorParameterRows, cursorUserConfigPath, CURSOR_DEFAULT_MODEL_KEYS, realCursorHome, buildCursorAgentCmd, cursorAgentArgv, cursorPwshArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, cursorPromptHooksRunning, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
34
+ import { grokAuthPlan, grokAuthStatus, grokAuthPane, GROK_MODEL_DEFAULTS, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokModelChoices, grokModelFromFooter, grokFooterModelName, grokModelDefaultsFromBody, restoreGrokModelDefaults, grokEventsTranscript, latestGrokCompletion, grokTurnSettled, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, GROK_INTERRUPT_KEY, grokRateLimitDialog, grokPromptAwaitingAccept, grokUnacceptedPromptRestored, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
35
+ import { codexAuthPlan, codexAuthStatus, codexAuthPane, realCodexHome, readCodexConfigPins, bindCodexTranscriptSession, latestCodexCompletion, codexTurnSettled, codexRunningTurn, codexInterruptionRecord, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, CODEX_INTERRUPT_KEY, codexTuiBusy, codexHelperProcess, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, codexFastCommandOffered, codexFooterShowsFast, codexServiceTierNotices, restoreCodexDefaults, restoreCodexDefaultInFile, } from "./harnesses/codex.js";
36
+ import { claudeAuthPlan, claudeAuthStatus, claudeAuthPane, OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, claudeModelChoices, claudeConfigDir, claudeFastIndicatorShown, claudeFastResult, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, CLAUDE_INTERRUPT_KEY, claudeUsageLimit, claudeLiveSelectMenu, claudeModelPicker, claudeModelPickerRow, claudeModelSwitchConfirm, claudeModelSwitchResult, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, claudeApiErrorAfter, } from "./harnesses/claude.js";
37
+ import { cursorAuthPlan, cursorAuthStatus, cursorAuthPane, bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, cursorModelChoices, cursorModelIsFastVariant, cursorFastParameter, cursorHeaderShowsFast, cursorPickerView, cursorEffortSelected, cursorParameterRows, cursorUserConfigPath, CURSOR_DEFAULT_MODEL_KEYS, realCursorHome, buildCursorAgentCmd, cursorAgentArgv, cursorPwshArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, CURSOR_INTERRUPT_KEY, cursorPromptHooksRunning, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
32
38
  import { interimFromNow, readInterimWords, recordInterimBoundary } from "./interim-words.js";
33
39
  import { codexSeatTurn, observeSteerDone, readSteerEntry, recordSteer } from "./steer-follow.js";
34
40
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
@@ -191,7 +197,7 @@ export function termiosIsCooked(sttyOutput) {
191
197
  return tokens.includes("icanon") || tokens.includes("echo");
192
198
  }
193
199
  function paneTtyOf(name) {
194
- const r = tmux("display-message", "-p", "-t", name, "#{pane_tty}");
200
+ const r = tmux("display-message", "-p", "-t", `=${name}:`, "#{pane_tty}");
195
201
  if (r.code !== 0)
196
202
  return null;
197
203
  const tty = r.stdout.trim();
@@ -203,6 +209,7 @@ function paneTtyOf(name) {
203
209
  * agent が tty 上に実在しない時は触らない(ready gate が入力受付不能として止める)。
204
210
  */
205
211
  export async function ensureAgentOwnsPaneInput(name, kind) {
212
+ assertExecutionContext(name);
206
213
  if (isWin)
207
214
  return [];
208
215
  const tty = paneTtyOf(name);
@@ -240,6 +247,13 @@ export async function ensureAgentOwnsPaneInput(name, kind) {
240
247
  const ttyFlag = process.platform === "linux" ? "-F" : "-f";
241
248
  const stty = spawnSync("/bin/stty", [ttyFlag, tty, "-a"], { encoding: "utf8", timeout: 5000, env: psEnv });
242
249
  if (stty.status === 0 && termiosIsCooked(stty.stdout ?? "")) {
250
+ // stty is a native mutation outside tmux's command gate. Recheck the
251
+ // captured device and the shared request generation/fence immediately
252
+ // before dispatch; a replaced pane must not inherit the old tty repair.
253
+ beforeExecutionEffect(name, () => {
254
+ if (paneTtyOf(name) !== tty)
255
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
256
+ });
243
257
  const set = spawnSync("/bin/stty", [ttyFlag, tty, "raw", "-echo"], { encoding: "utf8", timeout: 5000, env: psEnv });
244
258
  if (set.status !== 0) {
245
259
  throw new AitermError(`AGENT_TTY_COOKED session=${name}\n` +
@@ -667,6 +681,7 @@ function rtkRewrite(text) {
667
681
  // WSL launcher (bash.exe) に解決されてしまうため、Git for Windows の bash.exe を
668
682
  // 明示解決する(WSL 非依存の裁定に従う)。AITERM_BASH で上書き可。
669
683
  export function openSession(name, shell = "bash", envVars = []) {
684
+ assertExecutionContext(name);
670
685
  shell = resolveWinPaneShell(shell);
671
686
  for (const key of envVars) {
672
687
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
@@ -823,6 +838,7 @@ function prepareSendText(text, o) {
823
838
  return text;
824
839
  }
825
840
  export function send(name, text, o = {}) {
841
+ assertExecutionContext(name);
826
842
  assertSessionName(name);
827
843
  const enter = o.enter ?? true;
828
844
  if (!sessionExists(name))
@@ -948,10 +964,20 @@ export function send(name, text, o = {}) {
948
964
  }
949
965
  }
950
966
  export function sendKey(name, key, o = {}) {
967
+ const result = sendKeyResult(name, key, o);
968
+ return `sent key ${result.key} to ${result.session_id}`;
969
+ }
970
+ /** Native transport acknowledgement only. This does not observe a turn stop,
971
+ * a launch/conversation identity, or permission to retry a failed delivery.
972
+ */
973
+ export function sendKeyResult(name, key, o = {}) {
974
+ assertExecutionContext(name);
951
975
  assertSessionName(name);
952
976
  if (!sessionExists(name))
953
977
  throw new AitermError(`session '${name}' が無い`, 2);
954
978
  const k = KEYMAP[key.toLowerCase()] ?? key;
979
+ const receipt = () => ({ schema: "aiterm.pty-key-result.v1", session_id: name, key: k,
980
+ outcome: "accepted", interruption: "unconfirmed", session_preserved: "unconfirmed" });
955
981
  if (!o.preserveAgentOperation && managedClaudeOperation(name) !== undefined && k !== "C-c") {
956
982
  throw new AitermError("aiterm相関付きClaude agent sessionではturn相関を壊さないC-cだけをpty_keyで送れます。" +
957
983
  "他の対話操作はpty_send(自動dispatch)、終了はpty_closeを使ってください。", 2);
@@ -964,10 +990,303 @@ export function sendKey(name, key, o = {}) {
964
990
  raw: true,
965
991
  preserveAgentOperation: o.preserveAgentOperation,
966
992
  });
967
- return `sent key ${k} to ${name}`;
993
+ return receipt();
994
+ }
995
+ const result = tmux("send-keys", "-t", name, k);
996
+ if (result.code !== 0)
997
+ throw new AitermError("制御キー送信を確認できません。自動再送やcloseへの切替はしていません。", 2);
998
+ return receipt();
999
+ }
1000
+ // Interrupt needs the recorded harness/name/launch, not auth readiness or
1001
+ // transcript/operation proof. An expired login must not block the cancel key.
1002
+ function readInterruptionTarget(name) {
1003
+ try {
1004
+ const files = fs.readdirSync(agentsDir()).filter(file => file.startsWith(name + ".") && file.endsWith(".agent.json"));
1005
+ if (files.length !== 1)
1006
+ return null;
1007
+ const meta = JSON.parse(fs.readFileSync(path.join(agentsDir(), files[0]), "utf8"));
1008
+ return meta.aiterm_session === name && ["codex", "claude", "cursor", "grok"].includes(meta.kind)
1009
+ && typeof meta.launch_id === "string" && LAUNCH_ID_RE.test(meta.launch_id) ? { kind: meta.kind, launch_id: meta.launch_id } : null;
1010
+ }
1011
+ catch {
1012
+ return null;
968
1013
  }
969
- tmux("send-keys", "-t", name, k);
970
- return `sent key ${k} to ${name}`;
1014
+ }
1015
+ /** Immediate stop-button operation. No run/actor/claim journal or completion
1016
+ * proof is required to send the harness's normal cancel key. The native session ID prevents a same-name
1017
+ * replacement from receiving the key. This never closes or clears a session. */
1018
+ export function interruptSessionNow(name, expectedLaunchId) {
1019
+ assertSessionName(name);
1020
+ const listing = observeExecutionNativeSessions();
1021
+ if (listing.state !== "known")
1022
+ throw new ExecutionError("EXECUTION_UNCONFIRMED");
1023
+ const native = listing.sessions.find(session => session.session_id === name);
1024
+ if (!native)
1025
+ return { session_id: name, outcome: "idle" };
1026
+ const meta = readInterruptionTarget(name);
1027
+ if (!meta || !["codex", "claude", "cursor", "grok"].includes(meta.kind))
1028
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1029
+ const key = meta.kind === "codex" ? CODEX_INTERRUPT_KEY : meta.kind === "claude" ? CLAUDE_INTERRUPT_KEY
1030
+ : meta.kind === "cursor" ? CURSOR_INTERRUPT_KEY : GROK_INTERRUPT_KEY;
1031
+ // A normal Ctrl+C can quit an idle CLI. Use its existing immediate pane
1032
+ // observation, never repeated Ctrl+C or kill as an escalation.
1033
+ if (key === "C-c") {
1034
+ const screen = captureScreen(name, AGENT_TUI_READY_LINES);
1035
+ const state = (meta.kind === "cursor" ? cursorPaneObservation(screen) : grokPaneObservation(screen)).state;
1036
+ if (state === "idle")
1037
+ return { session_id: name, outcome: "idle" };
1038
+ if (state !== "busy")
1039
+ throw new ExecutionError("EXECUTION_UNCONFIRMED");
1040
+ }
1041
+ return withExecutionInterruptEffectScope(name, native.native_id, () => {
1042
+ if (expectedLaunchId && readInterruptionTarget(name)?.launch_id !== expectedLaunchId)
1043
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1044
+ }, () => {
1045
+ bindExecutionNativeTarget(name, native.native_id);
1046
+ beforeExecutionEffect(name);
1047
+ const result = tmux("send-keys", "-t", native.native_id, key);
1048
+ if (result.code !== 0)
1049
+ throw new ExecutionError("EXECUTION_UNCONFIRMED", "possible");
1050
+ return { schema: "aiterm.pty-key-result.v1", session_id: name, key, outcome: "accepted",
1051
+ interruption: "unconfirmed", session_preserved: "unconfirmed" };
1052
+ }, key);
1053
+ }
1054
+ export function interruptionLaunchId(name) {
1055
+ assertSessionName(name);
1056
+ return readInterruptionTarget(name)?.launch_id ?? null;
1057
+ }
1058
+ /** Private minimal harness primitive. The caller already owns the current
1059
+ * run's native admission frame. No public pty_key guard bypass is exposed to
1060
+ * Bridge/model args, and agent_stop remains disconnected until preservation
1061
+ * and interruption/fence evidence are implemented. Never close or retry.
1062
+ */
1063
+ export function interruptExpectedAgentRun(input) {
1064
+ const point = executionPointSchema.parse(input), name = point.launch.session_id;
1065
+ assertExecutionContext(name);
1066
+ if (!point.boundary)
1067
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1068
+ const verifyRun = (meta) => {
1069
+ if (meta.kind === "codex") {
1070
+ if (!point.boundary.turn_id)
1071
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1072
+ const record = codexInterruptionRecord(meta);
1073
+ if (!record)
1074
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1075
+ if (record.boundary.type !== "task_started" || record.boundary.turn_id !== point.boundary.turn_id)
1076
+ throw new ExecutionError("EXECUTION_CONFLICT");
1077
+ return record;
1078
+ }
1079
+ else if (meta.kind === "claude") {
1080
+ const marker = readClaudeOperationMarker(meta);
1081
+ if (!matchesClaudeInterruptionMarker(marker, point))
1082
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1083
+ // Anonymous identity is the v2 marker's original reservation, not a
1084
+ // native turn. Keep its file identity stable through the physical gate.
1085
+ return marker.operationId === null ? observeManagedClaudeInterruption(point) : marker;
1086
+ }
1087
+ else
1088
+ throw new ExecutionError("EXECUTION_UNSUPPORTED"); // no guessed C-c/Esc for Cursor/Grok
1089
+ };
1090
+ const current = () => {
1091
+ const observed = observeExecutionSessionGeneration(name);
1092
+ if (!observed.exists || !observed.launch_id)
1093
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1094
+ if (observed.launch_id !== point.launch.launch_id)
1095
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1096
+ const meta = loadAgentMetadata(name);
1097
+ if (meta.launch_id !== point.launch.launch_id)
1098
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1099
+ const run = verifyRun(meta);
1100
+ const process = observeExecutionParentProcess(name);
1101
+ const actor = process?.harness_process;
1102
+ if (process?.schema !== "aiterm.execution-parent-process.v1" || !isDeepStrictEqual(process.source?.launch, point.launch)
1103
+ || process.harness !== agentHarness(meta.kind) || !actor || !Number.isSafeInteger(actor.pid) || actor.pid < 1
1104
+ || !Number.isFinite(Date.parse(actor.started_identity)) || !meta.execution_native)
1105
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1106
+ const after = observeExecutionSessionGeneration(name), last = loadAgentMetadata(name);
1107
+ if (!after.exists || after.launch_id !== point.launch.launch_id || last.kind !== meta.kind
1108
+ || !isDeepStrictEqual(last.execution_native, meta.execution_native))
1109
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1110
+ if (!isDeepStrictEqual(verifyRun(last), run))
1111
+ throw new ExecutionError("EXECUTION_CONFLICT");
1112
+ return { actor, native: meta.execution_native, kind: meta.kind, run };
1113
+ };
1114
+ const before = current();
1115
+ bindExecutionNativeTarget(name, before.native.native_id);
1116
+ beforeExecutionEffect(name, () => {
1117
+ if (!isDeepStrictEqual(current(), before))
1118
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1119
+ });
1120
+ // This address came from the same expected-generation native stamp, rather
1121
+ // than a reusable name. Claude's operation marker is read, never cleared.
1122
+ const result = tmux("send-keys", "-t", before.native.native_id, "Escape");
1123
+ if (result.code !== 0)
1124
+ throw new ExecutionError("EXECUTION_UNCONFIRMED", "possible");
1125
+ return { schema: "aiterm.pty-key-result.v1", session_id: name, key: "Escape", outcome: "accepted",
1126
+ interruption: "unconfirmed", session_preserved: "unconfirmed" };
1127
+ }
1128
+ /** Private, read-only native record/identity evidence. This is deliberately not
1129
+ * an agent_stop result or finish proof. Even an exact native abort plus stable
1130
+ * observed identities does not establish the old invocation/parent-return
1131
+ * fence, or prevent a concurrent endpoint switch after these observations.
1132
+ */
1133
+ export function observeExpectedAgentRunInterruption(input) {
1134
+ const point = executionPointSchema.parse(input), name = point.launch.session_id;
1135
+ if (point.boundary && !point.boundary.turn_id && !point.boundary.steer_id
1136
+ && loadAgentMetadata(name).kind === "claude")
1137
+ return observeManagedClaudeInterruption(point);
1138
+ if (!point.boundary?.turn_id)
1139
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1140
+ const snapshot = () => {
1141
+ const generation = observeExecutionSessionGeneration(name), meta = loadAgentMetadata(name);
1142
+ if (!generation.exists || !generation.launch_id || !meta.execution_native)
1143
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1144
+ if (generation.launch_id !== point.launch.launch_id || meta.launch_id !== point.launch.launch_id)
1145
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1146
+ if (meta.kind !== "codex")
1147
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1148
+ const process = observeExecutionParentProcess(name);
1149
+ const actor = process?.harness_process, record = codexInterruptionRecord(meta);
1150
+ if (process?.schema !== "aiterm.execution-parent-process.v1" || !isDeepStrictEqual(process.source?.launch, point.launch)
1151
+ || process.harness !== "codex-cli" || !actor || !Number.isSafeInteger(actor.pid) || actor.pid < 1
1152
+ || !Number.isFinite(Date.parse(actor.started_identity)) || !record)
1153
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1154
+ const after = observeExecutionSessionGeneration(name), last = loadAgentMetadata(name);
1155
+ if (!after.exists || after.launch_id !== point.launch.launch_id || last.kind !== meta.kind
1156
+ || !isDeepStrictEqual(last.execution_native, meta.execution_native))
1157
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1158
+ return { native: meta.execution_native, actor, record };
1159
+ };
1160
+ const before = snapshot(), after = snapshot();
1161
+ if (!isDeepStrictEqual(before, after))
1162
+ throw new ExecutionError("EXECUTION_CONFLICT");
1163
+ const boundary = after.record.boundary;
1164
+ const state = boundary.turn_id !== point.boundary.turn_id ? "other_run"
1165
+ : boundary.type === "turn_aborted" && boundary.reason === "interrupted" ? "interrupted"
1166
+ : boundary.type === "task_started" ? "running" : boundary.type === "task_complete" ? "completed" : "unconfirmed";
1167
+ return { schema: "aiterm.native-interruption-observation.v1", run: point,
1168
+ native: after.native, harness: "codex-cli", harness_process: after.actor,
1169
+ endpoint: { session_id: after.record.session_id, record_file: after.record.file },
1170
+ recorded_state: state, native_boundary: boundary,
1171
+ invocation_fence: "unknown", session_preserved: "unconfirmed" };
1172
+ }
1173
+ /** Existing Aiterm managed-operation provenance, not a native processing-start
1174
+ * or interruption proof. Reservation/receipt creation precedes input sending;
1175
+ * neither a marker nor its removal proves the CLI accepted or aborted it.
1176
+ * Keep the caller's original Point/cursor, never the current event-file size,
1177
+ * cached vendor conversation ID, latest job or another managed operation.
1178
+ */
1179
+ function matchesClaudeInterruptionMarker(marker, point) {
1180
+ const boundary = point.boundary;
1181
+ return !!marker && !!boundary && boundary.turn_id === null && boundary.steer_id === null
1182
+ && marker.operationId === boundary.operation_id
1183
+ && (marker.operationId !== null || marker.eventCursor !== null)
1184
+ && (marker.eventCursor === null || marker.eventCursor === boundary.event_cursor);
1185
+ }
1186
+ function observeManagedClaudeInterruption(point) {
1187
+ const name = point.launch.session_id, operation = point.boundary?.operation_id;
1188
+ if (!point.boundary || point.boundary.turn_id !== null || point.boundary.steer_id !== null)
1189
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1190
+ const files = (meta) => {
1191
+ const marker = agentClaudeOperationPath(name, point.launch.launch_id);
1192
+ const receipt = operation ? agentClaudeDispatchReceiptPath(name, point.launch.launch_id, operation) : null;
1193
+ const identity = (file, empty) => {
1194
+ const stat = fs.lstatSync(file, { bigint: true });
1195
+ if (!stat.isFile() || (empty && stat.size !== 0n) || !Number.isSafeInteger(Number(stat.dev)) || !Number.isSafeInteger(Number(stat.ino)))
1196
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1197
+ return { device: Number(stat.dev), inode: Number(stat.ino), size: stat.size, mtime: stat.mtimeNs, ctime: stat.ctimeNs };
1198
+ };
1199
+ try {
1200
+ const before = { marker: identity(marker, false), receipt: receipt ? identity(receipt, true) : null };
1201
+ if (!matchesClaudeInterruptionMarker(readClaudeOperationMarker(meta), point))
1202
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1203
+ const after = { marker: identity(marker, false), receipt: receipt ? identity(receipt, true) : null };
1204
+ if (!isDeepStrictEqual(before, after))
1205
+ throw new ExecutionError("EXECUTION_CONFLICT");
1206
+ return after;
1207
+ }
1208
+ catch (error) {
1209
+ if (error instanceof ExecutionError)
1210
+ throw error;
1211
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1212
+ }
1213
+ };
1214
+ const snapshot = () => {
1215
+ const generation = observeExecutionSessionGeneration(name), meta = loadAgentMetadata(name);
1216
+ if (!generation.exists || !generation.launch_id || meta.kind !== "claude" || !meta.execution_native)
1217
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1218
+ if (generation.launch_id !== point.launch.launch_id || meta.launch_id !== point.launch.launch_id)
1219
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1220
+ const beforeFiles = files(meta);
1221
+ const process = observeExecutionParentProcess(name);
1222
+ const actor = process?.harness_process;
1223
+ if (process?.schema !== "aiterm.execution-parent-process.v1" || !isDeepStrictEqual(process.source?.launch, point.launch)
1224
+ || process.harness !== "claude-code" || !actor || !Number.isSafeInteger(actor.pid) || actor.pid < 1
1225
+ || !Number.isFinite(Date.parse(actor.started_identity)))
1226
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1227
+ const after = observeExecutionSessionGeneration(name), last = loadAgentMetadata(name);
1228
+ if (!after.exists || after.launch_id !== point.launch.launch_id || last.kind !== meta.kind
1229
+ || !isDeepStrictEqual(last.execution_native, meta.execution_native))
1230
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1231
+ const afterFiles = files(last);
1232
+ if (!isDeepStrictEqual(beforeFiles, afterFiles))
1233
+ throw new ExecutionError("EXECUTION_CONFLICT");
1234
+ return { native: meta.execution_native, actor, files: afterFiles };
1235
+ };
1236
+ const before = snapshot(), after = snapshot();
1237
+ if (!isDeepStrictEqual(before, after))
1238
+ throw new ExecutionError("EXECUTION_CONFLICT");
1239
+ const reference = (file) => ({ device: file.device, inode: file.inode });
1240
+ const common = { run: point, native: after.native, harness: "claude-code", harness_process: after.actor,
1241
+ recorded_state: "unconfirmed", invocation_fence: "unknown", session_preserved: "unconfirmed" };
1242
+ if (!operation)
1243
+ return { ...common, schema: "aiterm.claude-anonymous-interruption-observation.v1",
1244
+ endpoint: { session_id: null, operation_marker: reference(after.files.marker), dispatch_receipt: null },
1245
+ native_boundary: { type: "anonymous_turn_reserved", event_cursor: point.boundary.event_cursor },
1246
+ marker_protocol: "aiterm.claude-operation-marker.v2", loaded_hook: "unconfirmed" };
1247
+ return { ...common, schema: "aiterm.claude-managed-interruption-observation.v1",
1248
+ endpoint: { session_id: null, operation_marker: reference(after.files.marker), dispatch_receipt: reference(after.files.receipt) },
1249
+ native_boundary: { type: "managed_operation_reserved", operation_id: operation } };
1250
+ }
1251
+ /** Select the current native run for an explicit expected launch. The cursor
1252
+ * is the existing native rollout's task_started byte boundary, NOT an alias for a
1253
+ * dispatch/before_send receipt. Callers must retain that distinction; the
1254
+ * cascade controller reports missing canonical aliases rather than rekeying.
1255
+ * No metadata update, native key, latest-job lookup or cached vendor fallback.
1256
+ */
1257
+ export function selectExpectedAgentRunInterruption(input, candidates) {
1258
+ const launch = executionPointSchema.shape.launch.parse(input), name = launch.session_id;
1259
+ const generation = observeExecutionSessionGeneration(name), meta = loadAgentMetadata(name);
1260
+ if (!generation.exists || !generation.launch_id)
1261
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1262
+ if (generation.launch_id !== launch.launch_id || meta.launch_id !== launch.launch_id)
1263
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1264
+ if (meta.kind === "claude") {
1265
+ const marker = readClaudeOperationMarker(meta);
1266
+ if (!marker)
1267
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1268
+ const references = executionPointSchema.array().parse(candidates ? Array.isArray(candidates) ? candidates : [candidates] : []);
1269
+ const point = references.find(value => isDeepStrictEqual(value.launch, launch) && matchesClaudeInterruptionMarker(marker, value));
1270
+ if (!point)
1271
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1272
+ // An existing operation receipt can supply its own canonical boundary.
1273
+ // Pick a matching recorded reference, not a latest job or inferred native
1274
+ // alias. Other work/Points do not veto Esc; unresolved aliases stay open.
1275
+ // This managed reservation is still unconfirmed, never native running.
1276
+ return observeManagedClaudeInterruption(point);
1277
+ }
1278
+ if (meta.kind !== "codex")
1279
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1280
+ const record = codexInterruptionRecord(meta);
1281
+ if (!record || record.boundary.type !== "task_started")
1282
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1283
+ const point = { launch, boundary: { event_cursor: record.event_cursor,
1284
+ turn_id: record.boundary.turn_id, operation_id: null, steer_id: null } };
1285
+ const observation = observeExpectedAgentRunInterruption(point);
1286
+ const last = codexInterruptionRecord(loadAgentMetadata(name));
1287
+ if (!isDeepStrictEqual(last, record) || observation.recorded_state !== "running")
1288
+ throw new ExecutionError("EXECUTION_CONFLICT");
1289
+ return observation;
971
1290
  }
972
1291
  // end 以下で最大の UTF-8 文字境界を返す(B3)。末尾が不完全なマルチバイト列なら、その開始位置まで
973
1292
  // 戻す。pipe-pane が多バイト文字の途中でフラッシュした瞬間の増分読みで先頭/末尾が U+FFFD 化するのを防ぐ。
@@ -1328,6 +1647,56 @@ export function observeSession(name, cursor) {
1328
1647
  };
1329
1648
  return result;
1330
1649
  }
1650
+ /** Internal generation admission read: same strict session listing/managed
1651
+ * metadata as observeSession, without screen or OS-process activity sampling.
1652
+ * No public tool is added. Call synchronously immediately before an effect.
1653
+ */
1654
+ export function observeSessionGeneration(name) {
1655
+ assertSessionName(name);
1656
+ const exists = listSessionsResult().sessions.some(session => session.session_id === name);
1657
+ return { schema: "aiterm.pty-observe-result.v1", session_id: name, exists,
1658
+ launch_id: exists ? tryLoadAgentMetadata(name)?.launch_id ?? null : null };
1659
+ }
1660
+ /** Private guarded-runtime read. Legacy metadata without an original native
1661
+ * creator stamp remains a present but unverified generation, never enrolled
1662
+ * from a current same-name PID or cached vendor session.
1663
+ */
1664
+ export function observeExecutionSessionGeneration(name) {
1665
+ assertSessionName(name);
1666
+ const result = (exists, launch_id) => ({ schema: "aiterm.pty-observe-result.v1",
1667
+ session_id: name, exists, launch_id });
1668
+ const before = observeExecutionNativeCreationScope();
1669
+ if (before.state !== "known")
1670
+ throw new ExecutionError("EXECUTION_UNCONFIRMED");
1671
+ const native = before.sessions.find(value => value.session_id === name);
1672
+ if (!native)
1673
+ return result(false, null);
1674
+ const meta = tryLoadAgentMetadata(name), proof = meta?.execution_native;
1675
+ if (!meta || !proof || !before.server || proof.native_id !== native.native_id
1676
+ || !isDeepStrictEqual(proof.server, before.server))
1677
+ return result(true, null);
1678
+ const after = observeExecutionNativeCreationScope(), last = tryLoadAgentMetadata(name);
1679
+ if (after.state !== "known" || !isDeepStrictEqual(after.server, proof.server)
1680
+ || after.sessions.find(value => value.session_id === name)?.native_id !== proof.native_id
1681
+ || last?.launch_id !== meta.launch_id || !isDeepStrictEqual(last.execution_native, proof))
1682
+ return result(true, null);
1683
+ return result(true, meta.launch_id);
1684
+ }
1685
+ /** Private source-provenance snapshot. It deliberately omits screen, argv,
1686
+ * environment and cached vendor_session_id. No public tool is added. The
1687
+ * consumer proves its native serving MCP process descends from this unique
1688
+ * harness in this same process table, and separately verifies the endpoint.
1689
+ */
1690
+ export function observeExecutionParentProcess(name) {
1691
+ assertSessionName(name);
1692
+ return readExecutionParentProcess(name, {
1693
+ present: () => listSessionsResult().sessions.some(session => session.session_id === name),
1694
+ metadata: () => tryLoadAgentMetadata(name),
1695
+ pane: () => tmux("display-message", "-p", "-t", name, "#{pane_pid}\t#{pane_dead}"),
1696
+ processes: readRuntimeProcesses,
1697
+ select: (meta, rows, pid) => ({ harness: agentHarness(meta.kind), processes: selectHarnessProcesses(meta, rows, processSubtree(rows, pid)) }),
1698
+ });
1699
+ }
1331
1700
  export function listSessionsResult(envKeys = []) {
1332
1701
  for (const key of envKeys) {
1333
1702
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
@@ -1515,6 +1884,7 @@ function closeSessionInternal(name, observeDependency = true, o = {}) {
1515
1884
  return `closed ${name}`;
1516
1885
  }
1517
1886
  export function closeSession(name) {
1887
+ assertExecutionContext(name);
1518
1888
  return closeSessionInternal(name, true);
1519
1889
  }
1520
1890
  /**
@@ -1525,6 +1895,7 @@ export function closeSession(name) {
1525
1895
  * already_closedとなり、close完了を文字列解析なしで確定できる。
1526
1896
  */
1527
1897
  export function closeSessionResult(name) {
1898
+ assertExecutionContext(name);
1528
1899
  assertSessionName(name);
1529
1900
  const existed = sessionExists(name);
1530
1901
  closeSessionInternal(name, true, { letHarnessExit: true });
@@ -1534,7 +1905,78 @@ export function closeSessionResult(name) {
1534
1905
  outcome: existed ? "closed" : "already_closed",
1535
1906
  };
1536
1907
  }
1908
+ /** Cascade-stop adapter only: caller must already own the shared issued close
1909
+ * slot across every serving mutation path. No graceful idle wait, name-only
1910
+ * kill, cleanup of a replacement's metadata, or retry. Not publicly mounted.
1911
+ */
1912
+ export function observeExecutionNativeSessions() {
1913
+ const result = tmux("list-sessions", "-F", "#{session_id}\t#{session_name}");
1914
+ if (result.code === 0)
1915
+ return parseExecutionNativeSessions(result.stdout);
1916
+ // A generic connection/list failure cannot prove absence.
1917
+ if (/^no server running on /i.test(result.stderr.trim()))
1918
+ return { state: "known", sessions: [] };
1919
+ return { state: "unknown" };
1920
+ }
1921
+ /** Existing POSIX server owner plus complete session listing. No screen,
1922
+ * cached generation, new process, server bootstrap or native mutation.
1923
+ */
1924
+ export function observeExecutionNativeCreationScope() {
1925
+ if (isWin)
1926
+ return { state: "unknown" };
1927
+ try {
1928
+ const read = () => tmux("list-sessions", "-F", "#{pid}\t#{session_id}\t#{session_name}");
1929
+ const first = read();
1930
+ if (first.code !== 0)
1931
+ return /^no server running on /i.test(first.stderr.trim())
1932
+ ? { state: "known", sessions: [], server: null } : { state: "unknown" };
1933
+ const rows = first.stdout.trim().split(/\r?\n/).filter(Boolean).map(line => line.split("\t"));
1934
+ if (!rows.length)
1935
+ return { state: "known", sessions: [], server: null };
1936
+ const pid = Number(rows[0][0]);
1937
+ if (!Number.isSafeInteger(pid) || pid < 1 || rows.some(row => row.length !== 3 || row[0] !== String(pid)))
1938
+ return { state: "unknown" };
1939
+ const parsed = parseExecutionNativeSessions(rows.map(row => `${row[1]}\t${row[2]}`).join("\n"));
1940
+ if (parsed.state !== "known")
1941
+ return parsed;
1942
+ const before = readProcessIdentities([pid]), afterListing = read(), after = readProcessIdentities([pid]);
1943
+ if (before.length !== 1 || before[0].pid !== pid || !before[0].started_identity || after.length !== 1
1944
+ || after[0].pid !== pid || after[0].started_identity !== before[0].started_identity
1945
+ || afterListing.code !== 0 || afterListing.stdout !== first.stdout)
1946
+ return { state: "unknown" };
1947
+ return { ...parsed, server: { pid, started_identity: before[0].started_identity } };
1948
+ }
1949
+ catch {
1950
+ return { state: "unknown" };
1951
+ }
1952
+ }
1953
+ /** Own the shared issued close slot before calling this primitive. */
1954
+ export function closeExpectedAgentSession(expected) {
1955
+ assertExecutionContext(expected.session_id);
1956
+ const initial = observeExecutionSessionGeneration(expected.session_id);
1957
+ if (initial.exists && initial.launch_id === null)
1958
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1959
+ if (initial.exists && initial.launch_id !== expected.launch_id)
1960
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1961
+ closeExpectedExecution(expected, {
1962
+ sessions() {
1963
+ const current = observeExecutionSessionGeneration(expected.session_id);
1964
+ if (current.exists && current.launch_id !== expected.launch_id)
1965
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
1966
+ return observeExecutionNativeCreationScope();
1967
+ },
1968
+ launch(name) {
1969
+ const current = observeExecutionSessionGeneration(name);
1970
+ return current.exists && current.launch_id ? { session_id: name, launch_id: current.launch_id } : null;
1971
+ },
1972
+ kill(nativeId) {
1973
+ bindExecutionNativeTarget(expected.session_id, nativeId);
1974
+ return tmux("kill-session", "-t", nativeId);
1975
+ },
1976
+ });
1977
+ }
1537
1978
  export function killAll() {
1979
+ assertExecutionContext(null);
1538
1980
  // agentsはAITERM_STATE_BASEを共有する別socketの席も含み得る。終了対象はこのsocketから確定する。
1539
1981
  const sessions = new Set();
1540
1982
  const listed = tmux("list-sessions", "-F", "#{session_name}");
@@ -1640,6 +2082,8 @@ function createAgentLineageContext(kind, sessionId, seed) {
1640
2082
  }
1641
2083
  export function runAgentApproval(options) {
1642
2084
  const { action, session_id: name, approval_choice: selected, observed_prompt_digest: expected } = options;
2085
+ if (action === "respond")
2086
+ assertExecutionContext(name);
1643
2087
  assertSessionName(name);
1644
2088
  if (action === "inspect" && (selected !== undefined || expected !== undefined))
1645
2089
  throw new AitermError("inspectでは承認選択とdigestを指定できません", 2);
@@ -1710,10 +2154,17 @@ let agentTuiReadyStableSamplesTestOverride = null;
1710
2154
  function writeClaudeOperationMarker(meta, operationId) {
1711
2155
  if (meta.kind !== "claude")
1712
2156
  throw new AitermError("operation_id はClaude agent sessionだけで使用できます", 2);
1713
- writeJson0600(agentClaudeOperationPath(meta.aiterm_session, meta.launch_id), {
1714
- schema: "aiterm.claude-operation-marker.v1",
1715
- operation_id: operationId === null ? null : validateOperationId(operationId),
1716
- });
2157
+ const operation = operationId === null ? null : validateOperationId(operationId);
2158
+ // Only the same private request frame can supply this anonymous reference.
2159
+ // Unmounted/legacy execution keeps v1; never infer a cursor from file size.
2160
+ const captured = operation === null ? readExecutionSendBoundary(meta.aiterm_session) : null;
2161
+ if (captured && (!isDeepStrictEqual(captured.launch, { session_id: meta.aiterm_session, launch_id: meta.launch_id })
2162
+ || !captured.boundary || captured.boundary.operation_id !== null || captured.boundary.turn_id !== null
2163
+ || captured.boundary.steer_id !== null))
2164
+ throw new ExecutionError("EXECUTION_CONFLICT");
2165
+ withExecutionPreparation(meta.aiterm_session, () => writeJson0600(agentClaudeOperationPath(meta.aiterm_session, meta.launch_id), captured ? {
2166
+ schema: "aiterm.claude-operation-marker.v2", operation_id: null, event_cursor: captured.boundary.event_cursor,
2167
+ } : { schema: "aiterm.claude-operation-marker.v1", operation_id: operation }));
1717
2168
  }
1718
2169
  function readClaudeOperationMarker(meta) {
1719
2170
  if (meta.kind !== "claude")
@@ -1741,14 +2192,12 @@ function readClaudeOperationMarker(meta) {
1741
2192
  catch {
1742
2193
  throw new AitermError("Claude operation markerを読めません", 2);
1743
2194
  }
1744
- const keys = value && typeof value === "object" && !Array.isArray(value) ? Object.keys(value).sort() : [];
1745
- if (keys.join(",") !== "operation_id,schema" ||
1746
- value.schema !== "aiterm.claude-operation-marker.v1" ||
1747
- (value.operation_id !== null &&
1748
- (typeof value.operation_id !== "string" || !OPERATION_ID_RE.test(value.operation_id)))) {
2195
+ try {
2196
+ return parseClaudeOperationMarker(value);
2197
+ }
2198
+ catch {
1749
2199
  throw new AitermError("Claude operation markerが不正です", 2);
1750
2200
  }
1751
- return { operationId: value.operation_id };
1752
2201
  }
1753
2202
  finally {
1754
2203
  fs.closeSync(fd);
@@ -1892,6 +2341,8 @@ function assertExpectedClaudeOperation(meta, expectedOperationId) {
1892
2341
  return active;
1893
2342
  }
1894
2343
  export function runClaudeApproval({ action, session_id: name, operation_id: operationIdInput, approval_choice: approvalChoice, observed_prompt_digest: observedPromptDigest, }) {
2344
+ if (action === "respond")
2345
+ assertExecutionContext(name);
1895
2346
  assertSessionName(name);
1896
2347
  if (!sessionExists(name))
1897
2348
  throw new AitermError(`session '${name}' が無い`, 2);
@@ -2435,7 +2886,13 @@ function loadAgentMetadata(name) {
2435
2886
  const deliveryFields = m.initial_prompt_delivery === undefined ? {} : {
2436
2887
  initial_prompt_delivery: m.initial_prompt_delivery, initial_prompt_cursor: m.initial_prompt_cursor,
2437
2888
  };
2438
- const executableFields = m.agent_executable === undefined ? {} : { agent_executable: m.agent_executable };
2889
+ const parsedNative = m.execution_native === undefined ? null : executionNativeBindingSchema.safeParse(m.execution_native);
2890
+ if (parsedNative && !parsedNative.success)
2891
+ throw new AitermError("agent native bindingが不正です", 2);
2892
+ const executableFields = {
2893
+ ...(m.agent_executable === undefined ? {} : { agent_executable: m.agent_executable }),
2894
+ ...(parsedNative?.success ? { execution_native: parsedNative.data } : {}),
2895
+ };
2439
2896
  // 席のファストの状態と、起動・設定替えで渡したmodel(ADR 0114)。この項目より前の版が書いた控えには無い。
2440
2897
  const fastFields = {
2441
2898
  ...(typeof m.fast === "boolean" ? { fast: m.fast } : {}),
@@ -3421,7 +3878,11 @@ async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
3421
3878
  else {
3422
3879
  readyStreak = 0;
3423
3880
  settledStreak = 0;
3424
- if (isAgentTuiActionRequired(kind, lastScreen))
3881
+ // The one keep-current key can be accepted before the TUI redraws.
3882
+ // Wait for that exact handled modal to disappear; never resend its key.
3883
+ const recoveringModal = opts.codexRateLimitRecovery && kind === "codex"
3884
+ && codexRateLimitModelSwitchDialog(lastScreen) !== null;
3885
+ if (isAgentTuiActionRequired(kind, lastScreen) && !recoveringModal)
3425
3886
  return { ready: false, samples, lastScreen };
3426
3887
  }
3427
3888
  if (now() >= deadline) {
@@ -3454,7 +3915,7 @@ function agentTurnSettled(meta) {
3454
3915
  return false;
3455
3916
  }
3456
3917
  async function waitAgentTuiReady(name, meta, timeoutMs = AGENT_TUI_READY_TIMEOUT_MS, o = {}) {
3457
- const result = await waitAgentTuiReadyImpl(meta.kind, () => captureScreen(name, AGENT_TUI_READY_LINES), sleep, { timeoutMs, launchLine: agentLaunchLines.get(name), ...(o.settledTurn ? { turnSettled: () => agentTurnSettled(meta) } : {}) });
3918
+ const result = await waitAgentTuiReadyImpl(meta.kind, () => captureScreen(name, AGENT_TUI_READY_LINES), sleep, { timeoutMs, codexRateLimitRecovery: o.codexRateLimitRecovery, launchLine: agentLaunchLines.get(name), ...(o.settledTurn ? { turnSettled: () => agentTurnSettled(meta) } : {}) });
3458
3919
  if (result.ready)
3459
3920
  agentLaunchLines.delete(name);
3460
3921
  return result;
@@ -3795,6 +4256,7 @@ async function prepareAgentInput(name, meta, options) {
3795
4256
  return { status: "ready", reason: "composer_ready" };
3796
4257
  }
3797
4258
  export async function sendInitialAgentPrompt(name, text, o = {}) {
4259
+ assertExecutionContext(name);
3798
4260
  assertSessionName(name);
3799
4261
  loadAgentMetadata(name);
3800
4262
  // 起動時promptの準備中に届いたpty_sendは、起動時promptを送り終えてから振り分ける(ADR 0088)。
@@ -3824,6 +4286,9 @@ async function sendInitialAgentPromptInLock(name, text, o) {
3824
4286
  prepareSendText(promptText, { raw: false });
3825
4287
  await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
3826
4288
  harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: null });
4289
+ captureExecutionSendBoundary({ launch: { session_id: name, launch_id: meta.launch_id }, boundary: {
4290
+ event_cursor: startOffset, operation_id: null, turn_id: null, steer_id: null
4291
+ } });
3827
4292
  if (meta.kind === "claude") {
3828
4293
  prepareSendText(text, { raw: false });
3829
4294
  reserveAnonymousClaudeTurn(meta);
@@ -3972,11 +4437,11 @@ async function recoverCodexRateLimitModelSwitch(name, meta) {
3972
4437
  async function waitAgentTuiReadyAfterCodexRateLimitRecovery(name, meta, timeoutMs, o = {}) {
3973
4438
  let codexRateLimitModelSwitch = await recoverCodexRateLimitModelSwitch(name, meta);
3974
4439
  // 上限のmodalを閉じた直後は、画面が落ち着くのを11回の確かめで待つ。
3975
- let ready = await waitAgentTuiReady(name, meta, timeoutMs, codexRateLimitModelSwitch ? {} : o);
4440
+ let ready = await waitAgentTuiReady(name, meta, timeoutMs, codexRateLimitModelSwitch ? { codexRateLimitRecovery: true } : o);
3976
4441
  if (!ready.ready && !codexRateLimitModelSwitch) {
3977
4442
  codexRateLimitModelSwitch = await recoverCodexRateLimitModelSwitch(name, meta);
3978
4443
  if (codexRateLimitModelSwitch)
3979
- ready = await waitAgentTuiReady(name, meta, timeoutMs);
4444
+ ready = await waitAgentTuiReady(name, meta, timeoutMs, { codexRateLimitRecovery: true });
3980
4445
  }
3981
4446
  return { ready, codexRateLimitModelSwitch };
3982
4447
  }
@@ -4069,6 +4534,8 @@ function launchAuthProcess(name, meta) {
4069
4534
  * (別のアカウントへ入り直す時と、期限切れを状態から見抜けない時のため)。資格情報を消すことはせず、置き換えは公式CLIが行う。
4070
4535
  */
4071
4536
  export async function authenticateAgent(kind, options) {
4537
+ if (options.action !== "status" || options.session_id !== undefined)
4538
+ assertExecutionContext(options.session_id);
4072
4539
  const result = (status, session, message, pane) => ({ schema: "aiterm.agent-auth-result.v1", harness: agentHarness(kind),
4073
4540
  status, session_id: session, url: pane?.url ?? null, user_code: pane?.user_code ?? null,
4074
4541
  input_required: pane?.input_required ?? false, message: message ?? pane?.message ?? null });
@@ -4891,6 +5358,7 @@ async function applyLaunchFast(name, meta) {
4891
5358
  }
4892
5359
  }
4893
5360
  export async function configureAgent(name, opts) {
5361
+ assertExecutionContext(name);
4894
5362
  assertSessionName(name);
4895
5363
  const model = opts.model?.trim() || null;
4896
5364
  const effort = opts.reasoning_effort?.trim() || null;
@@ -4941,8 +5409,10 @@ async function configureAgentInLock(name, model, effort, fast) {
4941
5409
  const rememberModel = () => {
4942
5410
  if (!model)
4943
5411
  return;
4944
- meta.model = model;
4945
- writeAgentMetadata(meta);
5412
+ withExecutionPreparation(name, () => {
5413
+ meta.model = model;
5414
+ writeAgentMetadata(meta);
5415
+ });
4946
5416
  };
4947
5417
  // fast: true は、modelやeffortを替える前に対応を確かめる(断る時は何も変えない)。
4948
5418
  let fastVerified = false;
@@ -5222,6 +5692,7 @@ async function assertCursorPromptNotHookBlocked(name) {
5222
5692
  } while (performance.now() < deadline || (performance.now() < hookDeadline && cursorPromptHooksRunning(screen)));
5223
5693
  }
5224
5694
  export async function dispatchAgentTurn(name, text, o = {}) {
5695
+ assertExecutionContext(name);
5225
5696
  assertSessionName(name);
5226
5697
  loadAgentMetadata(name);
5227
5698
  return withAgentSendLock(name, () => dispatchAgentTurnInLock(name, text, o));
@@ -5309,6 +5780,9 @@ async function dispatchAgentTurnInLock(name, text, o) {
5309
5780
  prepareSendText(dispatchText, { raw: o.raw });
5310
5781
  await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
5311
5782
  harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: operationId });
5783
+ captureExecutionSendBoundary({ launch: { session_id: name, launch_id: meta.launch_id }, boundary: {
5784
+ event_cursor: startOffset, operation_id: operationId, turn_id: null, steer_id: null
5785
+ } });
5312
5786
  recordInterimBoundary(meta, startOffset, operationId);
5313
5787
  if (meta.kind === "claude") {
5314
5788
  // durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
@@ -5462,6 +5936,7 @@ async function clearClaudeSelectMenuBeforeInput(name) {
5462
5936
  * 同じsessionへの送信は1本ずつ通し、重なった後の1通は前の1通が終わってから振り分け直す(ADR 0088)。
5463
5937
  */
5464
5938
  export async function sendAgentMessage(name, text, o = {}) {
5939
+ assertExecutionContext(name);
5465
5940
  assertSessionName(name);
5466
5941
  loadAgentMetadata(name);
5467
5942
  // 前置きは、順番待ちにも入力受付の待ちにも入る前に確かめる(外れた時に待たせない)。
@@ -5596,6 +6071,8 @@ async function steerRunningTurn(name, meta, text, o) {
5596
6071
  return receipt;
5597
6072
  }
5598
6073
  export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, before_send, }) {
6074
+ if (action === "issue")
6075
+ assertExecutionContext(name);
5599
6076
  assertSessionName(name);
5600
6077
  if (action !== "issue" && action !== "recover") {
5601
6078
  throw new AitermError('action は "issue" または "recover" を指定してください', 2);
@@ -5802,6 +6279,7 @@ function existingAgentLaunchResult(kind, sid, model, effort) {
5802
6279
  ];
5803
6280
  }
5804
6281
  export function openAgent(kind, opts = {}) {
6282
+ assertExecutionContext(opts.session_name);
5805
6283
  const label = agentLabel(kind);
5806
6284
  // 前提検証は session を作る前に全部済ませる(失敗の残骸 session を作らない)。
5807
6285
  // model/effort → bin → cwd の順: model/effort 検証は CLI 不在の端末でも同じ結果になる(テスト可能性)。
@@ -5942,23 +6420,37 @@ export function openAgent(kind, opts = {}) {
5942
6420
  let launchNote = "";
5943
6421
  try {
5944
6422
  const lineageContext = createAgentLineageContext(kind, sid, lineageSeed);
5945
- const meta = agentDone
5946
- ? kind === "claude"
5947
- ? createClaudeAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", launchOperationId, launchRequestDigest, lineageContext, model, effort)
5948
- : kind === "codex"
5949
- ? createCodexAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", { model, effort }, writeScope, lineageContext)
5950
- : kind === "cursor"
5951
- ? createCursorAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", writeScope, lineageContext)
5952
- : createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope, lineageContext)
5953
- : null;
5954
- if (meta) {
5955
- meta.agent_executable = bin;
5956
- // 席は通常の速度で始まる(Claude Code・Codexは起動の引数で明示する)。fast版のmodel IDで起こした時だけtrue。
5957
- meta.fast = opts.fast_state === true;
5958
- meta.model = opts.base_model !== undefined ? opts.base_model : model;
5959
- writeAgentMetadata(meta);
5960
- agentMetadataNegativeCache.delete(sid);
5961
- }
6423
+ if (hasExecutionCreationScope())
6424
+ beforeExecutionEffect(sid);
6425
+ const meta = withExecutionPreparation(sid, () => {
6426
+ const meta = agentDone
6427
+ ? kind === "claude"
6428
+ ? createClaudeAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", launchOperationId, launchRequestDigest, lineageContext, model, effort)
6429
+ : kind === "codex"
6430
+ ? createCodexAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", { model, effort }, writeScope, lineageContext)
6431
+ : kind === "cursor"
6432
+ ? createCursorAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", writeScope, lineageContext)
6433
+ : createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope, lineageContext)
6434
+ : null;
6435
+ if (meta) {
6436
+ // Metadata constructors publish their internally generated launch ID.
6437
+ // Stamp its exact native creator before binding and any later input.
6438
+ if (hasExecutionCreationScope())
6439
+ meta.execution_native = executionCreatedNativeBinding(sid);
6440
+ meta.agent_executable = bin;
6441
+ // 席は通常の速度で始まる(Claude Code・Codexは起動の引数で明示する)。fast版のmodel IDで起こした時だけtrue。
6442
+ meta.fast = opts.fast_state === true;
6443
+ meta.model = opts.base_model !== undefined ? opts.base_model : model;
6444
+ // The new physical shell is already pinned by its actual create receipt.
6445
+ // Validate it before publishing metadata, then bind this internally
6446
+ // generated managed launch. Model arguments cannot select this ID.
6447
+ writeAgentMetadata(meta);
6448
+ agentMetadataNegativeCache.delete(sid);
6449
+ }
6450
+ return meta;
6451
+ });
6452
+ if (meta && hasExecutionCreationScope())
6453
+ bindExecutionCreatedLaunch({ session_id: sid, launch_id: meta.launch_id });
5962
6454
  launchNote = buildAgentLaunchNote(kind, model, effort, meta);
5963
6455
  let full;
5964
6456
  if (paneShell === "pwsh" && meta) {
@@ -5982,7 +6474,7 @@ export function openAgent(kind, opts = {}) {
5982
6474
  });
5983
6475
  }
5984
6476
  catch (e) {
5985
- const failure = telemetryOwnedFailure("AITERM.VENDOR_LAUNCHER_FAILED", e);
6477
+ const failure = e instanceof ExecutionError ? e : telemetryOwnedFailure("AITERM.VENDOR_LAUNCHER_FAILED", e);
5986
6478
  // 起動コマンドを投入できなかった session は空のまま残る=残骸を作らない。片付けてから元エラーを伝える。
5987
6479
  try {
5988
6480
  closeSessionInternal(sid, false);
@@ -6020,6 +6512,7 @@ export class AgentLaunchPromptError extends AitermError {
6020
6512
  }
6021
6513
  }
6022
6514
  export async function openAgentWithInitialPrompt(kind, opts = {}) {
6515
+ assertExecutionContext(opts.session_name);
6023
6516
  const mission = opts.prompt ?? null;
6024
6517
  const sourceSessionId = opts.throughline_source_session ?? null;
6025
6518
  const supplementFile = opts.throughline_supplement_file ?? null;