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 +35 -1
- package/README.ja.md +14 -4
- package/README.md +16 -4
- package/dist/core.js +55 -5
- package/dist/delivery-wake-cli.js +142 -0
- package/dist/process-runtime.js +7 -0
- package/docs/DESIGN.md +3 -0
- package/package.json +3 -2
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.
|
|
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.
|
|
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の環境で決まる(
|
|
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
|
|
521
|
-
(`codex-code-mode-host
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1182
|
-
|
|
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
|
+
}
|
package/dist/process-runtime.js
CHANGED
|
@@ -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.
|
|
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.
|
|
74
|
+
"aiterm-steer-delivery": "^0.1.12",
|
|
74
75
|
"ws": "8.21.3",
|
|
75
76
|
"zod": "^4.4.3"
|
|
76
77
|
},
|