aiterm-mcp 0.29.5 → 0.29.7

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/README.md CHANGED
@@ -472,7 +472,7 @@ When an agent's answer is longer than the on-screen tail (pane height ≈ 24 lin
472
472
  As of v0.16 a parent agent **never blocks** on aiterm — there is no wait parameter anywhere (v0.17 makes the waiter's exit codes mirror its outcome). The whole flow is dispatch + one universal waiter:
473
473
 
474
474
  1. Launch the child with `agent_launch({ harness: ... })`; every launch shares the normal project/user environment and adds only completion correlation plus lineage. Send a turn with plain `pty_send` (or `claude_turn issue` for durable Claude operations). The call returns immediately with an `event_cursor` in its structured receipt.
475
- 2. Run `aiterm-wait --session <id> --cursor <event_cursor> [--operation sha256:<64hex>] [--timeout <sec>]`. It observes the harness-owned completion source, plus Claude's additive launch hook, as a **pure reader** and exits with a one-line `aiterm.agent-wait-result.v1` receipt. **Exit ≠ done**: the receipt's `outcome` is authoritative (`0` = `done`, `3` = `timeout`, `4` = `closed`, `1` = error).
475
+ 2. Pass the receipt's `wait_process.executable` and `wait_process.args` unchanged to a true argv process API. PowerShell 7's `Start-Process` is the exception because it joins `-ArgumentList` arrays; pass `windows_start_process_argument_list` as its one ready-made argument string instead. This invokes the bundled waiter through the exact Node runtime that is already running aiterm, including on native Windows where npm's human-facing bin is a PowerShell script shim and install paths may contain spaces. `wait_command` remains a compatibility display string for humans. The waiter observes the harness-owned completion source, plus Claude's additive launch hook, as a **pure reader** and exits with a one-line `aiterm.agent-wait-result.v1` receipt. **Exit ≠ done**: the receipt's `outcome` is authoritative (`0` = `done`, `3` = `timeout`, `4` = `closed`, `1` = error).
476
476
  3. **The parent never runs the waiter in its own foreground.** Waiting is correct — but the waiter is a separate process, not the parent's turn. A harness that re-invokes its agent when a background task exits (Claude Code) runs the waiter **in the background** and gets woken with zero polling. So that this is not left to interpretation, aiterm reads `clientInfo.name` from the MCP `initialize` handshake and its receipts name the concrete invocation for the detected host — for Claude Code, literally `Bash(command: "aiterm-wait …", run_in_background: true)`. Unknown or undeclared hosts get the generic "start it as a process that does not block the parent's turn" wording; nothing else about the contract changes. Every receipt leads with the same rule: dispatch and let go, then go do something else or end the turn.
477
477
  4. Collect the result exactly as before: `pty_read(agent_transcript: true)`, or `claude_turn recover` for durable Claude operations. The waiter carries the signal, never the payload.
478
478
 
package/dist/core.js CHANGED
@@ -11,6 +11,7 @@ import { spawnSync } from "node:child_process";
11
11
  import * as fs from "node:fs";
12
12
  import * as path from "node:path";
13
13
  import { createHash, randomBytes } from "node:crypto";
14
+ import { fileURLToPath } from "node:url";
14
15
  import * as rtk from "./rtk.js";
15
16
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
16
17
  import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, TMUX_EMPTY_CONFIG, attachCommand, normalizePaneCommand, appendMarkSentinel, settlePaneLog, paneCwdArgument, } from "./tmux-runtime.js";
@@ -2144,6 +2145,48 @@ function assertInitialPromptNotPendingForSend(name, force) {
2144
2145
  }
2145
2146
  // aiterm-wait の exit 契約(CLI と各所の案内文で共有する正)。exit≠完了: outcome が done の時だけ完了。
2146
2147
  export const AITERM_WAIT_OUTCOME_NOTE = `exit 0=done / 3=timeout(既定${DEFAULT_AGENT_DONE_TIMEOUT}秒・未完了) / 4=closed。receiptのoutcomeが正で、done以外は未完了`;
2148
+ function quoteWindowsProcessArgument(value) {
2149
+ if (value !== "" && !/[\s"]/u.test(value))
2150
+ return value;
2151
+ let quoted = '"';
2152
+ let backslashes = 0;
2153
+ for (const char of value) {
2154
+ if (char === "\\") {
2155
+ backslashes += 1;
2156
+ }
2157
+ else if (char === '"') {
2158
+ quoted += "\\".repeat(backslashes * 2 + 1) + '"';
2159
+ backslashes = 0;
2160
+ }
2161
+ else {
2162
+ quoted += "\\".repeat(backslashes) + char;
2163
+ backslashes = 0;
2164
+ }
2165
+ }
2166
+ return quoted + "\\".repeat(backslashes * 2) + '"';
2167
+ }
2168
+ export function windowsStartProcessArgumentList(args) {
2169
+ return args.map(quoteWindowsProcessArgument).join(" ");
2170
+ }
2171
+ // npmのplatform別bin shimをcallerに解釈させず、現在稼働中のNodeと同梱CLIを直接起動する。
2172
+ // Windowsでもbackendはpsmux、対話shellはPowerShell 7のまま。これはwaiter processの入口だけを所有する。
2173
+ export function agentWaitProcess(session, cursor, runtime = {}) {
2174
+ const executable = runtime.executable ?? process.execPath;
2175
+ const args = [
2176
+ runtime.cliPath ?? fileURLToPath(new URL("./aiterm-wait-cli.js", import.meta.url)),
2177
+ "--session",
2178
+ session,
2179
+ "--cursor",
2180
+ String(cursor),
2181
+ ];
2182
+ return {
2183
+ executable,
2184
+ args,
2185
+ windows_start_process_argument_list: (runtime.platform ?? process.platform) === "win32"
2186
+ ? windowsStartProcessArgumentList(args)
2187
+ : null,
2188
+ };
2189
+ }
2147
2190
  // 親ホストの識別(MCP initialize の clientInfo.name)。完了待ちコマンドを「親のターンを塞がない
2148
2191
  // 起動形」で名指しするためだけに使う。分からない時は汎用文へ落ち、機能は一切変えない。
2149
2192
  let parentClientName = null;
package/dist/index.js CHANGED
@@ -27,9 +27,17 @@ const server = new McpServer({ name: "aiterm", version: pkg.version });
27
27
  * ここでは「待つな」を断定形で先に置き、foreground 実行の禁止までを説明の側に含める。
28
28
  */
29
29
  const NON_BLOCKING_RULE = "dispatch した子は投げっぱなしでよい=親はここで待たない。" +
30
- "完了通知は `aiterm-wait --session <id> --cursor <event_cursor>` を親のターンを塞がない別プロセスとして起動して受け、" +
30
+ "完了通知はreceiptの `wait_process.executable` と `wait_process.args` をそのまま親のターンを塞がない別プロセスAPIへ渡して受け、" +
31
+ "PowerShell 7のStart-Processだけは `windows_start_process_argument_list` を単一文字列として渡す。" +
31
32
  `exit を完了通知として扱う(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
32
- "この待ちコマンドを foreground で実行して親のターンを塞ぐことはしない(receipt が実際の起動形を示す)。";
33
+ "`wait_command` は人間向け互換表示でありprocess境界へ使わない。foreground実行で親のターンを塞がない。";
34
+ const waitProcessOutputSchema = z
35
+ .object({
36
+ executable: z.string(),
37
+ args: z.array(z.string()),
38
+ windows_start_process_argument_list: z.string().nullable(),
39
+ })
40
+ .nullable();
33
41
  function ok(s) {
34
42
  return { content: [{ type: "text", text: s }] };
35
43
  }
@@ -134,6 +142,7 @@ server.registerTool("pty_send", {
134
142
  mode: z.enum(["sent", "agent_dispatch"]),
135
143
  session_id: z.string(),
136
144
  event_cursor: z.number().int().nullable(),
145
+ wait_process: waitProcessOutputSchema,
137
146
  launch_id: z.string().nullable(),
138
147
  vendor: z.enum(["claude", "codex", "grok", "composer", "cursor"]).nullable(),
139
148
  harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]).nullable(),
@@ -151,6 +160,7 @@ server.registerTool("pty_send", {
151
160
  if (rtk)
152
161
  throw new Error("agent session への dispatch は rtk:true と併用できません");
153
162
  const receipt = await core.dispatchAgentTurn(session_id, text, { raw });
163
+ const waitProcess = core.agentWaitProcess(receipt.session_id, receipt.event_cursor);
154
164
  return {
155
165
  content: [
156
166
  {
@@ -165,6 +175,7 @@ server.registerTool("pty_send", {
165
175
  mode: "agent_dispatch",
166
176
  session_id: receipt.session_id,
167
177
  event_cursor: receipt.event_cursor,
178
+ wait_process: waitProcess,
168
179
  launch_id: receipt.launch_id,
169
180
  vendor: receipt.vendor,
170
181
  harness: receipt.harness,
@@ -180,6 +191,7 @@ server.registerTool("pty_send", {
180
191
  mode: "sent",
181
192
  session_id,
182
193
  event_cursor: null,
194
+ wait_process: null,
183
195
  launch_id: null,
184
196
  vendor: null,
185
197
  harness: null,
@@ -457,13 +469,14 @@ const agentEffortDesc = (kind) => kind === "claude"
457
469
  : kind === "cursor"
458
470
  ? "Cursor catalogのeffort。指定時はmodel必須で、adapterが model-effort の正規IDへ変換してlive catalogに照合する。"
459
471
  : "Grok Build reasoning effort。利用可能値はCLI/modelのlive catalogに従う。省略時はCLI/model既定。";
460
- // 全launcher共通の完了受信ガイド。待ちコマンドは起動応答の wait_command(初回prompt時)または
461
- // pty_send dispatch の event_cursor から組む。文型は NON_BLOCKING_RULE と同じく「待たない」が先。
472
+ // 全launcher共通の完了受信ガイド。machine callerはreceiptのwait_processをそのまま別process APIへ渡す。
473
+ // wait_commandは人間向け互換表示。文型は NON_BLOCKING_RULE と同じく「待たない」が先。
462
474
  const agentCompletionDesc = `起動して投げたら投げっぱなしでよい=親はここで待たない。` +
463
- `完了通知は起動応答の wait_command(初回prompt時)または pty_send dispatch 後の ` +
464
- `aiterm-wait --session <id> --cursor <event_cursor> を親のターンを塞がない別プロセスとして起動して受ける` +
475
+ `完了通知は起動応答またはpty_send dispatch receiptの wait_processを、親のターンを塞がない` +
476
+ `別プロセスAPIへexecutable/argsの境界を保ったまま渡して受ける` +
477
+ `(PowerShell 7のStart-Processはwindows_start_process_argument_listを使う)` +
465
478
  `(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要・foreground実行はしない)。` +
466
- `結果回収は pty_read(agent_transcript:true)。`;
479
+ `wait_commandは人間向け互換表示。結果回収は pty_read(agent_transcript:true)。`;
467
480
  const agentEnvironmentDesc = `通常CLIと同じHOME・cwd・project/user/local設定・MCP・plugin・skill・permission/trustを共有する。` +
468
481
  `aitermは完了相関stateだけをlaunch単位で所有する。起動されたagentにはsub-agent自己認識、親session、` +
469
482
  `delegation depth/lineage、delegation_allowed=trueを注入し、必要な追加委譲は許可する。`;
@@ -492,6 +505,7 @@ async function launchAgent(kind, args) {
492
505
  session_id: sid,
493
506
  managed_completion: true,
494
507
  event_cursor: eventCursor,
508
+ wait_process: eventCursor === null ? null : core.agentWaitProcess(sid, eventCursor),
495
509
  wait_command: eventCursor === null ? null : `aiterm-wait --session ${sid} --cursor ${eventCursor}`,
496
510
  submit_residue: submitResidue,
497
511
  ...(supportsWriteScope && write_scope !== undefined
@@ -558,9 +572,10 @@ function registerAgentTool(toolName, kind, desc) {
558
572
  harness: z.literal(core.agentHarness(kind)),
559
573
  session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
560
574
  managed_completion: z.boolean().describe("後方互換field。trueはaiterm完了相関が有効という意味で、project/user環境の隔離を意味しない"),
561
- // 起動時 prompt でturnが走っている時だけ非null(additive拡張)。wait_command はそのままホストの
562
- // バックグラウンドタスクとして実行できる完了待ちコマンド。
575
+ // 起動時promptでturnが走っている時だけ非null。wait_processがmachine向けprocess境界、
576
+ // wait_commandは人間向け互換表示。
563
577
  event_cursor: z.number().int().nullable(),
578
+ wait_process: waitProcessOutputSchema,
564
579
  wait_command: z.string().nullable(),
565
580
  // 初回prompt dispatch後のsubmit座礁観測(additive)。true=composerに残存を確認(submit未成立の疑い)/
566
581
  // false=残存を観測せず(submit成立の保証ではない)/ null=promptなし・判定不能。
@@ -593,6 +608,7 @@ server.registerTool("agent_launch", {
593
608
  session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
594
609
  managed_completion: z.boolean(),
595
610
  event_cursor: z.number().int().nullable(),
611
+ wait_process: waitProcessOutputSchema,
596
612
  wait_command: z.string().nullable(),
597
613
  submit_residue: z.boolean().nullable(),
598
614
  write_scope: z.string().optional(),
@@ -11,7 +11,12 @@ const STORE_SCHEMA = "aiterm-mcp.runtime-errors.v1";
11
11
  const STATE_SCHEMA = "1.0";
12
12
  const MAX_CONFIG_BYTES = 16 * 1024;
13
13
  const MAX_STORE_BYTES = 1024 * 1024;
14
- const WORKER_TIMEOUT_MS = 2_000;
14
+ const POSIX_WORKER_TIMEOUT_MS = 2_000;
15
+ // Windows はprivate DACLの適用とreadbackをPowerShell 7境界で行う。診断は最大2回、
16
+ // 記録はlock queueとstate更新で複数回この境界を通るため、process起動だけを想定した
17
+ // POSIXの2秒上限では正常処理まで強制終了する。各境界自身の5秒上限は維持し、worker全体だけ分ける。
18
+ const WINDOWS_DIAGNOSTIC_WORKER_TIMEOUT_MS = 12_000;
19
+ const WINDOWS_RECORD_WORKER_TIMEOUT_MS = 30_000;
15
20
  export const RUNTIME_ERROR_DEFINITIONS = Object.freeze({
16
21
  "AITERM.PTY_DEPENDENCY_UNAVAILABLE": Object.freeze({
17
22
  component: "pty-dependency", message_template: "PTY dependency is unavailable", severity: "high",
@@ -635,6 +640,11 @@ export class RuntimeErrorStore {
635
640
  }
636
641
  const WORKER = fileURLToPath(new URL("./runtime-error-worker.js", import.meta.url));
637
642
  function fixedStoreFailure(stderr) { stderr("aiterm: runtime error store unavailable\n"); }
643
+ function defaultWorkerTimeoutMs(action) {
644
+ if (process.platform !== "win32")
645
+ return POSIX_WORKER_TIMEOUT_MS;
646
+ return action === "record" ? WINDOWS_RECORD_WORKER_TIMEOUT_MS : WINDOWS_DIAGNOSTIC_WORKER_TIMEOUT_MS;
647
+ }
638
648
  export function recordRuntimeError(code, options = {}) {
639
649
  validateRuntimeObservation({ code });
640
650
  const stderr = options.stderr ?? ((line) => process.stderr.write(line));
@@ -647,7 +657,7 @@ export function recordRuntimeError(code, options = {}) {
647
657
  const child = spawn(process.execPath, [options.workerPath ?? WORKER, "record", code], {
648
658
  stdio: "ignore", windowsHide: true, env: process.env,
649
659
  });
650
- const timer = setTimeout(() => { report(); forceKill(child); }, options.timeoutMs ?? WORKER_TIMEOUT_MS);
660
+ const timer = setTimeout(() => { report(); forceKill(child); }, options.timeoutMs ?? defaultWorkerTimeoutMs("record"));
651
661
  timer.unref();
652
662
  child.once("error", report);
653
663
  child.once("exit", (exitCode, signal) => { clearTimeout(timer); if (exitCode !== 0 || signal)
@@ -686,7 +696,7 @@ export async function runtimeErrorStoreDiagnostic(options = {}) {
686
696
  finish(fallback);
687
697
  return;
688
698
  }
689
- const timer = setTimeout(() => { forceKill(child); finish(fallback); }, options.timeoutMs ?? WORKER_TIMEOUT_MS);
699
+ const timer = setTimeout(() => { forceKill(child); finish(fallback); }, options.timeoutMs ?? defaultWorkerTimeoutMs("diagnostic"));
690
700
  child.stdout.setEncoding("utf8");
691
701
  child.stdout.on("data", (chunk) => { if (stdout.length <= 4096)
692
702
  stdout += chunk; });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.29.5",
3
+ "version": "0.29.7",
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": [