aiterm-mcp 0.61.7 → 0.61.9

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,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.61.9] - 2026-10-23
11
+
12
+ ### Fixed
13
+
14
+ - Asynchronous agent launches wait for graph admission before their initial checks and their synchronous create/metadata/CLI input segment. CLI validation and readiness waits stay outside that mutex. A short competing writer cannot strand a managed shell between generation binding and CLI input; native effects are never retried.
15
+ - Explicit native `pty_close` can recover a returned unknown launch only when the original native/server and managed generation still match, the raw initial prompt is absent, send remains unissued and the harness is confirmed exited. The close keeps the failed record and releases its slot only after native close and confirmed absence. Issued RPCs, live harnesses and uncertain input or identity remain blocked.
16
+
17
+ ### Added
18
+
19
+ - The BellTeam native execution entry accepts explicit `agent_execution/close_launch` with the original request and exact target generation. It checks that expectation during admission and before close, uses the serving SDK management identity, and returns a versioned `_meta["aiterm/closed_launch"]` receipt only after close, confirmed absence and metadata commit. The original unknown record stays unchanged. Failed or uncertain closes return no closed receipt; later generations and historical receipts cannot authorize another close. The proof supports empty exited Claude Code and Codex launches.
20
+
21
+ ## [0.61.8] - 2026-10-23
22
+
23
+ ### Fixed
24
+
25
+ - Repeating empty-launch reconciliation now rechecks the current managed generation, no-input readiness and competing session operations before returning an accepted launch with an unissued send. A historical accepted receipt cannot authorize clearing a newer generation's launch uncertainty. Rejection preserves the existing graph and issues no native input.
26
+
10
27
  ## [0.61.7] - 2026-10-23
11
28
 
12
29
  ### Fixed
@@ -2753,7 +2770,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2753
2770
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2754
2771
  provenance.
2755
2772
 
2756
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.7...HEAD
2773
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.9...HEAD
2774
+ [0.61.9]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.8...v0.61.9
2775
+ [0.61.8]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.7...v0.61.8
2757
2776
  [0.61.7]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.6...v0.61.7
2758
2777
  [0.61.6]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.5...v0.61.6
2759
2778
  [0.61.5]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.4...v0.61.5
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.7** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.61.9** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
@@ -738,7 +738,13 @@ BellTeamのmodel・effort・fast変更も、既存session世代に結合したna
738
738
 
739
739
  POSIXの明示native入口でも、`agent_auth`の名前なし開始、認証済みで何も起こさない開始、既存v1の確認・初回案内・取消を受け付ける。認証PTYは実際の作成元nativeと世代へ結合する。BellTeamの参照なし入力・キーは、この証拠がある認証PTYにだけ許可し、通常agentの本文には配送参照を引き続き必要とする。作成証拠のない旧認証metadataを自動登録しない。
740
740
 
741
- この明示入口では `agent_execution` の `reconcile_launch` に元の `request` と正確な `target: {session_id, launch_id}` を渡し、結果不明の空起動を既存世代へ結合できる。保存したnative/serverと現在のmanaged世代、raw状態で初期promptなし、生存して入力待ちのharnessをmetadata受付内で照合し、該当するunknown launch枠だけを解放する。起動・打鍵・close・本文再送は行わない。issuedのRPC、入力状態不明、世代交代や世代未確認は拒否する。後続本文は元参照で通常のsend受付を通して一度だけ発行する。照合成功は失われた元RPC receiptの回収を意味しない。
741
+ この明示入口では `agent_execution` の `reconcile_launch` に元の `request` と正確な `target: {session_id, launch_id}` を渡し、結果不明の空起動を既存世代へ結合できる。保存したnative/serverと現在のmanaged世代、raw状態で初期promptなし、生存して入力待ちのharnessをmetadata受付内で照合し、該当するunknown launch枠だけを解放する。受付済みでも本文未発行なら、照合を繰り返すたびに現在世代・入力待ち・競合操作なしを再確認する。保存した受付結果だけで後の世代を確認済みとしない。起動・打鍵・close・本文再送は行わない。issuedのRPC、入力状態不明、世代交代や世代未確認は拒否する。後続本文は元参照で通常のsend受付を通して一度だけ発行する。照合成功は失われた元RPC receiptの回収を意味しない。
742
+
743
+ CLIが終了した結果不明の空起動には、BellTeam native入口の `agent_execution({action: "close_launch", request, target: {session_id, launch_id}})` を明示的に使う。元requestと期待世代を受付内とnative close直前で照合し、原native/server・managed世代一致、raw初期prompt `none`・初期配送なし・send未発行・現在CLI終了の正規probeがすべて必要になる。Claude CodeとCodexで同じ証明条件を使う。生存・issued・送信済み・欠測・世代交代は拒否し、sessionだけのcloseへの代用や自動再起動・再送はしない。旧版は新actionを拒否する。
744
+
745
+ 成功時の `structuredContent` は応答するSDKの別管理requestのExecutionRecordで、元unknown recordは変えない。`_meta["aiterm/closed_launch"]` に厳密なreceiptを返す。固定項目は `schema: "aiterm.closed-launch.v1"`、`version: 1`、`action: "close_launch"`、`outcome: "closed"`、元 `request`、正確な `target`、`management_ref`(応答SDKのMCP request、`tool: "pty_close"`)。`evidence` は原 `native` binding、`initial_prompt: "none"`、`initial_prompt_delivery: null`、`harness_alive: false`、`absence_confirmed: true`、`metadata_committed: true` を持つ。実close・不在確認・slot解放commitが揃った場合だけ返し、失敗や結果不明には返さない。本体が不明を解除する際は元pending操作と自身のstop/versionも照合する。過去receiptで後発の不明を解除しない。
746
+
747
+ 同じ明示入口で、返却済みunknown launchのCLIが終了している場合は、元native/server・managed世代とraw初期promptなし・本文未発行・harness終了を同期検証できた時だけ通常の`pty_close`で対象席を閉じられる。成功と不在を確認して該当枠だけを外し、元unknown requestを保持する。発行中RPC、生存harness、入力や世代の欠測は拒否する。旧requestを再実行せず、必要な後続は新しい通常要求として行う。
742
748
 
743
749
 
744
750
  ## 人が覗く
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.7** · 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.9** · 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
 
@@ -796,7 +796,13 @@ Each `pty_send` accepts at most 64 KiB of UTF-8 text. Sends to the same session
796
796
 
797
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
798
 
799
- An explicit `agent_execution` action `reconcile_launch` can bind an existing empty launch after a lost launch result in this opt-in entry. Supply the original `request` and exact `target: {session_id, launch_id}`. The server checks the original native/server identity, current managed generation, raw no-prompt state and a live ready harness under metadata admission, then releases only that unknown launch slot. It never launches, types, closes or resends. `issued` RPCs, unknown input and changed or unverified generations remain blocked. Any subsequent body uses the original reference through normal send admission once; a successful reconciliation does not prove the lost RPC receipt.
799
+ An explicit `agent_execution` action `reconcile_launch` can bind an existing empty launch after a lost launch result in this opt-in entry. Supply the original `request` and exact `target: {session_id, launch_id}`. The server checks the original native/server identity, current managed generation, raw no-prompt state and a live ready harness under metadata admission, then releases only that unknown launch slot. Repeating an accepted empty launch before its send also rechecks the current generation, readiness and absence of competing session operations; a saved acceptance alone cannot confirm a later generation. It never launches, types, closes or resends. `issued` RPCs, unknown input and changed or unverified generations remain blocked. Any subsequent body uses the original reference through normal send admission once; a successful reconciliation does not prove the lost RPC receipt.
800
+
801
+ For an empty unknown launch whose harness has exited, the BellTeam native entry accepts `agent_execution({action: "close_launch", request, target: {session_id, launch_id}})`. The original request and generation are atomic expectations, checked within admission and immediately before the native close. Raw initial prompt must be `none`, initial delivery absent, send unissued, and the original native/server and managed identity must match. The private runtime proof covers Claude Code and Codex; live, issued, sent, missing or replaced targets remain refused. No session-only close fallback or automatic relaunch/send occurs. Older servers reject this action.
802
+
803
+ Success retains the original unknown record and returns the serving SDK's distinct management execution record in `structuredContent`. `_meta["aiterm/closed_launch"]` is a strict receipt with `schema: "aiterm.closed-launch.v1"`, `version: 1`, `action: "close_launch"`, `outcome: "closed"`, the original `request`, exact `target`, and `management_ref` (the serving MCP request, `tool: "pty_close"`). Its `evidence` contains original `native` binding, `initial_prompt: "none"`, `initial_prompt_delivery: null`, `harness_alive: false`, `absence_confirmed: true`, and `metadata_committed: true`. Only confirmed close, absence and committed slot release produce this receipt. Errors or lost/uncertain results do not. Hosts must match the original pending operation and local stop/version state before clearing its uncertainty; historical receipts cannot clear later uncertainty.
804
+
805
+ For a returned unknown launch whose harness has exited, the same native entry admits explicit `pty_close` only after synchronous proof of the original native/server and managed generation, raw no-initial-prompt state, an unissued body send and an exited harness. It releases the failed launch slot after successful close and confirmed absence, preserving the original unknown record. Issued RPCs, live harnesses and missing input or generation evidence remain blocked. The old request is never replayed; a later ordinary request may create a fresh generation.
800
806
 
801
807
 
802
808
  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.
package/dist/core.js CHANGED
@@ -1701,6 +1701,14 @@ export function selectExecutionAuthSessionName() { return "auth-" + randomBytes(
1701
1701
  /** Read-only proof for reconciling a created, empty launch. Inspect raw prompt
1702
1702
  * state: legacy/default normalization must never prove that input was absent. */
1703
1703
  export function observeExecutionCreatedLaunch(target) {
1704
+ return observeEmptyCreatedLaunch(target, true);
1705
+ }
1706
+ /** Explicit management close only. No generation enrollment or acceptance of
1707
+ * the failed launch; an unknown/missing harness proof remains a refusal. */
1708
+ export function observeExecutionExitedCreatedLaunch(target) {
1709
+ return observeEmptyCreatedLaunch(target, false);
1710
+ }
1711
+ function observeEmptyCreatedLaunch(target, alive) {
1704
1712
  target = executionLaunchSchema.parse(target);
1705
1713
  const unknown = () => { throw new ExecutionError("EXECUTION_UNCONFIRMED"); };
1706
1714
  const generation = () => {
@@ -1724,12 +1732,14 @@ export function observeExecutionCreatedLaunch(target) {
1724
1732
  };
1725
1733
  generation();
1726
1734
  const native = read(), live = observeSession(target.session_id);
1727
- if (live.launch_id !== target.launch_id || live.harness_alive !== true || live.harness_process === null || live.state !== "idle")
1735
+ if (live.launch_id !== target.launch_id || live.harness_alive !== alive
1736
+ || (alive ? live.harness_process === null || live.state !== "idle"
1737
+ : live.harness_process !== null || live.state !== "dead" || live.reason !== "harness_exited"))
1728
1738
  unknown();
1729
1739
  generation();
1730
1740
  if (!isDeepStrictEqual(read(), native))
1731
1741
  unknown();
1732
- return { target, native, initial_prompt: "none", initial_prompt_delivery: null, harness_alive: true, ready: true };
1742
+ return { target, native, initial_prompt: "none", initial_prompt_delivery: null, harness_alive: alive, ready: alive };
1733
1743
  }
1734
1744
  /** Private source-provenance snapshot. It deliberately omits screen, argv,
1735
1745
  * environment and cached vendor_session_id. No public tool is added. The
@@ -5044,7 +5054,7 @@ async function launchKeepingUserDefaults(configPath, o, launch) {
5044
5054
  return withUserConfigLock(configPath, async () => {
5045
5055
  const before = readFileOrNull(configPath);
5046
5056
  const mustWrite = o.mustWrite(before);
5047
- const launched = launch();
5057
+ const launched = await launch();
5048
5058
  const started = performance.now();
5049
5059
  const deadline = started + (mustWrite ? LAUNCH_DEFAULT_MUST_WRITE_WAIT_MS : LAUNCH_DEFAULT_WAIT_MS);
5050
5060
  let aliveCheckedAt = started;
@@ -6366,6 +6376,13 @@ function existingAgentLaunchResult(kind, sid, model, effort) {
6366
6376
  }
6367
6377
  export function openAgent(kind, opts = {}) {
6368
6378
  assertExecutionContext(opts.session_name);
6379
+ return prepareAgentLaunch(kind, opts)();
6380
+ }
6381
+ /** Resolve and validate the CLI before taking native admission. Only the
6382
+ * returned synchronous creation/metadata/CLI segment shares the graph mutex.
6383
+ * Public synchronous openAgent retains its existing immediate refusal contract.
6384
+ */
6385
+ function prepareAgentLaunch(kind, opts = {}) {
6369
6386
  const label = agentLabel(kind);
6370
6387
  // 前提検証は session を作る前に全部済ませる(失敗の残骸 session を作らない)。
6371
6388
  // model/effort → bin → cwd の順: model/effort 検証は CLI 不在の端末でも同じ結果になる(テスト可能性)。
@@ -6459,7 +6476,7 @@ export function openAgent(kind, opts = {}) {
6459
6476
  });
6460
6477
  if (launchOperationId !== null && sessionExists(opts.session_name)) {
6461
6478
  requireMatchingClaudeLaunch(opts.session_name, launchOperationId, launchRequestDigest);
6462
- return existingAgentLaunchResult("claude", opts.session_name, model, effort);
6479
+ return () => existingAgentLaunchResult("claude", opts.session_name, model, effort);
6463
6480
  }
6464
6481
  if (kind === "claude")
6465
6482
  assertClaudeAuthenticationReady(bin);
@@ -6489,100 +6506,102 @@ export function openAgent(kind, opts = {}) {
6489
6506
  if (kind === "cursor" && model) {
6490
6507
  assertCursorModelAvailable(bin, cwd ?? process.cwd(), model, effort, cursorFast);
6491
6508
  }
6492
- let sid;
6493
- let hint;
6494
- // WindowsのCursorだけはPowerShell 7 paneから起動する(理由はcursorPwshLaunchLine)。
6495
- const paneShell = isWin && kind === "cursor" && agentDone ? "pwsh" : "bash";
6496
- try {
6497
- [sid, hint] = openSession(opts.session_name ?? null, paneShell, envVars);
6498
- }
6499
- catch (error) {
6500
- if (launchOperationId !== null && sessionExists(opts.session_name)) {
6501
- requireMatchingClaudeLaunch(opts.session_name, launchOperationId, launchRequestDigest);
6502
- return existingAgentLaunchResult("claude", opts.session_name, model, effort);
6509
+ return () => {
6510
+ let sid;
6511
+ let hint;
6512
+ // WindowsのCursorだけはPowerShell 7 paneから起動する(理由はcursorPwshLaunchLine)。
6513
+ const paneShell = isWin && kind === "cursor" && agentDone ? "pwsh" : "bash";
6514
+ try {
6515
+ [sid, hint] = openSession(opts.session_name ?? null, paneShell, envVars);
6503
6516
  }
6504
- throw error;
6505
- }
6506
- let launchNote = "";
6507
- try {
6508
- const lineageContext = createAgentLineageContext(kind, sid, lineageSeed);
6509
- if (hasExecutionCreationScope())
6510
- beforeExecutionEffect(sid);
6511
- const meta = withExecutionPreparation(sid, () => {
6512
- const meta = agentDone
6513
- ? kind === "claude"
6514
- ? createClaudeAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", launchOperationId, launchRequestDigest, lineageContext, model, effort)
6515
- : kind === "codex"
6516
- ? createCodexAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", { model, effort }, writeScope, lineageContext)
6517
- : kind === "cursor"
6518
- ? createCursorAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", writeScope, lineageContext)
6519
- : createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope, lineageContext)
6520
- : null;
6521
- if (meta) {
6522
- // Metadata constructors publish their internally generated launch ID.
6523
- // Stamp its exact native creator before binding and any later input.
6524
- if (hasExecutionCreationScope())
6525
- meta.execution_native = executionCreatedNativeBinding(sid);
6526
- meta.agent_executable = bin;
6527
- // 席は通常の速度で始まる(Claude Code・Codexは起動の引数で明示する)。fast版のmodel IDで起こした時だけtrue。
6528
- meta.fast = opts.fast_state === true;
6529
- meta.model = opts.base_model !== undefined ? opts.base_model : model;
6530
- // The new physical shell is already pinned by its actual create receipt.
6531
- // Validate it before publishing metadata, then bind this internally
6532
- // generated managed launch. Model arguments cannot select this ID.
6533
- writeAgentMetadata(meta);
6534
- agentMetadataNegativeCache.delete(sid);
6517
+ catch (error) {
6518
+ if (launchOperationId !== null && sessionExists(opts.session_name)) {
6519
+ requireMatchingClaudeLaunch(opts.session_name, launchOperationId, launchRequestDigest);
6520
+ return existingAgentLaunchResult("claude", opts.session_name, model, effort);
6535
6521
  }
6536
- return meta;
6537
- });
6538
- if (meta && hasExecutionCreationScope())
6539
- bindExecutionCreatedLaunch({ session_id: sid, launch_id: meta.launch_id });
6540
- launchNote = buildAgentLaunchNote(kind, model, effort, meta);
6541
- let full;
6542
- if (paneShell === "pwsh" && meta) {
6543
- full = cursorPwshLaunchLine(cwd, cursorAgentEnvEntries(meta, sid, envVars), cursorPwshArgv(cursorAgentArgv(bin, model, effort, opts.prompt ?? null, meta, cursorFast), () => resolveWinPaneShell("bash")));
6522
+ throw error;
6544
6523
  }
6545
- else {
6546
- const cmd = buildAgentCmd(kind, binForCmd, model, effort, opts.prompt ?? null, meta, cursorFast);
6547
- const envPrefix = agentEnvPrefix(meta, sid, envVars);
6548
- full = cwdForCmd ? `cd ${shq(cwdForCmd)} && ${envPrefix}${cmd}` : `${envPrefix}${cmd}`;
6549
- }
6550
- if (meta)
6551
- agentLaunchLines.set(sid, full);
6552
- // force:true はagent sessionへの手動介入を表す。起動コマンド自体はAitermが組み立てて素送信する。
6553
- send(sid, full, {
6554
- enter: true,
6555
- mark: false,
6556
- force: true,
6557
- rtk: false,
6558
- raw: true,
6559
- preserveAgentOperation: meta?.kind === "claude",
6560
- });
6561
- }
6562
- catch (e) {
6563
- const failure = e instanceof ExecutionError ? e : telemetryOwnedFailure("AITERM.VENDOR_LAUNCHER_FAILED", e);
6564
- // 起動コマンドを投入できなかった session は空のまま残る=残骸を作らない。片付けてから元エラーを伝える。
6524
+ let launchNote = "";
6565
6525
  try {
6566
- closeSessionInternal(sid, false);
6567
- }
6568
- catch {
6569
- /* 片付け失敗より元エラーの伝達を優先 */
6526
+ const lineageContext = createAgentLineageContext(kind, sid, lineageSeed);
6527
+ if (hasExecutionCreationScope())
6528
+ beforeExecutionEffect(sid);
6529
+ const meta = withExecutionPreparation(sid, () => {
6530
+ const meta = agentDone
6531
+ ? kind === "claude"
6532
+ ? createClaudeAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", launchOperationId, launchRequestDigest, lineageContext, model, effort)
6533
+ : kind === "codex"
6534
+ ? createCodexAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", { model, effort }, writeScope, lineageContext)
6535
+ : kind === "cursor"
6536
+ ? createCursorAgentMetadata(sid, cwd, opts.prompt ? "pending" : "none", writeScope, lineageContext)
6537
+ : createGrokAgentMetadata(kind, sid, cwd, opts.prompt ? "pending" : "none", grokAuthPath, writeScope, lineageContext)
6538
+ : null;
6539
+ if (meta) {
6540
+ // Metadata constructors publish their internally generated launch ID.
6541
+ // Stamp its exact native creator before binding and any later input.
6542
+ if (hasExecutionCreationScope())
6543
+ meta.execution_native = executionCreatedNativeBinding(sid);
6544
+ meta.agent_executable = bin;
6545
+ // 席は通常の速度で始まる(Claude Code・Codexは起動の引数で明示する)。fast版のmodel IDで起こした時だけtrue。
6546
+ meta.fast = opts.fast_state === true;
6547
+ meta.model = opts.base_model !== undefined ? opts.base_model : model;
6548
+ // The new physical shell is already pinned by its actual create receipt.
6549
+ // Validate it before publishing metadata, then bind this internally
6550
+ // generated managed launch. Model arguments cannot select this ID.
6551
+ writeAgentMetadata(meta);
6552
+ agentMetadataNegativeCache.delete(sid);
6553
+ }
6554
+ return meta;
6555
+ });
6556
+ if (meta && hasExecutionCreationScope())
6557
+ bindExecutionCreatedLaunch({ session_id: sid, launch_id: meta.launch_id });
6558
+ launchNote = buildAgentLaunchNote(kind, model, effort, meta);
6559
+ let full;
6560
+ if (paneShell === "pwsh" && meta) {
6561
+ full = cursorPwshLaunchLine(cwd, cursorAgentEnvEntries(meta, sid, envVars), cursorPwshArgv(cursorAgentArgv(bin, model, effort, opts.prompt ?? null, meta, cursorFast), () => resolveWinPaneShell("bash")));
6562
+ }
6563
+ else {
6564
+ const cmd = buildAgentCmd(kind, binForCmd, model, effort, opts.prompt ?? null, meta, cursorFast);
6565
+ const envPrefix = agentEnvPrefix(meta, sid, envVars);
6566
+ full = cwdForCmd ? `cd ${shq(cwdForCmd)} && ${envPrefix}${cmd}` : `${envPrefix}${cmd}`;
6567
+ }
6568
+ if (meta)
6569
+ agentLaunchLines.set(sid, full);
6570
+ // force:true はagent sessionへの手動介入を表す。起動コマンド自体はAitermが組み立てて素送信する。
6571
+ send(sid, full, {
6572
+ enter: true,
6573
+ mark: false,
6574
+ force: true,
6575
+ rtk: false,
6576
+ raw: true,
6577
+ preserveAgentOperation: meta?.kind === "claude",
6578
+ });
6570
6579
  }
6571
- throw failure;
6572
- }
6573
- const driveHint = agentDone
6574
- ? `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
6575
- `turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch。${autoDeliveryParent() ? `回答本文は${autoDeliveryParent()}親へ自動配送する` : "完了通知はaiterm-wait"})。中断はpty_key(${sid}, "C-c")、` +
6576
- `Stopが来ない場合の解除はpty_close(${sid})を使う。`
6577
- : `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
6578
- `pty_send(${sid}, "...") で入力・pty_key(${sid}, "Enter"/"Up"/"C-c" 等) で操作する(対話)。`;
6579
- return [
6580
- sid,
6581
- `${label} を session ${sid} で起動した。${launchNote}${agentDone ? "agent_done 待機が有効。" : ""}` +
6582
- `${agentDone ? " 通常のproject/user環境を共有し、aiterm所有の完了相関とsub-agent lineageだけを加算。" : ""}\n${hint}\n` +
6583
- driveHint +
6584
- `起動直後に増分 pty_read すると空/半描画になり得るので screen:true を使う。`,
6585
- ];
6580
+ catch (e) {
6581
+ const failure = e instanceof ExecutionError ? e : telemetryOwnedFailure("AITERM.VENDOR_LAUNCHER_FAILED", e);
6582
+ // 起動コマンドを投入できなかった session は空のまま残る=残骸を作らない。片付けてから元エラーを伝える。
6583
+ try {
6584
+ closeSessionInternal(sid, false);
6585
+ }
6586
+ catch {
6587
+ /* 片付け失敗より元エラーの伝達を優先 */
6588
+ }
6589
+ throw failure;
6590
+ }
6591
+ const driveHint = agentDone
6592
+ ? `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
6593
+ `turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch。${autoDeliveryParent() ? `回答本文は${autoDeliveryParent()}親へ自動配送する` : "完了通知はaiterm-wait"})。中断はpty_key(${sid}, "C-c")、` +
6594
+ `Stopが来ない場合の解除はpty_close(${sid})を使う。`
6595
+ : `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
6596
+ `pty_send(${sid}, "...") で入力・pty_key(${sid}, "Enter"/"Up"/"C-c" 等) で操作する(対話)。`;
6597
+ return [
6598
+ sid,
6599
+ `${label} を session ${sid} で起動した。${launchNote}${agentDone ? "agent_done 待機が有効。" : ""}` +
6600
+ `${agentDone ? " 通常のproject/user環境を共有し、aiterm所有の完了相関とsub-agent lineageだけを加算。" : ""}\n${hint}\n` +
6601
+ driveHint +
6602
+ `起動直後に増分 pty_read すると空/半描画になり得るので screen:true を使う。`,
6603
+ ];
6604
+ };
6586
6605
  }
6587
6606
  export class AgentLaunchPromptError extends AitermError {
6588
6607
  session_id;
@@ -6598,7 +6617,10 @@ export class AgentLaunchPromptError extends AitermError {
6598
6617
  }
6599
6618
  }
6600
6619
  export async function openAgentWithInitialPrompt(kind, opts = {}) {
6601
- assertExecutionContext(opts.session_name);
6620
+ if (opts.session_name)
6621
+ await withExecutionNativeAdmission(opts.session_name, () => assertExecutionContext(opts.session_name));
6622
+ else
6623
+ assertExecutionContext(opts.session_name);
6602
6624
  const mission = opts.prompt ?? null;
6603
6625
  const sourceSessionId = opts.throughline_source_session ?? null;
6604
6626
  const supplementFile = opts.throughline_supplement_file ?? null;
@@ -6646,12 +6668,16 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
6646
6668
  };
6647
6669
  // GrokとCursorは、起動の --model の値を利用者の既定へ書く(Cursorは版による)。起動の前後で、直前の値へ戻す。
6648
6670
  const launch = () => {
6671
+ const admitted = async () => {
6672
+ const create = prepareAgentLaunch(kind, openOptions);
6673
+ return opts.session_name ? withExecutionNativeAdmission(opts.session_name, create) : create();
6674
+ };
6649
6675
  if (kind === "grok") {
6650
6676
  const configPath = grokUserConfigPath(realGrokHome());
6651
6677
  // Grokは起動完了の直後に書く(入力待ちと読める頃には書き終えている)。
6652
6678
  return launchKeepingUserDefaults(configPath, {
6653
6679
  restore: restoreGrokDefaults(configPath), written: grokDefaultsWritten, mustWrite: () => false, ready: grokTuiReady,
6654
- }, () => openAgent(kind, openOptions));
6680
+ }, admitted);
6655
6681
  }
6656
6682
  if (kind === "cursor" && openOptions.model) {
6657
6683
  const configPath = cursorUserConfigPath(realCursorHome());
@@ -6660,9 +6686,9 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
6660
6686
  restore: restoreCursorDefaults(configPath), written: cursorDefaultsWritten,
6661
6687
  mustWrite: (before) => { const config = parseJsonObject(before); return config !== null && "selectedModel" in config; },
6662
6688
  ready: cursorTuiReady,
6663
- }, () => openAgent(kind, openOptions));
6689
+ }, admitted);
6664
6690
  }
6665
- return Promise.resolve().then(() => openAgent(kind, openOptions));
6691
+ return admitted();
6666
6692
  };
6667
6693
  // 入力待ちになった席を、その席の中でfastへ切り替える(Claude Code・Codex)。出来なかった訳は返りへ入れ、起動は続ける。
6668
6694
  const switchAfterReady = async (name, meta) => {
@@ -1,12 +1,8 @@
1
1
  // Synchronous expected-generation close. The serving adapter must already own
2
2
  // the shared issued close slot; this helper does not replace admission/drain.
3
3
  import { executionLaunchSchema } from "./execution-contract.js";
4
+ export { executionNativeBindingSchema } from "./execution-contract.js";
4
5
  import { ExecutionError } from "./execution-wire.js";
5
- import { z } from "zod";
6
- export const executionNativeBindingSchema = z.object({ schema: z.literal("aiterm.execution-native.v1"),
7
- native_id: z.string().regex(/^\$\d+$/),
8
- server: z.object({ pid: z.number().int().positive(), started_identity: z.string().min(1).max(256) }).strict(),
9
- }).strict();
10
6
  export function parseExecutionNativeSessions(text) {
11
7
  const sessions = [];
12
8
  for (const line of text.split(/\r?\n/).filter(Boolean)) {
@@ -7,6 +7,10 @@ const referenceId = z.string().min(1).max(256);
7
7
  export const executionLaunchSchema = z.object({
8
8
  session_id: sessionId, launch_id: launchId,
9
9
  }).strict();
10
+ export const executionNativeBindingSchema = z.object({ schema: z.literal("aiterm.execution-native.v1"),
11
+ native_id: z.string().regex(/^\$\d+$/),
12
+ server: z.object({ pid: z.number().int().positive(), started_identity: z.string().min(1).max(256) }).strict(),
13
+ }).strict();
10
14
  // Existing references, normalized by the runtime adapter to an immutable run
11
15
  // boundary. A later native turn ID is an alias, not permission to rekey history.
12
16
  export const executionBoundarySchema = z.object({
@@ -123,6 +123,13 @@ export function createExecutionDispatcher(bindings) {
123
123
  throw new ExecutionError("EXECUTION_UNSUPPORTED");
124
124
  bindings.assertParticipant?.();
125
125
  return { sourceContext: extra => executionHostSourceFromMeta(extra._meta, bindings.initializedClientName()),
126
+ closeManagementRequest: (extra, session) => {
127
+ if (!registeredControls || !native || bindings.initializedClientName() !== "bellteam")
128
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
129
+ return executionRequestSchema.options[4].parse({ kind: "mcp", process_pid: native.pid,
130
+ process_started_identity: native.started_identity, request_id: extra.requestId,
131
+ tool: "pty_close", target_session_id: session });
132
+ },
126
133
  activate: () => { registeredControls = true; } };
127
134
  },
128
135
  async dispatch(name, input, extra, body) {
@@ -7,3 +7,6 @@ export const createdLaunchObservationSchema = z.object({
7
7
  initial_prompt: z.literal("none"), initial_prompt_delivery: z.null(),
8
8
  harness_alive: z.literal(true), ready: z.literal(true),
9
9
  }).strict();
10
+ export const exitedCreatedLaunchObservationSchema = createdLaunchObservationSchema.extend({
11
+ harness_alive: z.literal(false), ready: z.literal(false),
12
+ });
@@ -38,6 +38,7 @@ export function createNativeExecutionServer(input) {
38
38
  selectAuthSessionName: core.selectExecutionAuthSessionName,
39
39
  observeAuthSessionGeneration: core.observeExecutionAuthSessionGeneration,
40
40
  observeCreatedLaunchSync: core.observeExecutionCreatedLaunch,
41
+ observeExitedCreatedLaunchSync: core.observeExecutionExitedCreatedLaunch,
41
42
  closeExpectedSession: core.closeExpectedAgentSession, // explicit management only, never stop fallback
42
43
  observeInterruptionSync: core.observeExpectedAgentRunInterruption,
43
44
  selectInterruptionSync: core.selectExpectedAgentRunInterruption,
@@ -1,7 +1,7 @@
1
1
  // Private same-scope MCP parent producer. No public/index/profile mount. Queue
2
2
  // aliases lacking current independent native proof remain unsupported.
3
3
  import path from "node:path";
4
- import { observeAgentDone, selectExecutionAuthSessionName, observeExecutionAuthSessionGeneration, observeExecutionNativeCreationScope, closeExpectedAgentSession, observeExecutionCreatedLaunch } from "./core.js";
4
+ import { observeAgentDone, selectExecutionAuthSessionName, observeExecutionAuthSessionGeneration, observeExecutionNativeCreationScope, closeExpectedAgentSession, observeExecutionCreatedLaunch, observeExecutionExitedCreatedLaunch } from "./core.js";
5
5
  import { codexParentFromRequest } from "./codex-parent-receiver.js";
6
6
  import { claudeParentFromRequest } from "./claude-parent-receiver.js";
7
7
  import { cursorParentFromRequest } from "./cursor-parent-receiver.js";
@@ -33,15 +33,17 @@ const localProducerRuntime = {
33
33
  selectAuthSessionName: selectExecutionAuthSessionName,
34
34
  observeAuthSessionGeneration: observeExecutionAuthSessionGeneration,
35
35
  observeCreatedLaunchSync: observeExecutionCreatedLaunch,
36
+ observeExitedCreatedLaunchSync: observeExecutionExitedCreatedLaunch,
36
37
  closeExpectedSession: closeExpectedAgentSession,
37
38
  };
38
39
  export function createExecutionParentProducer(input) {
39
40
  const runtime = input.runtime ?? localProducerRuntime;
40
- for (const name of ["selectAuthSessionName", "observeAuthSessionGeneration", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner", "observeCreatedLaunchSync"])
41
+ for (const name of ["selectAuthSessionName", "observeAuthSessionGeneration", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner", "observeCreatedLaunchSync", "observeExitedCreatedLaunchSync"])
41
42
  if (runtime[name] !== undefined && typeof runtime[name] !== "function")
42
43
  throw new ExecutionError("EXECUTION_UNSUPPORTED");
43
44
  const selectAuthName = runtime.selectAuthSessionName?.bind(runtime), observeAuth = runtime.observeAuthSessionGeneration?.bind(runtime);
44
45
  const createdLaunch = runtime.observeCreatedLaunchSync?.bind(runtime);
46
+ const exitedLaunch = runtime.observeExitedCreatedLaunchSync?.bind(runtime);
45
47
  const interruption = runtime.observeInterruptionSync?.bind(runtime);
46
48
  const selectInterruption = runtime.selectInterruptionSync?.bind(runtime), interruptKey = runtime.interruptKeySync?.bind(runtime);
47
49
  const observeActors = runtime.observeActors?.bind(runtime);
@@ -73,6 +75,14 @@ export function createExecutionParentProducer(input) {
73
75
  } } : {}),
74
76
  ...(interruptKey ? { interruptKeySync: (point) => { assertScope(); return interruptKey(point); } } : {}),
75
77
  observeSession: observe, observeSessionSync: observe,
78
+ ...(runtime.closeExpectedSession ? { closeCreatedLaunchSync: (target) => {
79
+ assertScope();
80
+ return runtime.closeExpectedSession(target);
81
+ } } : {}),
82
+ ...(exitedLaunch ? { observeExitedCreatedLaunchSync: (target) => {
83
+ assertScope();
84
+ return exitedLaunch(target);
85
+ } } : {}),
76
86
  ...(createdLaunch ? { observeCreatedLaunchSync: (target) => {
77
87
  assertScope();
78
88
  return createdLaunch(target);
@@ -12,7 +12,7 @@ function snapshotRuntime(input) {
12
12
  for (const name of functions)
13
13
  if (typeof input[name] !== "function")
14
14
  throw new ExecutionError("EXECUTION_UNSUPPORTED");
15
- const optional = ["selectAuthSessionName", "observeAuthSessionGeneration", "observeNativeSessions", "closeExpectedSession", "observeActors", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner", "observeCreatedLaunchSync"];
15
+ const optional = ["selectAuthSessionName", "observeAuthSessionGeneration", "observeNativeSessions", "closeExpectedSession", "observeActors", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync", "observeOriginOwner", "observeCreatedLaunchSync", "observeExitedCreatedLaunchSync"];
16
16
  for (const name of optional)
17
17
  if (input[name] !== undefined && typeof input[name] !== "function")
18
18
  throw new ExecutionError("EXECUTION_UNSUPPORTED");
@@ -45,7 +45,7 @@ const waitSchema = z.object({
45
45
  });
46
46
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
47
47
  export function createExecutionRuntime(bindings) {
48
- for (const name of ["observeCreatedLaunchSync", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync"])
48
+ for (const name of ["observeCreatedLaunchSync", "observeExitedCreatedLaunchSync", "closeCreatedLaunchSync", "observeInterruptionSync", "selectInterruptionSync", "interruptKeySync"])
49
49
  if (bindings[name] !== undefined && typeof bindings[name] !== "function")
50
50
  throw new ExecutionError("EXECUTION_UNSUPPORTED");
51
51
  const interruption = bindings.observeInterruptionSync;
@@ -72,8 +72,10 @@ export function createExecutionRuntime(bindings) {
72
72
  return { state: "unknown" };
73
73
  }
74
74
  };
75
- const created = bindings.observeCreatedLaunchSync;
75
+ const created = bindings.observeCreatedLaunchSync, exited = bindings.observeExitedCreatedLaunchSync;
76
76
  return {
77
+ ...(bindings.closeCreatedLaunchSync ? { closeCreatedLaunchSync: (target) => bindings.closeCreatedLaunchSync(executionLaunchSchema.parse(target)) } : {}),
78
+ ...(exited ? { observeExitedCreatedLaunchSync: (target) => exited(executionLaunchSchema.parse(target)) } : {}),
77
79
  ...(created ? { observeCreatedLaunchSync: (target) => created(executionLaunchSchema.parse(target)) } : {}),
78
80
  ...(interruption ? { interruptionSync: (point) => interruption(executionPointSchema.parse(point)) } : {}),
79
81
  ...(selectInterruption ? { selectInterruptionSync: (expected, candidates) => selectInterruption(executionLaunchSchema.parse(expected), z.array(executionPointSchema).parse(candidates ?? [])) } : {}),
@@ -1,15 +1,16 @@
1
1
  // Metadata/admission service with injected runtime. It never owns a payload or
2
2
  // retries a launch/send/close. The core/MCP adapter is a separate integration.
3
3
  import { z } from "zod";
4
- import { assessExecutionSource, planCascadeStop, unfinishedExecutionRelations, cascadeStopRequestSchema, cascadeStopResultSchema, cascadeInterruptRequestSchema, cascadeInterruptResultSchema, executionPointSchema, executionLaunchSchema } from "./execution-contract.js";
4
+ import { assessExecutionSource, planCascadeStop, unfinishedExecutionRelations, cascadeStopRequestSchema, cascadeStopResultSchema, cascadeInterruptRequestSchema, cascadeInterruptResultSchema, executionPointSchema, executionLaunchSchema, executionRequestSchema } from "./execution-contract.js";
5
5
  import { executionActionSchema, executionRecordSchema, executionRefMetaSchema, executionRootsRequestSchema, executionRootsResultSchema, ExecutionError } from "./execution-wire.js";
6
- import { withExecutionEffectScope, withExecutionInterruptEffectScope } from "./execution-effect.js";
6
+ import { withExecutionEffectScope, withExecutionInterruptEffectScope, withExecutionNativeAdmission, bindExecutionManagementCloseTarget } from "./execution-effect.js";
7
7
  import { nativeInterruptionObservationSchema, interruptionIdentity, interruptionBlocksLaunch, interruptionBlocksRequest, interruptionSlots, assertInterruptionSource, sameInterruptionRowRun } from "./execution-interruption.js";
8
8
  import { interruptionKeyReference } from "./execution-interruption.js";
9
9
  import { isDeepStrictEqual } from "node:util";
10
10
  import { executionActorSchema } from "./execution-inventory.js";
11
- import { createdLaunchObservationSchema } from "./execution-launch-recovery.js";
11
+ import { createdLaunchObservationSchema, exitedCreatedLaunchObservationSchema } from "./execution-launch-recovery.js";
12
12
  import { executionCheckpoint } from "./execution-checkpoint.js";
13
+ import { executionCloseLaunchSchema, executionClosedLaunchSchema } from "./execution-wire.js";
13
14
  const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
14
15
  const key = (value) => JSON.stringify(value);
15
16
  const stopped = (state, launch) => !!launch && (state.stopping.some(value => same(value, launch)) || interruptionBlocksLaunch(state, launch));
@@ -43,29 +44,50 @@ function find(state, request) {
43
44
  return record;
44
45
  }
45
46
  const managementClose = (request) => request.kind === "mcp" && request.tool === "pty_close";
46
- /** Only a returned, unknown send may be displaced by an explicit close of
47
- * that exact launch. Issued work, management/parent slots and other generations
48
- * remain blockers. The record is never turned into accepted or not_sent.
47
+ /** A returned unknown send, or an empty exited created launch with additional
48
+ * native proof, may be displaced by explicit close of that exact generation.
49
+ * Issued work and other generations remain blockers; failed records are kept.
49
50
  */
50
- function recoverableSends(state, request, session, expected) {
51
+ function recoverableCloseSlots(state, request, session, expected) {
51
52
  if (!managementClose(request) || expected === null)
52
53
  return [];
53
54
  return state.slots.filter(slot => {
54
- if (slot.session_id !== session || slot.expected_launch_id !== expected || slot.stage !== "send"
55
+ if (slot.session_id !== session || slot.expected_launch_id !== expected || !["send", "launch"].includes(slot.stage)
55
56
  || slot.state !== "unknown" || !slot.request || same(slot.request, request))
56
57
  return false;
58
+ const original = state.records.find(record => same(record.reservation.request, slot.request));
59
+ if (slot.stage === "launch")
60
+ return !!original && !!slot.created_native_id && !!slot.created_server
61
+ && (slot.request.kind !== "mcp" || slot.request.tool === "agent_launch")
62
+ && original.operations.launch === "unknown" && original.operations.send === "not_issued"
63
+ && original.relation.phase === "submitted_unknown" && original.relation.completion === "unknown"
64
+ && original.relation.lifecycle === "tracking" && !openParentEffect(state, slot.request)
65
+ && original.relation.target?.boundary === null
66
+ && same(original.relation.target.launch, { session_id: session, launch_id: expected });
57
67
  if (slot.request.kind === "mcp" && !["pty_send", "claude_turn.issue"].includes(slot.request.tool))
58
68
  return false;
59
- const original = state.records.find(record => same(record.reservation.request, slot.request));
60
69
  return original?.operations.send === "unknown" && !["issued", "unknown"].includes(original.operations.launch)
61
70
  && original.relation.phase === "submitted_unknown" && original.relation.completion === "unknown"
62
71
  && same(original.relation.target?.launch, { session_id: session, launch_id: expected });
63
72
  });
64
73
  }
65
74
  function targetBlocked(state, request, session, expected) {
66
- const recoverable = recoverableSends(state, request, session, expected);
75
+ const recoverable = recoverableCloseSlots(state, request, session, expected);
67
76
  return state.slots.some(slot => slot.session_id === session && !recoverable.includes(slot));
68
77
  }
78
+ function assertCloseLaunchExpectation(state, expected, management) {
79
+ const original = find(state, expected.request), target = expected.target;
80
+ if (original.reservation.target.session_id !== target.session_id
81
+ || !same(original.relation.target?.launch, target))
82
+ throw new ExecutionError("EXECUTION_STALE_GENERATION");
83
+ if (original.operations.launch === "issued" || original.operations.send === "issued")
84
+ throw new ExecutionError("EXECUTION_BUSY");
85
+ const slots = state.slots.filter(slot => slot.session_id === target.session_id && !same(slot.request, management));
86
+ const candidates = recoverableCloseSlots(state, management, target.session_id, target.launch_id);
87
+ if (slots.length !== 1 || !same(slots[0].request, expected.request) || slots[0].stage !== "launch"
88
+ || !candidates.includes(slots[0]))
89
+ throw new ExecutionError("EXECUTION_UNCONFIRMED");
90
+ }
69
91
  function view(state, record) {
70
92
  return executionRecordSchema.parse({ ...record,
71
93
  stopping: record.stopping || stopped(state, record.relation.source?.launch) || stopped(state, record.relation.target?.launch)
@@ -444,16 +466,30 @@ export class ExecutionService {
444
466
  throw new ExecutionError("EXECUTION_UNSUPPORTED");
445
467
  return this.store.transactionSync(state => {
446
468
  const record = find(state, request), target = record.reservation.target;
447
- if (!recoverableSends(state, request, target.session_id, target.expected_launch_id).length)
469
+ if (!recoverableCloseSlots(state, request, target.session_id, target.expected_launch_id).length)
448
470
  return undefined;
449
471
  if (record.operations.send !== "preparing" || targetBlocked(state, request, target.session_id, target.expected_launch_id))
450
472
  throw new ExecutionError("EXECUTION_UNCONFIRMED");
451
473
  const native = this.runtime.nativeSessionSync?.(target.session_id);
452
474
  if (native?.state !== "present" || !native.server)
453
475
  throw new ExecutionError("EXECUTION_UNCONFIRMED");
476
+ this.proveExitedLaunchClose(state, request, target.session_id, target.expected_launch_id, native);
454
477
  return native.native_id;
455
478
  }, { readOnly: true });
456
479
  }
480
+ proveExitedLaunchClose(state, request, session, expected, native) {
481
+ const launches = recoverableCloseSlots(state, request, session, expected).filter(slot => slot.stage === "launch");
482
+ if (!launches.length)
483
+ return;
484
+ if (!expected || !this.runtime.observeExitedCreatedLaunchSync)
485
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
486
+ const target = { session_id: session, launch_id: expected };
487
+ const proof = exitedCreatedLaunchObservationSchema.safeParse(this.runtime.observeExitedCreatedLaunchSync(target));
488
+ if (!proof.success || !same(proof.data.target, target) || proof.data.native.native_id !== native.native_id
489
+ || !same(proof.data.native.server, native.server) || launches.some(slot => slot.created_native_id !== native.native_id
490
+ || !same(slot.created_server, native.server)))
491
+ throw new ExecutionError("EXECUTION_UNCONFIRMED");
492
+ }
457
493
  verifyCreationServer(metaInput, expected) {
458
494
  const { request } = executionRefMetaSchema.parse(metaInput);
459
495
  if (!this.store.transactionSync)
@@ -547,8 +583,52 @@ export class ExecutionService {
547
583
  return this.store.transaction(state => view(state, find(state, args.request)), { readOnly: true });
548
584
  if (args.action === "reconcile_launch")
549
585
  return this.reconcileLaunch(args);
586
+ if (args.action === "close_launch")
587
+ throw new ExecutionError("EXECUTION_UNSUPPORTED"); // serving SDK identity required
550
588
  return this.finish(args);
551
589
  }
590
+ /** One explicit host RPC. The expectation is checked within registration,
591
+ * every effect admission and final commit; it never becomes a new source or
592
+ * rekeys the original failed request. No successful receipt survives a failed commit.
593
+ */
594
+ async closeLaunch(input, managementInput, context) {
595
+ const expected = executionCloseLaunchSchema.parse(input), management = executionRequestSchema.options[4].parse(managementInput);
596
+ if (management.tool !== "pty_close" || management.target_session_id !== expected.target.session_id
597
+ || same(management, expected.request) || !this.runtime.closeCreatedLaunchSync)
598
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
599
+ const source = context?.source ?? null;
600
+ await this.register({ action: "register", source_session_id: source?.launch.session_id ?? null,
601
+ reservation: { schema: "aiterm.execution-reservation.v1", request: management,
602
+ source: { launch: source?.launch ?? null, boundary: source?.boundary ?? null },
603
+ target: { session_id: expected.target.session_id, expected_launch_id: expected.target.launch_id } } }, context, expected);
604
+ const ref = { schema: "aiterm.execution-ref.v1", request: management };
605
+ const result = await this.invokeDeferred(ref, "send", (issue, prepare) => withExecutionEffectScope(expected.target.session_id, issue, async () => {
606
+ const native = await withExecutionNativeAdmission(expected.target.session_id, () => {
607
+ const id = this.managementCloseNative(ref);
608
+ if (!id)
609
+ throw new ExecutionError("EXECUTION_UNCONFIRMED");
610
+ const binding = this.store.transactionSync(state => {
611
+ assertCloseLaunchExpectation(state, expected, management);
612
+ const slot = state.slots.find(slot => same(slot.request, expected.request));
613
+ return { schema: "aiterm.execution-native.v1", native_id: slot.created_native_id, server: slot.created_server };
614
+ }, { readOnly: true });
615
+ bindExecutionManagementCloseTarget(expected.target.session_id, id);
616
+ const closed = this.runtime.closeCreatedLaunchSync(expected.target);
617
+ if (closed && typeof closed.then === "function") {
618
+ void Promise.resolve(closed).catch(() => undefined);
619
+ throw new ExecutionError("EXECUTION_UNSUPPORTED", "possible");
620
+ }
621
+ return binding;
622
+ });
623
+ return { value: native, target: { launch: expected.target, boundary: null } };
624
+ }, undefined, prepare, body => this.admitNative(body)), expected.target.session_id, undefined, expected);
625
+ return { record: result.record, receipt: executionClosedLaunchSchema.parse({
626
+ schema: "aiterm.closed-launch.v1", version: 1, action: "close_launch", outcome: "closed",
627
+ request: expected.request, target: expected.target, management_ref: management,
628
+ evidence: { native: result.value, initial_prompt: "none", initial_prompt_delivery: null,
629
+ harness_alive: false, absence_confirmed: true, metadata_committed: true },
630
+ }) };
631
+ }
552
632
  async admitNative(body) {
553
633
  if (!this.store.admitNative)
554
634
  throw new ExecutionError("EXECUTION_UNSUPPORTED");
@@ -620,8 +700,10 @@ export class ExecutionService {
620
700
  roots: selected, relation_coverage: !unresolved.length && coverage === "complete" ? "complete" : "incomplete", unresolved });
621
701
  }, { readOnly: true });
622
702
  }
623
- async register(args, context) {
703
+ async register(args, context, closeExpectation) {
624
704
  return this.store.transaction(async (state) => {
705
+ if (closeExpectation)
706
+ assertCloseLaunchExpectation(state, closeExpectation, args.reservation.request);
625
707
  const previous = state.records.find(record => same(record.reservation.request, args.reservation.request));
626
708
  if (previous) {
627
709
  if (!same(previous.reservation, args.reservation) || previous.source_session_id !== args.source_session_id)
@@ -678,16 +760,18 @@ export class ExecutionService {
678
760
  * issue() immediately before its first actual effect, on the same stack.
679
761
  * Synchronous metadata admission persists issuance before any native action.
680
762
  */
681
- async invokeDeferred(metaInput, stage, body, targetSessionId, admission) {
682
- return this.invokeInternal(metaInput, stage, body, targetSessionId, true, admission);
763
+ async invokeDeferred(metaInput, stage, body, targetSessionId, admission, closeExpectation) {
764
+ return this.invokeInternal(metaInput, stage, body, targetSessionId, true, admission, closeExpectation);
683
765
  }
684
- async invokeInternal(metaInput, stage, body, targetSessionId, deferred, admission) {
766
+ async invokeInternal(metaInput, stage, body, targetSessionId, deferred, admission, closeExpectation) {
685
767
  const { request } = executionRefMetaSchema.parse(metaInput);
686
768
  let expectedCloseNative;
687
769
  if (deferred && (!this.store.transactionSync || !this.runtime.observeSync))
688
770
  throw new ExecutionError("EXECUTION_UNSUPPORTED");
689
771
  await this.store.transaction(async (state) => {
690
772
  admission?.();
773
+ if (closeExpectation)
774
+ assertCloseLaunchExpectation(state, closeExpectation, request);
691
775
  const record = find(state, request);
692
776
  const relation = record.relation;
693
777
  if (interruptionBlocksRequest(state, request))
@@ -704,13 +788,14 @@ export class ExecutionService {
704
788
  throw new ExecutionError("EXECUTION_UNCONFIRMED");
705
789
  if (deferred) {
706
790
  const target = record.reservation.target;
707
- if (recoverableSends(state, request, target.session_id, target.expected_launch_id).length) {
791
+ if (recoverableCloseSlots(state, request, target.session_id, target.expected_launch_id).length) {
708
792
  if (targetBlocked(state, request, target.session_id, target.expected_launch_id))
709
793
  throw new ExecutionError("EXECUTION_UNCONFIRMED");
710
794
  this.expectTarget(await this.runtime.observe(target.session_id), target.expected_launch_id, target.session_id);
711
795
  const native = this.runtime.nativeSessionSync?.(target.session_id);
712
796
  if (native?.state !== "present" || !native.server)
713
797
  throw new ExecutionError("EXECUTION_UNCONFIRMED");
798
+ this.proveExitedLaunchClose(state, request, target.session_id, target.expected_launch_id, native);
714
799
  expectedCloseNative = { native_id: native.native_id, server: structuredClone(native.server) };
715
800
  }
716
801
  record.operations[stage] = "preparing";
@@ -762,6 +847,8 @@ export class ExecutionService {
762
847
  // wait between them. Owning its slot is not permission to ignore a
763
848
  // stop, uncertain slot or generation replacement after the first one.
764
849
  return this.store.transactionSync(state => {
850
+ if (closeExpectation)
851
+ assertCloseLaunchExpectation(state, closeExpectation, request);
765
852
  const record = find(state, request), relation = record.relation;
766
853
  if (interruptionBlocksRequest(state, request))
767
854
  throw new ExecutionError("EXECUTION_STOPPING");
@@ -785,6 +872,8 @@ export class ExecutionService {
785
872
  }
786
873
  const current = this.runtime.observeSync(slot.session_id);
787
874
  checkCloseNative(slot.session_id);
875
+ if (expectedCloseNative)
876
+ this.proveExitedLaunchClose(state, request, slot.session_id, slot.expected_launch_id, expectedCloseNative);
788
877
  if (current.state === "present" && stopped(state, current.launch))
789
878
  throw new ExecutionError("EXECUTION_STOPPING");
790
879
  if (slot.created_native_id) {
@@ -808,6 +897,8 @@ export class ExecutionService {
808
897
  }, { readOnly: true });
809
898
  }
810
899
  const result = this.store.transactionSync(state => {
900
+ if (closeExpectation)
901
+ assertCloseLaunchExpectation(state, closeExpectation, request);
811
902
  const record = find(state, request), relation = record.relation;
812
903
  if (interruptionBlocksRequest(state, request))
813
904
  throw new ExecutionError("EXECUTION_STOPPING");
@@ -828,6 +919,8 @@ export class ExecutionService {
828
919
  const current = this.runtime.observeSync(target.session_id);
829
920
  this.expectTarget(current, expected, target.session_id);
830
921
  checkCloseNative(target.session_id);
922
+ if (expectedCloseNative)
923
+ this.proveExitedLaunchClose(state, request, target.session_id, expected, expectedCloseNative);
831
924
  if (current.state === "present" && stopped(state, current.launch))
832
925
  throw new ExecutionError("EXECUTION_STOPPING");
833
926
  if ((stage === "launch" && current.state !== "absent") || (stage === "send" && current.state !== "present"))
@@ -906,6 +999,8 @@ export class ExecutionService {
906
999
  // Parsed above after issuance; null is supported only for an unissued noop.
907
1000
  const targetPoint = outcome.target;
908
1001
  const record = await this.store.transaction(async (state) => {
1002
+ if (closeExpectation)
1003
+ assertCloseLaunchExpectation(state, closeExpectation, request);
909
1004
  const record = find(state, request);
910
1005
  const slot = state.slots.find(value => same(value.request, request));
911
1006
  if (!slot || slot.stage !== stage || record.operations[stage] !== "issued")
@@ -935,7 +1030,7 @@ export class ExecutionService {
935
1030
  // Actual management close plus confirmed absence releases only these
936
1031
  // failed send admissions. Old callbacks still fail their missing-slot
937
1032
  // guard, and their records remain unknown and cannot be invoked again.
938
- const recovered = recoverableSends(state, request, slot.session_id, expected);
1033
+ const recovered = recoverableCloseSlots(state, request, slot.session_id, expected);
939
1034
  state.slots = state.slots.filter(value => !recovered.includes(value));
940
1035
  this.interruptRelations(state, targetPoint.launch);
941
1036
  }
@@ -967,8 +1062,24 @@ export class ExecutionService {
967
1062
  async reconcileLaunch(args) {
968
1063
  return this.store.transaction(state => {
969
1064
  const record = find(state, args.request), relation = record.relation;
970
- if (record.operations.launch === "accepted" && same(relation.target?.launch, args.target))
1065
+ if (record.operations.launch === "accepted" && same(relation.target?.launch, args.target)) {
1066
+ // A saved accepted receipt is historical. Before a caller can use an
1067
+ // empty launch to resume input, prove that this generation is still
1068
+ // present and no later operation owns the same session slot.
1069
+ if (record.operations.send === "not_issued") {
1070
+ if (record.stopping || stopped(state, args.target) || stopped(state, relation.source?.launch)
1071
+ || interruptionBlocksRequest(state, args.request))
1072
+ throw new ExecutionError("EXECUTION_STOPPING");
1073
+ if (state.slots.some(slot => slot.session_id === args.target.session_id) || openParentEffect(state, args.request))
1074
+ throw new ExecutionError("EXECUTION_UNCONFIRMED");
1075
+ if (!this.runtime.observeCreatedLaunchSync)
1076
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
1077
+ const parsed = createdLaunchObservationSchema.safeParse(this.runtime.observeCreatedLaunchSync(args.target));
1078
+ if (!parsed.success || !same(parsed.data.target, args.target))
1079
+ throw new ExecutionError("EXECUTION_UNCONFIRMED");
1080
+ }
971
1081
  return view(state, record);
1082
+ }
972
1083
  if (record.operations.launch === "issued")
973
1084
  throw new ExecutionError("EXECUTION_BUSY");
974
1085
  if (record.stopping || stopped(state, args.target) || stopped(state, relation.source?.launch)
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { executionLaunchSchema, executionReservationSchema, executionRequestSchema, executionPointSchema, cascadeInterruptRequestSchema, cascadeInterruptResultObjectSchema } from "./execution-contract.js";
3
- import { executionActionSchema, executionFinishSchema, executionRecordSchema, executionBindingSchema, executionRootsRequestSchema, executionRootsResultObjectSchema, executionRefFromMeta, executionErrorResult, ExecutionError, EXECUTION_BINDING_META_KEY, EXECUTION_ERROR_META_KEY, EXECUTION_SOURCE_META_KEY } from "./execution-wire.js";
3
+ import { executionActionSchema, executionFinishSchema, executionRecordSchema, executionBindingSchema, executionRootsRequestSchema, executionRootsResultObjectSchema, executionRefFromMeta, executionErrorResult, ExecutionError, EXECUTION_BINDING_META_KEY, EXECUTION_ERROR_META_KEY, EXECUTION_SOURCE_META_KEY, EXECUTION_CLOSED_LAUNCH_META_KEY } from "./execution-wire.js";
4
4
  import { withExecutionEffectScope, bindExecutionManagementCloseTarget } from "./execution-effect.js";
5
5
  import { immediateStopInputSchema, immediateStopResultSchema } from "./execution-immediate-stop.js";
6
6
  export function executionBindingMeta(record) {
@@ -32,6 +32,14 @@ export function createExecutionToolHandlers(service, options = {}) {
32
32
  throw new ExecutionError("EXECUTION_UNSUPPORTED");
33
33
  // Capture and detach before entering the shared-store transaction.
34
34
  const context = options.sourceContext?.(extra);
35
+ if (args.action === "close_launch") {
36
+ if (!options.closeManagementRequest)
37
+ throw new ExecutionError("EXECUTION_UNSUPPORTED");
38
+ const management = options.closeManagementRequest(extra, args.target.session_id);
39
+ const closed = await service.closeLaunch(args, management, context === undefined ? undefined : structuredClone(context));
40
+ return { content: [{ type: "text", text: JSON.stringify(closed.record) }], structuredContent: closed.record,
41
+ _meta: { ...executionBindingMeta(closed.record), [EXECUTION_CLOSED_LAUNCH_META_KEY]: closed.receipt } };
42
+ }
35
43
  const result = await service.execute(args, context === undefined ? undefined : structuredClone(context));
36
44
  return { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result,
37
45
  _meta: executionBindingMeta(result) };
@@ -65,12 +73,12 @@ export function createExecutionToolHandlers(service, options = {}) {
65
73
  export function registerExecutionTools(server, service, options = {}) {
66
74
  const registration = controlRegistrars.get(server)?.(service);
67
75
  const handlers = createExecutionToolHandlers(service, { ...options,
68
- ...(registration ? { sourceContext: registration.sourceContext } : {}) });
76
+ ...(registration ? { sourceContext: registration.sourceContext, closeManagementRequest: registration.closeManagementRequest } : {}) });
69
77
  server.registerTool("agent_execution", {
70
- description: "既存の実配送/実行参照を登録・照会・終了証拠で更新し、結果不明の空起動を同世代へ照合する。本文や再送queueは保存しない。",
78
+ description: "既存の実配送/実行参照を登録・照会・終了証拠で更新する。結果不明の空起動は元requestと期待世代を指定して照合、または終了済みCLIだけcloseする。本文や再送queueは保存しない。",
71
79
  // MCP accepts a raw object shape; the handler enforces the stricter action
72
80
  // union, including required fields and forbidden fields for each action.
73
- inputSchema: { action: z.enum(["register", "status", "finish", "reconcile_launch"]),
81
+ inputSchema: { action: z.enum(["register", "status", "finish", "reconcile_launch", "close_launch"]),
74
82
  reservation: executionReservationSchema.optional(), source_session_id: z.string().nullable().optional(),
75
83
  request: executionRequestSchema.optional(), target: executionLaunchSchema.optional(), evidence: executionFinishSchema.shape.evidence.optional(),
76
84
  remote: z.never().optional() },
@@ -1,10 +1,11 @@
1
1
  // Versioned Bridge/MCP contract. No dependency on core, PTY or the tool server.
2
2
  import { z } from "zod";
3
- import { executionLaunchSchema, executionPointSchema, executionRequestSchema, executionReservationSchema, executionRelationSchema, executionPhaseSchema, cascadeStopRequestSchema, cascadeStopResultSchema } from "./execution-contract.js";
3
+ import { executionLaunchSchema, executionPointSchema, executionRequestSchema, executionReservationSchema, executionRelationSchema, executionPhaseSchema, cascadeStopRequestSchema, cascadeStopResultSchema, executionNativeBindingSchema } from "./execution-contract.js";
4
4
  export const EXECUTION_REQUEST_META_KEY = "aiterm/execution";
5
5
  export const EXECUTION_BINDING_META_KEY = "aiterm/execution_binding";
6
6
  export const EXECUTION_ERROR_META_KEY = "aiterm/execution_error";
7
7
  export const EXECUTION_SOURCE_META_KEY = "aiterm/execution_source";
8
+ export const EXECUTION_CLOSED_LAUNCH_META_KEY = "aiterm/closed_launch";
8
9
  export const executionTransportSourceSchema = z.object({
9
10
  schema: z.literal("aiterm.execution-source.v1"), source: executionPointSchema.nullable(),
10
11
  }).strict();
@@ -46,7 +47,22 @@ export const executionFinishSchema = z.object({
46
47
  export const executionReconcileLaunchSchema = z.object({
47
48
  action: z.literal("reconcile_launch"), request: executionRequestSchema, target: executionLaunchSchema,
48
49
  }).strict();
49
- export const executionActionSchema = z.union([executionRegisterSchema, executionStatusSchema, executionFinishSchema, executionReconcileLaunchSchema]);
50
+ export const executionCloseLaunchSchema = z.object({
51
+ action: z.literal("close_launch"), request: executionRequestSchema, target: executionLaunchSchema,
52
+ }).strict();
53
+ export const executionClosedLaunchSchema = z.object({
54
+ schema: z.literal("aiterm.closed-launch.v1"), version: z.literal(1), action: z.literal("close_launch"),
55
+ outcome: z.literal("closed"), request: executionRequestSchema, target: executionLaunchSchema,
56
+ management_ref: executionRequestSchema.options[4],
57
+ evidence: z.object({ native: executionNativeBindingSchema, initial_prompt: z.literal("none"),
58
+ initial_prompt_delivery: z.null(), harness_alive: z.literal(false), absence_confirmed: z.literal(true),
59
+ metadata_committed: z.literal(true) }).strict(),
60
+ }).strict().superRefine((value, ctx) => {
61
+ if (value.management_ref.tool !== "pty_close" || value.management_ref.target_session_id !== value.target.session_id
62
+ || (value.request.kind === "aiterm" ? value.request.launch.session_id : value.request.target_session_id) !== value.target.session_id)
63
+ ctx.addIssue({ code: "custom", message: "close receipt must bind the same original target and management request" });
64
+ });
65
+ export const executionActionSchema = z.union([executionRegisterSchema, executionStatusSchema, executionFinishSchema, executionReconcileLaunchSchema, executionCloseLaunchSchema]);
50
66
  export const executionRefMetaSchema = z.object({
51
67
  schema: z.literal("aiterm.execution-ref.v1"), request: executionRequestSchema,
52
68
  }).strict();
package/docs/DESIGN.md CHANGED
@@ -453,11 +453,25 @@ unknown send枠を保持したまま既存core closeへ進み、native close成
453
453
  旧callbackは未知/消失した元slotで拒否され、同IDの新launchへnative作用を発行できない。
454
454
  issued、管理close自体のunknown、親作用のslot、世代/native/serverの不明・交代は対象外。
455
455
  既存の送信・待機lockの拒否も維持し、他席やissuerの停止を復旧条件へ追加しない。
456
+ 返却済みunknown launchも、元native/server・managed世代が一致し、raw initial_prompt:none・初期配送なし・
457
+ send:not_issuedで、harnessの終了を現在の同期primitiveが証明できる場合だけ、同じ明示管理closeを許す(ADR 0131)。
458
+ 受付時とnative作用直前にこの証拠を検査し、closeと不在確認後に該当slotを外す。元unknown recordを保ち、
459
+ 旧requestのlaunch/sendは再実行しない。生存harnessはreconcile_launchの対象として維持する。
460
+ 本体からの明示復旧には既存agent_executionのclose_launch actionを使う(ADR 0132)。元requestと
461
+ target(session_id,launch_id)を原子的な期待条件として受付と作用直前に照合し、応答SDK自身の管理refで
462
+ 既存close primitiveへ進む。呼出元がobserveしたsessionだけをcloseする手順で代用しない。
463
+ close・不在確認・metadata commit後だけ_meta["aiterm/closed_launch"]のversion1 receiptを返す。
464
+ structuredContentは別管理requestのrecord、元unknown recordは不変。結果不明や保存失敗にはclosed証明なし。
465
+ 本体は元pending操作・stop/versionを別途照合し、後発不明を過去receiptで解除しない。
466
+ 非同期launcherはCLIの検証をgraph mutex外で行い、実create→metadata公開→世代結合→CLI入力の同期区間を
467
+ 一度のnative admission内で通す。起動準備のawaitをmutex内に持ち込まず、待機後には通常の世代・停止・slot検査を行う。
456
468
  空起動の結果不明は明示 `agent_execution/reconcile_launch` で既存世代へ結合できる(ADR 0128)。
457
469
  対象は返却済みunknown launch、send:not_issued、元native/server stamp付きの単独slotだけ。
458
470
  同じmetadata transaction内のprivate同期primitiveで現在のnative/managed世代、生のinitial_prompt:none・初期配送なし、
459
471
  harness生存とidleを照合する。既存recordのlaunchをaccepted、targetを当該世代へ結合し、該当slotだけを外す。
460
472
  これは現在世代の明示採用で、失われたRPC receiptの復元ではない。本文・初期prompt・claim・nativeには作用しない。
473
+ 同じaccepted targetへの再照合もsend:not_issuedなら現在のmanaged世代・生の未入力状態・入力待ちを再確認し、
474
+ 同じsessionへの競合slotや親の未確定作用があれば拒否する。保存済みacceptedを後の世代の不明解除の証拠にしない。
461
475
  issued、入力済み・不明、停止、別slot、世代/native/serverの不一致は拒否。後続sendは元source世代を含め通常受付を通る。
462
476
  遅着した元callbackはまだ所有するissued stage/slotがある時だけunknownを書け、照合後や別stageを上書きしない。
463
477
  native handlerのisErrorは元の本文を保持し、`aiterm/execution_error`のcode/effectを併記する。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.61.7",
3
+ "version": "0.61.9",
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": [