aiterm-mcp 0.50.0 → 0.51.1

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,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.51.1] - 2026-10-04
11
+
12
+ ### 修正
13
+
14
+ - `pty_observe`の`activity.post_startup_process_count`で、harnessが立て直した起動時のprocessを数えない(ADR 0083)。
15
+ Cursorは起動時にstdioのMCPを立て、最初のつなぎが済むと止めて立て直す。つながるのが遅いMCPがあると、立て直しが
16
+ 起動完了の後になり、何もしていない席で数が残っていた(実物で、直結3・中継ごし4)。起動時のprocessが終了していて、
17
+ 同じ親の下に同じargvで立っているprocessは、その立て直しとして扱う。その下で動くprocessは今までどおり数える。
18
+ この版より前のAitermが起動したagentは、今までどおり数える。
19
+
20
+ ## [0.51.0] - 2026-10-04
21
+
22
+ ### 追加
23
+
24
+ - `aiterm-delivery-wake --parent <codex|claude|cursor>`。MCPの本体を使う時だけ起こす中継(`mcp-lazy`)の後ろで、眠っている本体を
25
+ 親配送の引き取りのために起こすべきかを返す(ADR 0082)。終了した持ち主がその種類の親の配送を持ったままならexit 0、
26
+ 無ければexit 1。何も出力せず、子processを起こさない。同じ判定を回す席のうち1席だけが起こす。
27
+ 中継には先行起動の判定として渡す(`MCP_LAZY_WAKE_COMMAND`)。
28
+
29
+ ### 変更
30
+
31
+ - `pty_observe`の`activity.post_startup_process_count`で、中継`mcp-lazy`の直接の子を数えない(ADR 0082)。
32
+ 中継は道具を初めて呼ばれた時に本体を自分の下に起こすので、中継ごしの席は道具を一度使うと数が0へ戻らなかった。
33
+ 見分けるのは、親の起動時のargvの先頭のbasenameが`mcp-lazy`で始まること。その下で動くprocessは今までどおり数える。
34
+
35
+ ### 修正
36
+
37
+ - Cursor親の受け取りprocess(`cursor-parent-receive`)が、親のCursorが終わった後も残り、後から完了した子の回答を引き取っていた
38
+ (aiterm-steer-delivery 0.1.12)。Cursorの背景shellは親から切り離されて動くので、席を閉じても最長24時間残る。
39
+ 引き取られた回答は`submitted`になるが、読む相手は居ない。受け取りprocessは、出力の読み手が居なくなったら引き取らずに終わる。
40
+ その配送は`sending`のまま残り、24時間で`CURSOR_PARENT_DELIVERY_UNCLAIMED`になる。本文は今までどおり記録に残る。
41
+
10
42
  ## [0.50.0] - 2026-10-04
11
43
 
12
44
  ### 変更
@@ -2028,7 +2060,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2028
2060
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2029
2061
  provenance.
2030
2062
 
2031
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.50.0...HEAD
2063
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.1...HEAD
2064
+ [0.51.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.0...v0.51.1
2065
+ [0.51.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.50.0...v0.51.0
2032
2066
  [0.50.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.49.0...v0.50.0
2033
2067
  [0.49.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.48.0...v0.49.0
2034
2068
  [0.48.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.47.1...v0.48.0
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.50.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.51.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
@@ -299,7 +299,7 @@ $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeo
299
299
 
300
300
  Cursorの`model`は`gpt-5.6-luna`のようなbase model、`reasoning_effort`は`high`のように別指定する。adapterは現行`model-effort` IDを`cursor-agent models`へ照合し、起動中変更はCursor標準model pickerのparameter editorを使う。不在時は別modelへfallbackしない。
301
301
 
302
- 新しい端末の環境は、その端末を開いたMCP processの環境で決まる(v0.50.0から。ADR 0081)。tmuxのserverを
302
+ 新しい端末の環境は、その端末を開いたMCP processの環境で決まる(0.50.0から。ADR 0081)。tmuxのserverを
303
303
  先に起こした別の呼び出し元の値は入らない。Aitermが端末ごとに付け直す`AITERM_SESSION_ID`・`AITERM_AGENT_*`は
304
304
  継がない。harnessがMCP processへ渡す環境が少なければ、端末の環境もその分だけになる(Codexは既定で
305
305
  `HOME`・`PATH`・`SHELL`・`TERM`。要る変数は`[mcp_servers.aiterm]`の`env_vars`で渡す)。
@@ -517,11 +517,21 @@ sessionへ登録する名前の配列で、`pty_list({ env_keys: ["JOB_OWNER"] }
517
517
  `token_hint`は画面の直近token表示値またはnull。画面本文や生argvを解析する必要はない。
518
518
 
519
519
  agent sessionを閉じると失うものは、次の2項目で分かる。`activity.post_startup_process_count`は、`agent_launch`の起動準備が
520
- 完了した時点(初手を送る前)に居なかったprocessの数。起動直後の1分に始めた裏の作業も数える。Codex自身の補助process
521
- (`codex-code-mode-host`)は数えない。`pending_child_deliveries`は、そのsessionが親として待っていて、まだ届け終えていない
520
+ 完了した時点(初手を送る前)に居なかったprocessの数。起動直後の1分に始めた裏の作業も数える。数えないものは2つ。Codex自身の
521
+ 補助process(`codex-code-mode-host`)と、中継`mcp-lazy`の直接の子(初めて使う時に起こすMCPの本体と、先行起動の判定)。
522
+ その下で動くprocessは数える。harnessが立て直した起動時のprocess(終了した起動時のprocessと同じ親の下に、同じargvで立ったもの。
523
+ つなぎ直したMCPなど)も数えない。`pending_child_deliveries`は、そのsessionが親として待っていて、まだ届け終えていない
522
524
  子の結果の数。呼び出した側が誰でも付く。どちらも、分からない時はnull(通常PTY、harnessのprocessを特定できない時。
523
525
  processの数は0.48.0以前が起動したagentも)。別端末が旧版の時は項目ごと無い。nullと項目なしは「分からない」で、0ではない。
524
526
 
527
+ MCPの本体を初めて使う時まで起こさない中継の後ろにAitermを登録すると、眠っている本体は、終了した持ち主の配送を引き取れない。
528
+ `aiterm-delivery-wake --parent <codex|claude|cursor>`は、起こす必要があるかを返す。その種類の親の配送を、終了した持ち主が
529
+ 持ったままならexit 0、そうでなければexit 1、引数の誤りはexit 2。何も出力せず、子processを起こさない。同じ判定を回す席のうち
530
+ 1席だけが起こすよう、配送に印を付ける(引き取られないまま30秒過ぎたら、次の判定が印を取り直す)。`mcp-lazy`には、本体と同じ環境で
531
+ 先行起動の判定として渡す。
532
+ `MCP_LAZY_WAKE_COMMAND='["/absolute/path/to/node","/absolute/path/to/aiterm-mcp/dist/delivery-wake-cli.js","--parent","claude"]'`
533
+ 持ち主の開始時刻はLinuxでは`/proc`で照合する。それ以外のOSでは、pidがあれば生きている持ち主として扱う。
534
+
525
535
  `agent_launch({ harness, cwd, trust_project: true })`はpromptなしでも既知のworkspace・project hooks・MCP初期同意を
526
536
  進め、入力受付とharness生存を確認して`startup.status="ready"`を返す。指定なしのpromptなし起動は`not_checked`。
527
537
  Claude Code初回起動の文字表示テーマ選択では、画面で選択済みの項目を確定して起動を続ける。
package/README.md CHANGED
@@ -223,7 +223,7 @@ collection is off by default and performs no network I/O. It ships via
223
223
  tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
224
224
  Release re-registers the Official MCP Registry entry.
225
225
 
226
- **Status:** actively maintained · current public release **v0.50.0** · 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).
226
+ **Status:** actively maintained · current public release **v0.51.1** · 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).
227
227
 
228
228
  ### Update and rollback
229
229
 
@@ -321,7 +321,7 @@ The canonical harness choices are:
321
321
  | `grok-cli` | Grok Build CLI | Grok model selected with `model`; live catalog check |
322
322
  | `cursor-cli` | Cursor Agent CLI | GPT, Claude, Grok, or another Cursor catalog model; normal transcript completion |
323
323
 
324
- A new terminal gets the environment of the MCP process that opened it (since v0.50.0, ADR 0081).
324
+ A new terminal gets the environment of the MCP process that opened it (since 0.50.0, ADR 0081).
325
325
  Values from another caller that happened to start the tmux server first no longer appear in it.
326
326
  `AITERM_SESSION_ID` and `AITERM_AGENT_*` are not inherited; aiterm sets them per terminal. If a harness
327
327
  passes only a few variables to its MCP process, the terminal has only those (Codex passes `HOME`, `PATH`,
@@ -550,11 +550,23 @@ displayed token count or null. Callers do not need raw argv or pane-text parsing
550
550
 
551
551
  Two fields tell a caller what closing an agent session would lose. `activity.post_startup_process_count` is the number of
552
552
  processes in the session that did not exist when `agent_launch` finished startup (before the first prompt), so background work
553
- started in the first minute is counted too; Codex's own `codex-code-mode-host` helper is not counted. `pending_child_deliveries`
553
+ started in the first minute is counted too. Two kinds of process are not counted: Codex's own `codex-code-mode-host` helper, and
554
+ the direct children of an `mcp-lazy` relay (the MCP server it starts on first use and its wake predicate). What runs below them
555
+ is counted. A startup process that the harness restarted (a process with the same parent and the same arguments as a startup
556
+ process that has exited, such as a reconnected MCP server) is not counted either. `pending_child_deliveries`
554
557
  is the number of sub-agent results this session is still waiting for as a parent, whoever the caller is. Both are null when
555
558
  Aiterm cannot tell (ordinary terminals, agents launched by 0.48.0 or earlier for the process count, or an unresolved harness
556
559
  process), and may be absent when a remote host runs an older Aiterm. Treat null or absent as unknown, not as zero.
557
560
 
561
+ When Aiterm is registered behind a relay that starts the MCP server only on first use, a sleeping server cannot take over the
562
+ deliveries of an owner that has exited. `aiterm-delivery-wake --parent <codex|claude|cursor>` answers whether one needs to start:
563
+ exit 0 when an exited owner still holds a delivery for that parent kind, exit 1 otherwise, exit 2 for bad arguments. It prints
564
+ nothing, starts no child process, and marks the delivery so that only one of the seats running the same check starts its server
565
+ (the mark is retaken after 30 seconds if nobody took the delivery over). For `mcp-lazy`, pass it as the wake predicate with the
566
+ same environment as the server:
567
+ `MCP_LAZY_WAKE_COMMAND='["/absolute/path/to/node","/absolute/path/to/aiterm-mcp/dist/delivery-wake-cli.js","--parent","claude"]'`.
568
+ Owner start times are checked through `/proc` on Linux; elsewhere an existing PID is treated as a live owner.
569
+
558
570
  `agent_launch({ harness, cwd, trust_project: true })` completes known workspace, project-hook, and project-MCP startup
559
571
  consent even without a prompt, then verifies input readiness and harness liveness before returning `startup.status="ready"`.
560
572
  For Claude Code's first-run text-style menu, it confirms the item already selected on screen before continuing startup.
@@ -791,7 +803,7 @@ trust, memory, or history stores. Cleanup removes only aiterm-owned launch metad
791
803
  correlation files.
792
804
 
793
805
  The ordinary environment comes from the MCP process that opened the terminal, not from whichever caller
794
- started the persistent multiplexer server (since v0.50.0). Every harness still accepts
806
+ started the persistent multiplexer server (since 0.50.0). Every harness still accepts
795
807
  `env_vars: ["NAME", ...]`; those names are placed on the launch command and registered on the session.
796
808
 
797
809
  ## License
package/dist/core.js CHANGED
@@ -16,7 +16,7 @@ import * as rtk from "./rtk.js";
16
16
  import { isCursorMcpClient } from "aiterm-steer-delivery";
17
17
  import { paneTokenHint } from "./harnesses/pane-tokens.js";
18
18
  import { unfinishedDeliveriesOwnedBy } from "./parent-delivery-owners.js";
19
- import { readRuntimeProcesses, processSubtree, parentProcess, processIdentity, backgroundProcesses } from "./process-runtime.js";
19
+ import { readRuntimeProcesses, processSubtree, parentProcess, processIdentity, backgroundProcesses, lazyRelayProcess } from "./process-runtime.js";
20
20
  import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
21
21
  import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, tmuxNewSession, registeredSessionEnvironment, attachCommand, normalizePaneCommand, atomicShellMultiline, appendMarkSentinel, markShellCommand, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
22
22
  import { sleep, currentUid, runtimeStateBase, safeStatSize, readFileRange, writeJson0600, createEmpty0600, shq, LAUNCH_ID_RE, AGENT_DONE_POLL_MS, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
@@ -1077,6 +1077,54 @@ export function selectHarnessProcesses(meta, rows, subtree) {
1077
1077
  });
1078
1078
  return roots;
1079
1079
  }
1080
+ const processKey = (row) => `${row.pid}:${row.started_identity}`;
1081
+ /** 起動完了の控えに入れたprocessの、親とargvのdigest。控えた後に終了したprocessは入らない。 */
1082
+ export function startupProcessShapes(identities, rows) {
1083
+ const byPid = new Map(rows.map(row => [row.pid, row]));
1084
+ const byKey = new Map(rows.map(row => [processKey(row), row]));
1085
+ return identities.flatMap(identity => {
1086
+ const row = byKey.get(identity);
1087
+ if (!row)
1088
+ return [];
1089
+ const parent = parentProcess(row, byPid);
1090
+ return [{ identity, parent: parent ? processKey(parent) : null, argv_digest: row.argv_digest }];
1091
+ });
1092
+ }
1093
+ // 起動完了の控えに無いprocessの数。harnessと中継が自分のために立てるprocessは、利用者の作業ではないので数えない。
1094
+ // その下で動くprocessは数える。
1095
+ // harnessは起動の足場(MCP、language server)を自分で立て直す。控えた後に立て直されると、控えに無いprocessとして残る。
1096
+ // 控えの物が終了していて、同じ親の下に同じargvで立ったprocessは、その立て直しとして数えない。
1097
+ // 親も立て直されている時は、立て直された親の下を見る。終了した控え1つにつき、数えないのは1つまで。
1098
+ export function postStartupProcessCount(kind, activityRows, rows, startup, shapes = []) {
1099
+ const byPid = new Map(rows.map(row => [row.pid, row]));
1100
+ const relayChild = (row) => {
1101
+ const parent = parentProcess(row, byPid);
1102
+ return parent !== undefined && lazyRelayProcess(parent.command);
1103
+ };
1104
+ const present = new Set(activityRows.map(processKey));
1105
+ const added = new Map(activityRows
1106
+ .filter(row => !startup.has(processKey(row)) && !(kind === "codex" && codexHelperProcess(row.command)) && !relayChild(row))
1107
+ .map(row => [processKey(row), row]));
1108
+ const gone = shapes.filter(shape => startup.has(shape.identity) && !present.has(shape.identity));
1109
+ const replacedBy = new Map();
1110
+ for (let changed = gone.length > 0; changed;) {
1111
+ changed = false;
1112
+ for (const [key, row] of added) {
1113
+ const parent = parentProcess(row, byPid);
1114
+ if (!parent)
1115
+ continue;
1116
+ const parentKey = processKey(parent);
1117
+ const original = gone.find(shape => !replacedBy.has(shape.identity) && shape.argv_digest === row.argv_digest
1118
+ && shape.parent !== null && (shape.parent === parentKey || replacedBy.get(shape.parent) === parentKey));
1119
+ if (!original)
1120
+ continue;
1121
+ replacedBy.set(original.identity, key);
1122
+ added.delete(key);
1123
+ changed = true;
1124
+ }
1125
+ }
1126
+ return added.size;
1127
+ }
1080
1128
  export function observeSession(name, cursor) {
1081
1129
  assertSessionName(name);
1082
1130
  const previous = cursor === undefined ? null : decodeActivityCursor(cursor, name);
@@ -1178,9 +1226,8 @@ export function observeSession(name, cursor) {
1178
1226
  background_cpu_seconds: Object.values(backgroundCpu).reduce((sum, cpu) => sum + cpu, 0),
1179
1227
  background_cpu_delta_seconds: comparable ? Object.entries(backgroundCpu).reduce((sum, [identity, cpu]) => sum + cpu - (previous.background_processes[identity] ?? 0), 0) : null,
1180
1228
  background_cpu_delta_complete: comparable ? Object.keys(previous.background_processes).every(identity => identity in backgroundCpu) : null,
1181
- post_startup_process_count: startup ? new Set(activityRows
1182
- .filter(row => !(meta?.kind === "codex" && codexHelperProcess(row.command)))
1183
- .map(row => `${row.pid}:${row.started_identity}`).filter(identity => !startup.has(identity))).size : null,
1229
+ post_startup_process_count: startup
1230
+ ? postStartupProcessCount(meta.kind, activityRows, rows, startup, meta.startup_process_shapes) : null,
1184
1231
  };
1185
1232
  return result;
1186
1233
  }
@@ -2003,8 +2050,10 @@ function loadAgentMetadata(name) {
2003
2050
  initial_prompt_delivery: m.initial_prompt_delivery, initial_prompt_cursor: m.initial_prompt_cursor,
2004
2051
  };
2005
2052
  const executableFields = m.agent_executable === undefined ? {} : { agent_executable: m.agent_executable };
2053
+ const startupShapes = Array.isArray(m.startup_process_shapes) && m.startup_process_shapes.every((shape) => shape && typeof shape.identity === "string" && (shape.parent === null || typeof shape.parent === "string") && typeof shape.argv_digest === "string")
2054
+ ? { startup_process_shapes: m.startup_process_shapes } : {};
2006
2055
  const startupFields = Array.isArray(m.startup_processes) && m.startup_processes.every((identity) => typeof identity === "string")
2007
- ? { startup_processes: m.startup_processes } : {};
2056
+ ? { startup_processes: m.startup_processes, ...startupShapes } : {};
2008
2057
  if (m.kind === "claude") {
2009
2058
  const expectedSettings = agentManagedClaudeSettingsPath(name, m.launch_id);
2010
2059
  const expectedResult = agentClaudeResultPath(name, m.launch_id);
@@ -3225,6 +3274,7 @@ async function prepareAgentInput(name, meta, options) {
3225
3274
  // ここまでに居るprocessは起動時の足場(harness・MCP・起動時hook)。初手を送る前に控える。
3226
3275
  if (live.activity.cursor !== null) {
3227
3276
  meta.startup_processes = Object.keys(decodeActivityCursor(live.activity.cursor, name).processes);
3277
+ meta.startup_process_shapes = startupProcessShapes(meta.startup_processes, readRuntimeProcesses());
3228
3278
  writeAgentMetadata(meta);
3229
3279
  }
3230
3280
  return { status: "ready", reason: "composer_ready" };
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ // aiterm-delivery-wake — 眠っているAitermのMCP本体を、親配送の引き取りのために起こすべきかを返す。
3
+ // MCPを使う時だけ起こす中継(mcp-lazy)の先行起動の判定に渡す。本体が眠っている間は、終了した持ち主の配送を
4
+ // 引き取る者が居ない(引き取りは起きている本体が5秒おきに行う)。
5
+ // exit 0=起こす / 1=眠ったままでよい / 2=引数の誤り。stdoutへは何も出さない。
6
+ // 中継の下で動くので、子processを起こさない(起こすと席のprocess数に入る)。node builtinとstate-rootだけに依存する。
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { currentUid, runtimeStateBase } from "./state-root.js";
11
+ const DIRECTORY = {
12
+ codex: "parent-deliveries", claude: "claude-parent-deliveries", cursor: "cursor-parent-deliveries",
13
+ };
14
+ // 起こす役を引き受けた印の寿命。起こした本体は起動してすぐ引き取る。引き取られないまま過ぎたら、次の判定が引き受け直す。
15
+ const CLAIM_TTL_MS = 30_000;
16
+ const USAGE = "usage: aiterm-delivery-wake --parent <codex|claude|cursor>";
17
+ function processExists(pid) {
18
+ if (pid <= 0)
19
+ return false;
20
+ try {
21
+ process.kill(pid, 0);
22
+ return true;
23
+ }
24
+ catch (error) {
25
+ return error.code === "EPERM";
26
+ }
27
+ }
28
+ // psのlstartと同じ値を/procから作る(起動時刻+開始tick。LinuxのUSER_HZは100)。/procの無いOSはnull。
29
+ function linuxStartedAt(pid) {
30
+ try {
31
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
32
+ const ticks = Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]);
33
+ const boot = Number(/^btime (\d+)$/m.exec(fs.readFileSync("/proc/stat", "utf8"))?.[1]);
34
+ return Number.isFinite(ticks) && Number.isFinite(boot) ? (boot + Math.floor(ticks / 100)) * 1000 : null;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ const DEFAULTS = { exists: processExists, started_at: linuxStartedAt, now: () => Date.now() };
41
+ function ownerGone(owner, deps) {
42
+ const value = owner;
43
+ if (!value || !Number.isSafeInteger(value.pid) || typeof value.started_identity !== "string" || typeof value.closed !== "boolean")
44
+ return false;
45
+ if (value.closed)
46
+ return true;
47
+ const pid = value.pid;
48
+ if (!deps.exists(pid))
49
+ return true;
50
+ // 同じpidが別のprocessへ再利用されていないかを、開始時刻で見る。照合できない時は生きている扱い(本体が起きた時に照合する)。
51
+ const recorded = Date.parse(value.started_identity);
52
+ const actual = deps.started_at(pid);
53
+ return Number.isFinite(recorded) && actual !== null && Math.abs(actual - recorded) > 2_000;
54
+ }
55
+ /**
56
+ * 終了した持ち主の下に、引き取られていない配送の記録があれば、起こす役を引き受けてtrueを返す。
57
+ * 同じ種類の席は全部が同じ判定を回す。印を先に作れた1席だけが起こす。
58
+ */
59
+ export function shouldWakeForDeliveries(stateRoot, parent, dependencies = {}) {
60
+ const deps = { ...DEFAULTS, ...dependencies };
61
+ let wake = false;
62
+ for (const prefix of ["", "remote-"]) {
63
+ const root = path.join(stateRoot, prefix + DIRECTORY[parent]);
64
+ const active = path.join(root, "active");
65
+ const claims = path.join(root, "wake-claims");
66
+ let owners;
67
+ try {
68
+ owners = fs.readdirSync(active, { withFileTypes: true });
69
+ }
70
+ catch {
71
+ continue;
72
+ }
73
+ const orphaned = new Set();
74
+ for (const directory of owners) {
75
+ if (!directory.isDirectory())
76
+ continue;
77
+ const ownerDir = path.join(active, directory.name);
78
+ let owner;
79
+ let records;
80
+ try {
81
+ owner = JSON.parse(fs.readFileSync(path.join(ownerDir, "owner.json"), "utf8"));
82
+ records = fs.readdirSync(ownerDir).filter(name => name !== "owner.json" && name.endsWith(".json"));
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ if (records.length > 0 && ownerGone(owner, deps))
88
+ orphaned.add(directory.name);
89
+ }
90
+ // 引き取りが済んだ持ち主の印を片付ける。
91
+ try {
92
+ for (const name of fs.readdirSync(claims))
93
+ if (!orphaned.has(name))
94
+ fs.rmSync(path.join(claims, name), { recursive: true, force: true });
95
+ }
96
+ catch { /* 印の置き場がまだ無い */ }
97
+ for (const name of orphaned) {
98
+ const claim = path.join(claims, name);
99
+ try {
100
+ fs.mkdirSync(claims, { recursive: true, mode: 0o700 });
101
+ fs.mkdirSync(claim, { mode: 0o700 });
102
+ wake = true;
103
+ }
104
+ catch (error) {
105
+ if (error.code !== "EEXIST")
106
+ throw error;
107
+ const now = deps.now();
108
+ if (now - fs.statSync(claim).mtimeMs < CLAIM_TTL_MS)
109
+ continue;
110
+ fs.utimesSync(claim, now / 1000, now / 1000);
111
+ wake = true;
112
+ }
113
+ }
114
+ }
115
+ return wake;
116
+ }
117
+ function main(argv) {
118
+ if (argv.length !== 2 || argv[0] !== "--parent" || !(argv[1] in DIRECTORY)) {
119
+ process.stderr.write(`${USAGE}\n`);
120
+ return 2;
121
+ }
122
+ return shouldWakeForDeliveries(path.join(runtimeStateBase(), `aiterm-mcp-${currentUid()}`), argv[1]) ? 0 : 1;
123
+ }
124
+ function invokedDirectly() {
125
+ if (!process.argv[1])
126
+ return false;
127
+ try {
128
+ return fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url));
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ }
134
+ if (invokedDirectly()) {
135
+ try {
136
+ process.exitCode = main(process.argv.slice(2));
137
+ }
138
+ catch (error) {
139
+ process.stderr.write(`aiterm-delivery-wake: ${error instanceof Error ? error.message : String(error)}\n`);
140
+ process.exitCode = 3;
141
+ }
142
+ }
@@ -1,6 +1,7 @@
1
1
  // OSのprocess表とnative PIDの所有者。argvは相関にだけ使い、公開identityにはdigestだけを載せる。
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
+ import path from "node:path";
4
5
  import { AitermError } from "./errors.js";
5
6
  import { isWin } from "./tmux-runtime.js";
6
7
  import { resolveWindowsPowerShell7 } from "./windows-powershell.js";
@@ -153,6 +154,12 @@ export function processSubtree(rows, rootPid) {
153
154
  }
154
155
  return rows.filter(row => selected.has(row.pid));
155
156
  }
157
+ // 中継(mcp-lazy)は、登録されたMCPの本体と先行起動の判定を、自分の直接の子として起こす。
158
+ // 見分けるのは起動した実行ファイルの名前(argvの先頭のbasenameが`mcp-lazy`で始まる)。取り決めはmcp-lazyのREADMEにある。
159
+ export function lazyRelayProcess(command) {
160
+ const first = /^(?:"([^"]+)"|(\S+))/.exec(command);
161
+ return path.posix.basename((first?.[1] ?? first?.[2] ?? "").replace(/\\/g, "/")).toLowerCase().startsWith("mcp-lazy");
162
+ }
156
163
  export function backgroundProcesses(rows, root) {
157
164
  // 起動時の足場processを除く既存契約。pane開始から60秒以上後に生成された子孫だけを集計する。
158
165
  const rootStart = Date.parse(root.started_identity);
package/docs/DESIGN.md CHANGED
@@ -54,6 +54,9 @@ PIDは開始識別子・argv digestと組にし、paneとharnessを同一視し
54
54
  POSIXの停止状態はOSのprocess表から取得し、SIGSTOP中は残画面より優先して`blocked/harness_stopped`を返す。
55
55
  sessionを閉じると失うものは数で返す(ADR 0080)。`activity.post_startup_process_count`は起動完了の時点に居なかったprocessの数、
56
56
  `pending_child_deliveries`はそのsessionが親として待つ未配送の数。数えられない時はnullで、0と区別する。
57
+ harnessと中継が自分のために立てるprocessは数えない(Codexの`codex-code-mode-host`、中継`mcp-lazy`の直接の子。ADR 0082)。その下は数える。
58
+ harnessが同じ親の下に同じargvで立て直した起動時のprocessも数えない(ADR 0083)。
59
+ 中継の後ろで眠っている本体を配送の引き取りのために起こすかは、`aiterm-delivery-wake`が返す(ADR 0082)。
57
60
  Claude Codeの入力待ちは、起動時の見出しが取得範囲から流れ出た後も、入力欄の形(`❯`行の上下の罫線)で読む。
58
61
  画面本文とargv本文は返さず、活動cursorには画面digestとprocess別CPUだけを持たせる。
59
62
  初回とpane再作成後の差分はnull。区間中にprocessが消えた時は観測できたCPU増分だけを返し、
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiterm-mcp",
3
- "version": "0.50.0",
3
+ "version": "0.51.1",
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": [
@@ -43,6 +43,7 @@
43
43
  "aiterm-setup": "dist/setup-cli.js",
44
44
  "aiterm-runtime-errors": "dist/runtime-errors-cli.js",
45
45
  "aiterm-wait": "dist/aiterm-wait-cli.js",
46
+ "aiterm-delivery-wake": "dist/delivery-wake-cli.js",
46
47
  "aiterm-update": "dist/update-cli.js"
47
48
  },
48
49
  "files": [
@@ -70,7 +71,7 @@
70
71
  },
71
72
  "dependencies": {
72
73
  "@modelcontextprotocol/sdk": "^1.29.0",
73
- "aiterm-steer-delivery": "^0.1.10",
74
+ "aiterm-steer-delivery": "^0.1.12",
74
75
  "ws": "8.21.3",
75
76
  "zod": "^4.4.3"
76
77
  },