aiterm-mcp 0.61.4 → 0.61.6

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 CHANGED
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.61.6] - 2026-10-22
11
+
12
+ ### Fixed
13
+
14
+ - Agent startup waits asynchronously for the existing metadata mutex before checking pane ownership, restoring its terminal mode, or accepting startup-menu keys. A short concurrent writer after promptless native creation no longer turns an otherwise valid launch into `unknown`. Stops, uncertain slots, generation changes and the existing lock-acquisition deadline still refuse further effects; no launch or input is replayed.
15
+
16
+ ## [0.61.5] - 2026-10-22
17
+
18
+ ### Fixed
19
+
20
+ - In the opt-in POSIX native entry, an ordinary incoming send to a verified caller no longer invalidates that caller's outgoing send between paste and Enter. The incoming slot remains owned by its original request; unknown input, management operations, stops and generation changes remain blockers. Reciprocal sends do not wait on each other.
21
+ - Agent paste/submit segments asynchronously acquire the existing metadata mutex before running their synchronous native gates. Routine graph-writer contention no longer poisons an already pasted input. Each effect still rechecks generations and stop state, and issuance is durable before input; no native command is replayed.
22
+ - BellTeam's explicit promptless management `agent_launch` now uses the serving process/request identity after `pty_close`. Unreferenced prompt delivery, portable forks and automatic names remain refused.
23
+
10
24
  ## [0.61.4] - 2026-10-22
11
25
 
12
26
  ### 修正
@@ -2728,7 +2742,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2728
2742
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2729
2743
  provenance.
2730
2744
 
2731
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.4...HEAD
2745
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.6...HEAD
2746
+ [0.61.6]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.5...v0.61.6
2747
+ [0.61.5]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.4...v0.61.5
2732
2748
  [0.61.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.3...v0.61.4
2733
2749
  [0.61.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.2...v0.61.3
2734
2750
  [0.61.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.1...v0.61.2
package/README.ja.md CHANGED
@@ -209,7 +209,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
209
209
  場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
210
210
  Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
211
211
 
212
- **状態:** 開発継続中 · 現行公開版 **v0.61.4** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.61.6** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
@@ -559,6 +559,7 @@ MCPの本体を初めて使う時まで起こさない中継の後ろにAiterm
559
559
 
560
560
  `agent_launch({ harness, cwd, trust_project: true })`はpromptなしでも既知のworkspace・project hooks・MCP初期同意を
561
561
  進め、入力受付とharness生存を確認して`startup.status="ready"`を返す。指定なしのpromptなし起動は`not_checked`。
562
+ 起動準備は競合するmetadata保存が終わるのを待って続行する。端末回復と起動メニューの各キーでは世代・停止状態を再確認し、native起動が未確認になった場合は自動再実行せず診断用に保持する。
562
563
  Claude Code初回起動の文字表示テーマ選択では、画面で選択済みの項目を確定して起動を続ける。
563
564
  続いてログイン方式の選択が出た場合は`vendor_onboarding_required`を返す。公式の対話型Claude Code CLIで初回設定を完了する。
564
565
  入力受付の待ちは30秒。混んだ端末でagentの画面がまだ描かれていない間に切れた時だけ、起動から50秒まで待つ。
@@ -735,6 +736,8 @@ agent_launch({ "harness": "codex-cli", "remote": { "host": "rabbit" }, "cwd": "/
735
736
 
736
737
  ## 人が覗く
737
738
 
739
+ POSIXの明示native実行構成では、検証済みcallerへ通常メッセージが届く間も、そのcallerから別席への送信を継続できる。callerの世代や各requestのslotは書き換えない。agentの貼付け・Enterはmetadata mutexの競合を入力前に待ち、不明な操作・世代交代・停止では入力を止める。BellTeamは閉じた席をpromptなしの明示管理`agent_launch`で再作成できる。promptを送る呼出しには元の実行参照が必要である。
740
+
738
741
  セッションは共有tmux socket(Windows nativeはpsmux namespace)上にある。`pty_open`/`agent_launch`の戻り値に表示されるattachコマンドで、人間がClaude/Codex/Grok/Cursor harnessの同じ端末へ入り、途中でキーボードを引き取れる。
739
742
  POSIXでは、tmuxのソケットがまだ無くても最初の`agent_launch`でserverを起動できる。Aitermは作成したsessionとserverを実際のreceiptから照合して受付を確定し、不明な結果を入力の再送で補わない。
740
743
 
package/README.md CHANGED
@@ -227,7 +227,7 @@ collection is off by default and performs no network I/O. It ships via
227
227
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
228
228
  Release re-registers the Official MCP Registry entry.
229
229
 
230
- **Status:** actively maintained · current public release **v0.61.4** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
230
+ **Status:** actively maintained · current public release **v0.61.6** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
231
231
 
232
232
  ### Update and rollback
233
233
 
@@ -600,6 +600,7 @@ Owner start times are checked through `/proc` on Linux; elsewhere an existing PI
600
600
 
601
601
  `agent_launch({ harness, cwd, trust_project: true })` completes known workspace, project-hook, and project-MCP startup
602
602
  consent even without a prompt, then verifies input readiness and harness liveness before returning `startup.status="ready"`.
603
+ Startup checks wait for concurrent metadata writers before continuing. Each terminal recovery or startup-menu key still checks the current generation and stop state; an uncertain native launch remains available for status inspection without automatic replay.
603
604
  For Claude Code's first-run text-style menu, it confirms the item already selected on screen before continuing startup.
604
605
  If the CLI then requests an account login method, the launch reports `vendor_onboarding_required`; complete that choice in the official interactive Claude Code CLI.
605
606
  The input-readiness wait is 30 seconds. Only when it expires while the agent has not drawn anything yet (a busy host), the launch
@@ -793,6 +794,8 @@ Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session
793
794
 
794
795
  ## A human can watch
795
796
 
797
+ In the opt-in POSIX native execution entry, an ordinary message arriving at a verified caller can overlap that caller's outgoing send. It does not replace the caller's generation or clear either request's slot. Agent paste/submit segments wait for metadata mutex contention before input; uncertain operations, generation changes and stops still prevent input. BellTeam may explicitly recreate a closed session with a promptless management `agent_launch`; sending a prompt still requires its original execution reference.
798
+
796
799
  Sessions live on a shared tmux socket on POSIX or a shared psmux namespace on native Windows. The attach line printed by `pty_open` and `agent_launch` lets a human attach to the same terminal and intervene, including a Claude/Codex/Grok/Cursor harness session: `tmux -S … attach -t <id>` on POSIX, or `psmux -L <namespace> attach -t <id>` on native Windows.
797
800
  On POSIX, the first `agent_launch` can create the tmux server when its socket does not yet exist. Aiterm verifies the exact created session and server before accepting that launch; an uncertain result is reported without retrying the input.
798
801
 
package/dist/core.js CHANGED
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
16
16
  import * as rtk from "./rtk.js";
17
17
  import { closeExpectedExecution, parseExecutionNativeSessions, executionNativeBindingSchema } from "./execution-close.js";
18
18
  import { executionPointSchema } from "./execution-contract.js";
19
- import { withExecutionInterruptEffectScope, bindExecutionNativeTarget, beforeExecutionEffect, bindExecutionCreatedLaunch, executionCreatedNativeBinding, hasExecutionCreationScope, assertExecutionContext, withExecutionPreparation, captureExecutionSendBoundary, readExecutionSendBoundary, executionManagementCloseTarget } from "./execution-effect.js";
19
+ import { withExecutionInterruptEffectScope, bindExecutionNativeTarget, beforeExecutionEffect, bindExecutionCreatedLaunch, executionCreatedNativeBinding, hasExecutionCreationScope, assertExecutionContext, withExecutionPreparation, captureExecutionSendBoundary, readExecutionSendBoundary, executionManagementCloseTarget, withExecutionNativeAdmission } from "./execution-effect.js";
20
20
  import { ExecutionError } from "./execution-wire.js";
21
21
  import { readExecutionParentProcess } from "./execution-parent-process.js";
22
22
  import { isCursorMcpClient } from "aiterm-steer-delivery";
@@ -209,7 +209,10 @@ function paneTtyOf(name) {
209
209
  * agent が tty 上に実在しない時は触らない(ready gate が入力受付不能として止める)。
210
210
  */
211
211
  export async function ensureAgentOwnsPaneInput(name, kind) {
212
- assertExecutionContext(name);
212
+ // Startup resumes after native creation/readiness awaits. A short metadata
213
+ // writer is not a reason to abandon that already created launch. Wait for
214
+ // admission, then run the unchanged source/target/generation checks.
215
+ await withExecutionNativeAdmission(name, () => assertExecutionContext(name));
213
216
  if (isWin)
214
217
  return [];
215
218
  const tty = paneTtyOf(name);
@@ -225,9 +228,11 @@ export async function ensureAgentOwnsPaneInput(name, kind) {
225
228
  return [];
226
229
  if (state.agentPresent && !state.agentForeground) {
227
230
  const wasStopped = state.agentStopped;
228
- tmux("send-keys", "-t", name, "C-u");
229
- tmux("send-keys", "-l", "-t", name, "fg");
230
- tmux("send-keys", "-t", name, "Enter");
231
+ await withExecutionNativeAdmission(name, () => {
232
+ tmux("send-keys", "-t", name, "C-u");
233
+ tmux("send-keys", "-l", "-t", name, "fg");
234
+ tmux("send-keys", "-t", name, "Enter");
235
+ });
231
236
  const deadline = performance.now() + AGENT_FOREGROUND_RECOVERY_TIMEOUT_MS;
232
237
  for (;;) {
233
238
  await sleep(AGENT_FOREGROUND_RECOVERY_POLL_MS);
@@ -250,11 +255,13 @@ export async function ensureAgentOwnsPaneInput(name, kind) {
250
255
  // stty is a native mutation outside tmux's command gate. Recheck the
251
256
  // captured device and the shared request generation/fence immediately
252
257
  // 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");
258
+ const set = await withExecutionNativeAdmission(name, () => {
259
+ beforeExecutionEffect(name, () => {
260
+ if (paneTtyOf(name) !== tty)
261
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
262
+ });
263
+ return spawnSync("/bin/stty", [ttyFlag, tty, "raw", "-echo"], { encoding: "utf8", timeout: 5000, env: psEnv });
256
264
  });
257
- const set = spawnSync("/bin/stty", [ttyFlag, tty, "raw", "-echo"], { encoding: "utf8", timeout: 5000, env: psEnv });
258
265
  if (set.status !== 0) {
259
266
  throw new AitermError(`AGENT_TTY_COOKED session=${name}\n` +
260
267
  `tty が icanon/echo のままで raw へ戻せません: ${(set.stderr ?? "").trim() || `code=${set.status}`}。送信していません。`, 2);
@@ -4230,7 +4237,7 @@ async function prepareAgentInput(name, meta, options) {
4230
4237
  handled.add(action.kind);
4231
4238
  const keys = action.selected ? action.keys.slice(0, -1) : action.keys;
4232
4239
  for (const key of keys) {
4233
- sendKey(name, key, { preserveAgentOperation: true });
4240
+ await withExecutionNativeAdmission(name, () => sendKey(name, key, { preserveAgentOperation: true }));
4234
4241
  await sleep(AGENT_SUBMIT_DELAY_MS);
4235
4242
  }
4236
4243
  if (action.selected) {
@@ -4238,14 +4245,14 @@ async function prepareAgentInput(name, meta, options) {
4238
4245
  let moved = await waitStartupSelection(name, action.selected);
4239
4246
  if (!moved && keys.length > 0) {
4240
4247
  for (const key of keys) {
4241
- sendKey(name, key, { preserveAgentOperation: true });
4248
+ await withExecutionNativeAdmission(name, () => sendKey(name, key, { preserveAgentOperation: true }));
4242
4249
  await sleep(AGENT_SUBMIT_DELAY_MS);
4243
4250
  }
4244
4251
  moved = await waitStartupSelection(name, action.selected);
4245
4252
  }
4246
4253
  if (!moved)
4247
4254
  break;
4248
- sendKey(name, action.keys.at(-1), { preserveAgentOperation: true });
4255
+ await withExecutionNativeAdmission(name, () => sendKey(name, action.keys.at(-1), { preserveAgentOperation: true }));
4249
4256
  await sleep(AGENT_SUBMIT_DELAY_MS);
4250
4257
  }
4251
4258
  ready = await waitAgentTuiReady(name, meta, options.ready_timeout ?? AGENT_TUI_READY_TIMEOUT_MS);
@@ -5792,29 +5799,31 @@ async function dispatchAgentTurnInLock(name, text, o) {
5792
5799
  prepareSendText(dispatchText, { raw: o.raw });
5793
5800
  await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
5794
5801
  harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: operationId });
5795
- captureExecutionSendBoundary({ launch: { session_id: name, launch_id: meta.launch_id }, boundary: {
5796
- event_cursor: startOffset, operation_id: operationId, turn_id: null, steer_id: null
5797
- } });
5798
- recordInterimBoundary(meta, startOffset, operationId);
5799
- if (meta.kind === "claude") {
5800
- // durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
5801
- // receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
5802
- prepareSendText(text, { raw: o.raw });
5803
- if (operationId) {
5804
- reserveClaudeOperation(meta, operationId);
5802
+ await withExecutionNativeAdmission(name, () => {
5803
+ captureExecutionSendBoundary({ launch: { session_id: name, launch_id: meta.launch_id }, boundary: {
5804
+ event_cursor: startOffset, operation_id: operationId, turn_id: null, steer_id: null
5805
+ } });
5806
+ recordInterimBoundary(meta, startOffset, operationId);
5807
+ if (meta.kind === "claude") {
5808
+ // durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
5809
+ // receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
5810
+ prepareSendText(text, { raw: o.raw });
5811
+ if (operationId) {
5812
+ reserveClaudeOperation(meta, operationId);
5813
+ }
5814
+ else
5815
+ reserveAnonymousClaudeTurn(meta);
5805
5816
  }
5806
- else
5807
- reserveAnonymousClaudeTurn(meta);
5808
- }
5809
- send(name, dispatchText, {
5810
- enter: false,
5811
- force: o.force,
5812
- raw: o.raw,
5813
- mark: false,
5814
- rtk: false,
5815
- preserveAgentOperation: meta.kind === "claude",
5816
- bracketedPaste: true,
5817
- typedPreface: prefaced.typedPreface,
5817
+ send(name, dispatchText, {
5818
+ enter: false,
5819
+ force: o.force,
5820
+ raw: o.raw,
5821
+ mark: false,
5822
+ rtk: false,
5823
+ preserveAgentOperation: meta.kind === "claude",
5824
+ bracketedPaste: true,
5825
+ typedPreface: prefaced.typedPreface,
5826
+ });
5818
5827
  });
5819
5828
  // Cursorの冷間起動ではbracketed pasteの反映が250msを超えることがある。本文がcomposerへ
5820
5829
  // 現れた実測をsubmit条件にし、未反映のままEnterだけを失う競合を作らない。
@@ -5829,7 +5838,7 @@ async function dispatchAgentTurnInLock(name, text, o) {
5829
5838
  // Codex TUI は literal text 投入直後の Enter を取り落とすことがある。agent 経路だけ submit を分離する。
5830
5839
  await sleep(AGENT_SUBMIT_DELAY_MS);
5831
5840
  }
5832
- sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" });
5841
+ await withExecutionNativeAdmission(name, () => sendKey(name, "Enter", { preserveAgentOperation: meta.kind === "claude" }));
5833
5842
  let residue = await detectAgentSubmitResidue(name, meta.kind, dispatchText);
5834
5843
  // Cursorはcomposerへの貼付反映後でも最初のEnterを取り落とすことがある。本文残留を
5835
5844
  // 陽性観測した場合だけ同じEnterを一度再送し、再検査後も残る時は失敗として返す。
@@ -6029,7 +6038,7 @@ async function steerRunningTurn(name, meta, text, o) {
6029
6038
  };
6030
6039
  // Claudeのoperation相関は変えない。差し込みは実行中turnの一部であり、新しいturnを予約しない。
6031
6040
  const preserveAgentOperation = meta.kind === "claude";
6032
- send(name, text, {
6041
+ await withExecutionNativeAdmission(name, () => send(name, text, {
6033
6042
  enter: false,
6034
6043
  force: true,
6035
6044
  raw: o.raw,
@@ -6038,7 +6047,7 @@ async function steerRunningTurn(name, meta, text, o) {
6038
6047
  preserveAgentOperation,
6039
6048
  bracketedPaste: true,
6040
6049
  typedPreface: prefaced.typedPreface,
6041
- });
6050
+ }));
6042
6051
  if (meta.kind === "cursor") {
6043
6052
  const visible = await waitCursorPromptVisible(name, text);
6044
6053
  if (!visible.visible) {
@@ -6048,7 +6057,7 @@ async function steerRunningTurn(name, meta, text, o) {
6048
6057
  else {
6049
6058
  await sleep(AGENT_SUBMIT_DELAY_MS);
6050
6059
  }
6051
- sendKey(name, "Enter", { preserveAgentOperation });
6060
+ await withExecutionNativeAdmission(name, () => sendKey(name, "Enter", { preserveAgentOperation }));
6052
6061
  // GrokとCursorは実行中の送信を待ち行列へ入れる。そのままだと現在turnの完了後に別turnとして動き、
6053
6062
  // 完了通知が差し込み前の回答で届いてしまう。待ち行列へ入ったことを確かめ、標準の「今すぐ送る」で現在turnへ移す。
6054
6063
  const queued = meta.kind === "grok" ? grokSteerQueued
@@ -6064,7 +6073,7 @@ async function steerRunningTurn(name, meta, text, o) {
6064
6073
  `差し込む文が${label}の待ち行列へ入ったことを確認できません。子のturnが送る直前に終わっていた時は、` +
6065
6074
  `文は新しいturnとして始まっており、その完了は通知されません。再送する前にpty_read(screen:true)で確かめてください。`, 2);
6066
6075
  }
6067
- sendKey(name, "Enter");
6076
+ await withExecutionNativeAdmission(name, () => sendKey(name, "Enter"));
6068
6077
  if (!await waitSteerScreen(name, screen => !queued(screen), stillRunning)) {
6069
6078
  throw new AitermError(`STEER_STILL_QUEUED vendor=${meta.kind} session=${name}\n` +
6070
6079
  `差し込む文が${label}の待ち行列に残っています。現在のturnが終わると別turnとして実行されます。`, 2);
@@ -143,12 +143,15 @@ export function createExecutionDispatcher(bindings) {
143
143
  // A host's explicit session close is a management RPC with its own
144
144
  // existing process/request identity, not a delivery or resend.
145
145
  const hostClose = client === "bellteam" && route.tool === "pty_close" && !supplied;
146
+ const hostLaunch = client === "bellteam" && route.tool === "agent_launch" && !supplied
147
+ && args.prompt == null && args.throughline_source_session === undefined;
148
+ const hostManagement = hostClose || hostLaunch;
146
149
  if (args.remote !== undefined && args.remote !== null)
147
150
  throw new ExecutionError("EXECUTION_UNSUPPORTED");
148
151
  let reference = supplied;
149
152
  let directRequest = null;
150
153
  let frozenParent = null;
151
- if (!reference && (client !== "bellteam" || hostClose) && native) {
154
+ if (!reference && (client !== "bellteam" || hostManagement) && native) {
152
155
  directRequest = executionRequestSchema.options[4].parse({ kind: "mcp", process_pid: native.pid,
153
156
  process_started_identity: native.started_identity, request_id: fixedExtra.requestId,
154
157
  tool: route.tool, target_session_id: route.target });
@@ -189,7 +192,7 @@ export function createExecutionDispatcher(bindings) {
189
192
  }
190
193
  else {
191
194
  // BellTeam must retain its existing delivery/room/call reference.
192
- if ((client === "bellteam" && !hostClose) || !directRequest)
195
+ if ((client === "bellteam" && !hostManagement) || !directRequest)
193
196
  throw new ExecutionError("EXECUTION_UNSUPPORTED");
194
197
  const current = await observe(route.target);
195
198
  if (route.stage === "send" && !current.point)
@@ -106,15 +106,46 @@ function prepareInScope(scope, body) {
106
106
  throw scope.failure;
107
107
  }
108
108
  }
109
- export function withExecutionEffectScope(session, issue, body, creation, prepare) {
109
+ export function withExecutionEffectScope(session, issue, body, creation, prepare, admitNative) {
110
110
  const enclosing = scopes.getStore();
111
111
  if (enclosing?.interrupt)
112
112
  refuse(enclosing, "EXECUTION_UNSUPPORTED");
113
113
  const target = executionLaunchSchema.shape.session_id.parse(session);
114
- const scope = { session: target, issue, prepare, nativeTargets: new Set(), didIssue: false,
114
+ const scope = { session: target, issue, prepare, admitNative, nativeTargets: new Set(), didIssue: false,
115
115
  ...(creation ? { creation: { ...creation } } : {}) };
116
116
  return runEffectScope(scope, body);
117
117
  }
118
+ /** Resume one synchronous native segment after asynchronous readiness/waits.
119
+ * Waits for the existing metadata mutex, never replays the native body. The
120
+ * normal gates inside body still check source/target/stop and persist issuance.
121
+ */
122
+ export async function withExecutionNativeAdmission(session, body) {
123
+ const scope = scopes.getStore();
124
+ if (!scope) {
125
+ if (required)
126
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
127
+ return body();
128
+ }
129
+ if (scope.session !== session)
130
+ refuse(scope, "EXECUTION_CONFLICT");
131
+ if (scope.failure)
132
+ throw scope.failure;
133
+ if (!scope.admitNative)
134
+ return body(); // explicit legacy/fake frame
135
+ try {
136
+ return await scope.admitNative(() => {
137
+ if (scope.failure)
138
+ throw scope.failure;
139
+ assertExecutionContext(session);
140
+ return body();
141
+ });
142
+ }
143
+ catch (error) {
144
+ scope.failure ??= error instanceof ExecutionError ? new ExecutionError(error.code, scope.didIssue ? "possible" : error.effect)
145
+ : new ExecutionError("EXECUTION_UNCONFIRMED", scope.didIssue ? "possible" : "not_started");
146
+ throw scope.failure;
147
+ }
148
+ }
118
149
  /** Private stop-owned synchronous frame over the existing native gate. It
119
150
  * creates no invocation/edge/request. The ordinary stop path supplies only
120
151
  * its target check, without a durable journal or completion/fence prerequisite.
@@ -70,11 +70,29 @@ function view(state, record) {
70
70
  stopping: record.stopping || stopped(state, record.relation.source?.launch) || stopped(state, record.relation.target?.launch)
71
71
  || interruptionBlocksRequest(state, record.reservation.request) });
72
72
  }
73
+ /** An admitted ordinary input to the caller does not replace its generation
74
+ * or revoke its outstanding outgoing RPC. Blocking that input's issued slot
75
+ * would make reciprocal sends poison each other between paste and Enter.
76
+ * Unknown operations and management/creation slots are never exempted.
77
+ */
78
+ function blocksSource(state, source, seatHint, own) {
79
+ return state.slots.some(slot => {
80
+ if (slot === own || slot.session_id !== (source?.session_id ?? seatHint))
81
+ return false;
82
+ if (!source || slot.stage !== "send" || slot.state !== "issued" || slot.expected_launch_id !== source.launch_id || !slot.request
83
+ || slot.request.kind === "mcp" && !["pty_send", "claude_turn.issue"].includes(slot.request.tool))
84
+ return true;
85
+ const incoming = state.records.find(value => same(value.reservation.request, slot.request));
86
+ return incoming?.operations.send !== "issued" || ["issued", "unknown"].includes(incoming.operations.launch)
87
+ || incoming.relation.phase !== "submitted_unknown" || incoming.relation.completion !== "open"
88
+ || incoming.relation.lifecycle !== "tracking" || !same(incoming.relation.target?.launch, source);
89
+ });
90
+ }
73
91
  function sourceAdmission(state, point, seatHint) {
74
92
  assertInterruptionSource(state, point, seatHint);
75
93
  if (stopped(state, point?.launch))
76
94
  throw new ExecutionError("EXECUTION_STOPPING");
77
- if (state.slots.some(slot => slot.session_id === (point?.launch.session_id ?? seatHint)))
95
+ if (blocksSource(state, point?.launch ?? null, seatHint))
78
96
  throw new ExecutionError("EXECUTION_UNCONFIRMED");
79
97
  if (!point && seatHint && state.stopping.some(value => value.session_id === seatHint))
80
98
  throw new ExecutionError("EXECUTION_UNCONFIRMED");
@@ -528,6 +546,11 @@ export class ExecutionService {
528
546
  return this.store.transaction(state => view(state, find(state, args.request)), { readOnly: true });
529
547
  return this.finish(args);
530
548
  }
549
+ async admitNative(body) {
550
+ if (!this.store.admitNative)
551
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
552
+ return this.store.admitNative(body);
553
+ }
531
554
  /** Read-only root selection, not a stop or a reservation. Generations come
532
555
  * only from current observation or persisted unfinished relations/slots. A
533
556
  * completed/retired contact never becomes a root just because its name matches.
@@ -748,7 +771,7 @@ export class ExecutionService {
748
771
  throw new ExecutionError("EXECUTION_STOPPING");
749
772
  if (!relation.source && record.source_session_id && state.stopping.some(value => value.session_id === record.source_session_id))
750
773
  throw new ExecutionError("EXECUTION_UNCONFIRMED");
751
- if (state.slots.some(value => value !== slot && value.session_id === (relation.source?.launch.session_id ?? record.source_session_id)))
774
+ if (blocksSource(state, relation.source?.launch ?? null, record.source_session_id, slot))
752
775
  throw new ExecutionError("EXECUTION_UNCONFIRMED");
753
776
  if (relation.source) {
754
777
  const source = this.runtime.observeSync(relation.source.launch.session_id);
@@ -81,6 +81,7 @@ export class FileExecutionStore {
81
81
  directory;
82
82
  lockTimeoutMs;
83
83
  lockActor;
84
+ nativeAdmission = false;
84
85
  constructor(directory, lockTimeoutMs = 2000, lockActor) {
85
86
  this.directory = directory;
86
87
  this.lockTimeoutMs = lockTimeoutMs;
@@ -151,7 +152,8 @@ export class FileExecutionStore {
151
152
  fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
152
153
  let lock;
153
154
  try {
154
- lock = acquireExecutionLock(this.directory, this.lockActor);
155
+ if (!this.nativeAdmission)
156
+ lock = acquireExecutionLock(this.directory, this.lockActor);
155
157
  }
156
158
  catch (error) {
157
159
  if (error instanceof ExecutionError)
@@ -169,10 +171,51 @@ export class FileExecutionStore {
169
171
  return structuredClone(result);
170
172
  }
171
173
  finally {
174
+ if (lock)
175
+ releaseExecutionLock(lock);
176
+ }
177
+ }
178
+ async admitNative(body) {
179
+ // The marker exists only on a synchronous stack, never across an await.
180
+ // Re-entry shares the physical mutex but not a mutable state snapshot.
181
+ if (this.nativeAdmission)
182
+ return this.nativeBody(body);
183
+ fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
184
+ const deadline = performance.now() + this.lockTimeoutMs;
185
+ let lock;
186
+ for (;;) {
187
+ try {
188
+ lock = acquireExecutionLock(this.directory, this.lockActor);
189
+ break;
190
+ }
191
+ catch (error) {
192
+ if (error.code !== "EEXIST" && !(error instanceof ExecutionError && error.code === "EXECUTION_BUSY"))
193
+ throw new ExecutionError("EXECUTION_STATE_UNAVAILABLE", "not_started");
194
+ if (performance.now() >= deadline)
195
+ throw new ExecutionError("EXECUTION_BUSY", "not_started");
196
+ await new Promise(resolve => setTimeout(resolve, 10));
197
+ }
198
+ }
199
+ this.nativeAdmission = true;
200
+ try {
201
+ return this.nativeBody(body);
202
+ }
203
+ finally {
204
+ this.nativeAdmission = false;
172
205
  releaseExecutionLock(lock);
173
206
  }
174
207
  }
208
+ nativeBody(body) {
209
+ const result = body();
210
+ if (result && typeof result.then === "function") {
211
+ void Promise.resolve(result).catch(() => undefined);
212
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
213
+ }
214
+ return result;
215
+ }
175
216
  async transaction(body, options = {}) {
217
+ if (this.nativeAdmission)
218
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
176
219
  fs.mkdirSync(this.directory, { recursive: true, mode: 0o700 });
177
220
  let lock;
178
221
  const deadline = performance.now() + this.lockTimeoutMs;
@@ -134,7 +134,7 @@ async function runExecutionReceipt(service, meta, stage, body, targetSessionId,
134
134
  return service.captureCreatedNative(ref, session, nativeId, expectedServer, serverPid);
135
135
  },
136
136
  launch: launch => service.bindCreatedLaunch(ref, launch),
137
- } : undefined, prepare);
137
+ } : undefined, prepare, body => service.admitNative(body));
138
138
  // A swallowed native refusal poisons the frame. Only a cleanly returned
139
139
  // frame has a genuine successful action receipt to preserve on binding.
140
140
  observed = action.value;
package/docs/DESIGN.md CHANGED
@@ -28,6 +28,15 @@ native emit直前に照合したhook参照は既存deliveryへ保存する。Cod
28
28
  POSIXのraw tty回復もnative作用であり、tmux打鍵と同じ受付を通す。ttyは正確なsession selectorで読み、
29
29
  実stty直前にも同じdeviceを確認する。停止や世代/tty交代後に古いdeviceへ回復を発行しない。
30
30
  一つのRPCが複数のnative作用を発行するときも、各回に同じissued slotとsource/target世代・停止を再確認する。
31
+ agentの貼付け・Enterの同期区間は、既存metadata mutexを非同期に取得してから実行する。
32
+ native作成後に再開する起動準備も同じ非同期取得を通す。pane所有検査、fg/stty回復、
33
+ 起動メニューの各キーを短い同期区間で実行し、待機後の停止・世代・slot検査を省略しない。
34
+ mutexをawait中へ持ち越さず、同じ同期stack内の各gateが発行記録を永続化してからnative作用へ進む。
35
+ 検証済みsourceと同世代への通常sendがissuedである間も、そのsourceの送信先が別ならoutgoing RPCは継続できる。
36
+ incomingのslotを解放・書換・成功扱いせず、unknown・管理操作・世代不一致は従来どおり拒否する。
37
+ incomingの完了待ちをoutgoingへ課して相互送信を循環待ちにしない(ADR 0127)。
38
+ BellTeamのpromptなし明示管理launchは、管理close同様にserving process/request identityへ結合する。
39
+ promptやThroughline読出しを含む未登録送信、auto-name、remoteはこの管理例外へ含めない。
31
40
  新launchは、既存POSIX serverのnative owner、またはserver未作成の完全な空観測を受付時と発行直前に確認する。
32
41
  冷間時は最初のnew-session自身でserverを作り、その実receiptのnative IDとserver PIDを、作成後の安定したnative一覧とprocess開始識別子で照合する。
33
42
  既存server時も冷間時も、作成したsessionのnative ID・server owner・core内部のlaunch世代を同じ要求slotへ段階結合する。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.61.4",
3
+ "version": "0.61.6",
4
4
  "mcpName": "io.github.kitepon/aiterm-mcp",
5
5
  "description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
6
6
  "keywords": [