aiterm-mcp 0.58.3 → 0.58.4
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 +20 -1
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/parent-delivery.js +55 -3
- package/dist/process-runtime.js +34 -6
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.58.4] - 2026-10-12
|
|
11
|
+
|
|
12
|
+
### 修正
|
|
13
|
+
|
|
14
|
+
- AitermのMCP processが、互いの生き死にを確かめるためにprocessを起こし続け、端末を重くしていた件を直す(ADR 0112)。
|
|
15
|
+
親への配送の回収は、MCP process 1個につき2本、5秒おきに回り、ほかの持ち主の開始時刻を60秒おきに照合していた
|
|
16
|
+
(WindowsはPowerShell 7とWMI、ほかは`ps`)。照合の時刻が持ち主ごとにずれるので、回数がMCP processの数の掛け算で増えていた。
|
|
17
|
+
Windowsの端末(process 1312個、AitermのMCP process 23個)で、Aitermだけで20秒に78回のPowerShellが起き、
|
|
18
|
+
WMIの側のprocessがCPUを中央2.8コア使っていた。
|
|
19
|
+
- 持ち主は、自分の保存場所へ「生きている印」(`active/<持ち主>/alive`の時刻)を15秒おきに付ける。
|
|
20
|
+
印が60秒以内の持ち主は、OSへ聞かずに生きていると読む。pidを使い回した別のprocessは、この印を書かない。
|
|
21
|
+
- 印の無い持ち主(0.58.3までの版)と、印が古い持ち主だけ、今までどおり開始時刻で照合する。照合する回は全員をまとめ、1本が1分に1回まで。
|
|
22
|
+
- Windowsで1個のprocessの開始時刻を読む時は、WMIを使わず、.NETのprocessの読みを使う(1個1.5ミリ秒。値は今までと同じ)。
|
|
23
|
+
- Windowsの実物で、MCP processを6個・150秒動かした時のPowerShellの起動は、0.58.3が80回、この版は0回。
|
|
24
|
+
- `owner.json`の形、保存場所の名前、配送の記録と返りは変わらない。前の版のMCP processが残っている間、その分の照合は続く。
|
|
25
|
+
- 共通部品`aiterm-steer-delivery`を0.4.5へ上げる。AitermのClaude Codeの親への配送で、受信hookが走るたびに端末の全processの一覧を
|
|
26
|
+
取っていたのが、確かめるprocessだけを読む形になる。
|
|
27
|
+
|
|
10
28
|
## [0.58.3] - 2026-10-12
|
|
11
29
|
|
|
12
30
|
### 修正
|
|
@@ -2524,7 +2542,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2524
2542
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2525
2543
|
provenance.
|
|
2526
2544
|
|
|
2527
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.
|
|
2545
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.4...HEAD
|
|
2546
|
+
[0.58.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.3...v0.58.4
|
|
2528
2547
|
[0.58.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.2...v0.58.3
|
|
2529
2548
|
[0.58.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.1...v0.58.2
|
|
2530
2549
|
[0.58.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.0...v0.58.1
|
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.58.
|
|
212
|
+
**状態:** 開発継続中 · 現行公開版 **v0.58.4** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
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.58.
|
|
230
|
+
**Status:** actively maintained · current public release **v0.58.4** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
|
|
231
231
|
|
|
232
232
|
### Update and rollback
|
|
233
233
|
|
package/dist/parent-delivery.js
CHANGED
|
@@ -64,6 +64,22 @@ async function submitParentAnswer(parent, deliveryId, text) {
|
|
|
64
64
|
const OWNER_IDENTITY_RECHECK_MS = 60_000;
|
|
65
65
|
// owner.jsonの無い保存場所を、作っている途中の持ち主として扱う時間。
|
|
66
66
|
const OWNER_SETUP_GRACE_MS = 60_000;
|
|
67
|
+
// 持ち主が自分の保存場所へ付ける「生きている印」(ADR 0112)。中身は無く、最後に書いた時刻(mtime)だけを使う。
|
|
68
|
+
// 印が新しい持ち主は、OSへ聞かずに生きていると読む。pidを使い回した別のprocessは、この印を書かない。
|
|
69
|
+
// 印の無い持ち主(0.58.3までの版)と、印が古い持ち主だけ、今までどおり開始時刻で照合する。
|
|
70
|
+
// 名前は.jsonで終わらせない(回収は、保存場所の.jsonを配送の記録として引き取る)。
|
|
71
|
+
const OWNER_HEARTBEAT_FILE = "alive";
|
|
72
|
+
const OWNER_HEARTBEAT_MS = 15_000;
|
|
73
|
+
// 持ち主のprocessが同期の処理で塞がる間(最長で十数秒)も印を新しいと読めるよう、書く間隔の4倍にする。
|
|
74
|
+
const OWNER_HEARTBEAT_FRESH_MS = 60_000;
|
|
75
|
+
function ownerHeartbeatAt(directory) {
|
|
76
|
+
try {
|
|
77
|
+
return fs.statSync(path.join(directory, OWNER_HEARTBEAT_FILE)).mtimeMs;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
67
83
|
function processExists(pid) {
|
|
68
84
|
// 0以下はprocess groupを指す。持ち主のpidにはならない。
|
|
69
85
|
if (pid <= 0)
|
|
@@ -129,10 +145,11 @@ export class ParentDeliveryManager {
|
|
|
129
145
|
verifiedOwners = new Map();
|
|
130
146
|
serviceError = null;
|
|
131
147
|
closing = false;
|
|
148
|
+
lastHeartbeat = Number.NEGATIVE_INFINITY;
|
|
132
149
|
constructor(options = {}) {
|
|
133
150
|
this.deps = { observe: observeAnywhere, answer: answerAnywhere, submit: submitParentAnswer,
|
|
134
151
|
verify: verifyParent, processes: readProcessIdentities, exists: processExists,
|
|
135
|
-
now: () => performance.now(), ...options.dependencies };
|
|
152
|
+
now: () => performance.now(), wall: () => Date.now(), ...options.dependencies };
|
|
136
153
|
const stateRoot = ensureStateRoot();
|
|
137
154
|
// 別端末の子の記録はremote-で始まる保存場所へ分け、boundary.remoteを知らない旧版のreaderへ渡さない。
|
|
138
155
|
const prefix = options.remote ? "remote-" : "";
|
|
@@ -414,14 +431,40 @@ export class ParentDeliveryManager {
|
|
|
414
431
|
this.recovering = this.recoverOrphans().finally(() => { this.recovering = null; });
|
|
415
432
|
return this.recovering;
|
|
416
433
|
}
|
|
434
|
+
/** 自分の保存場所の「生きている印」を新しくする。書けない回は次の回へ回す(印が古くなれば、相手は開始時刻で照合する)。 */
|
|
435
|
+
heartbeat() {
|
|
436
|
+
const wall = this.deps.wall();
|
|
437
|
+
if (this.closing || Math.abs(wall - this.lastHeartbeat) < OWNER_HEARTBEAT_MS)
|
|
438
|
+
return;
|
|
439
|
+
const file = path.join(this.ownerDir, OWNER_HEARTBEAT_FILE);
|
|
440
|
+
try {
|
|
441
|
+
const at = new Date(wall);
|
|
442
|
+
try {
|
|
443
|
+
fs.utimesSync(file, at, at);
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
if (error.code !== "ENOENT")
|
|
447
|
+
throw error;
|
|
448
|
+
fs.writeFileSync(file, "", { mode: 0o600 });
|
|
449
|
+
fs.utimesSync(file, at, at);
|
|
450
|
+
}
|
|
451
|
+
this.lastHeartbeat = wall;
|
|
452
|
+
}
|
|
453
|
+
catch { /* 次の回 */ }
|
|
454
|
+
}
|
|
417
455
|
async recoverOrphans() {
|
|
456
|
+
this.heartbeat();
|
|
418
457
|
// 生存を確かめる相手は、closeしていない他の持ち主だけ。居なければOSへ問い合わせない。
|
|
419
|
-
// この処理は5秒おきに全MCP process
|
|
458
|
+
// この処理は5秒おきに全MCP processで回る。processを起こして確かめると、MCP processの数の掛け算で端末を重くする
|
|
459
|
+
// (Windowsで、23個のMCP processがPowerShellを毎秒4回起こしていた。2026-10-12)。
|
|
420
460
|
const directories = fs.readdirSync(this.active, { withFileTypes: true })
|
|
421
461
|
.filter((directory) => directory.isDirectory() && path.join(this.active, directory.name) !== this.ownerDir);
|
|
422
462
|
const now = this.deps.now();
|
|
463
|
+
const wall = this.deps.wall();
|
|
423
464
|
const fate = new Map();
|
|
424
465
|
const unverified = [];
|
|
466
|
+
// 前の照合を覚えている持ち主。誰かを照合する回に、まとめて照合し直す。
|
|
467
|
+
const remembered = [];
|
|
425
468
|
for (const directory of directories) {
|
|
426
469
|
let owner;
|
|
427
470
|
try {
|
|
@@ -437,14 +480,23 @@ export class ParentDeliveryManager {
|
|
|
437
480
|
fate.set(directory.name, "gone");
|
|
438
481
|
continue;
|
|
439
482
|
}
|
|
483
|
+
const beat = ownerHeartbeatAt(path.join(this.active, directory.name));
|
|
484
|
+
if (beat !== null && Math.abs(wall - beat) < OWNER_HEARTBEAT_FRESH_MS) {
|
|
485
|
+
fate.set(directory.name, "alive");
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
440
488
|
const verified = this.verifiedOwners.get(directory.name);
|
|
441
489
|
if (verified && verified.pid === pid && verified.started_identity === owner.started_identity
|
|
442
|
-
&& now - verified.at < OWNER_IDENTITY_RECHECK_MS)
|
|
490
|
+
&& now - verified.at < OWNER_IDENTITY_RECHECK_MS) {
|
|
443
491
|
fate.set(directory.name, verified.alive ? "alive" : "unverified");
|
|
492
|
+
remembered.push({ name: directory.name, pid, started_identity: owner.started_identity });
|
|
493
|
+
}
|
|
444
494
|
else
|
|
445
495
|
unverified.push({ name: directory.name, pid, started_identity: owner.started_identity });
|
|
446
496
|
}
|
|
447
497
|
if (unverified.length > 0) {
|
|
498
|
+
// 覚えた時刻が持ち主ごとにずれると、1分の間に何度もprocessを起こす。照合する回は、全員を1回で照合し直して時刻を揃える。
|
|
499
|
+
unverified.push(...remembered);
|
|
448
500
|
const processes = this.deps.processes([...new Set(unverified.map((entry) => entry.pid))]);
|
|
449
501
|
for (const entry of unverified) {
|
|
450
502
|
const row = processes.find((candidate) => candidate.pid === entry.pid);
|
package/dist/process-runtime.js
CHANGED
|
@@ -101,17 +101,31 @@ export function readRuntimeProcesses() {
|
|
|
101
101
|
};
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
|
+
/** そのpidのprocessが今あるか。OSへ直接聞き、processを起こさない。権限が無くて触れないprocessも「ある」と読む。 */
|
|
105
|
+
function pidExists(pid) {
|
|
106
|
+
if (pid <= 0)
|
|
107
|
+
return false;
|
|
108
|
+
try {
|
|
109
|
+
process.kill(pid, 0);
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return error.code === "EPERM";
|
|
114
|
+
}
|
|
115
|
+
}
|
|
104
116
|
/**
|
|
105
117
|
* 指定したpidの開始時刻だけを引く。readRuntimeProcessesと同じ開始時刻を返し、存在しないpidは結果に含めない。
|
|
106
118
|
* process表にpidはあるが開始時刻を読めない時は、started_identityをnullで返す(pidが無いのとは区別する)。
|
|
107
119
|
* 全processのargvを読む一覧取得は、processの多い端末で重い(定期実行から呼ばない)。
|
|
108
120
|
*/
|
|
109
121
|
export function readProcessIdentities(pids) {
|
|
110
|
-
|
|
122
|
+
if (pids.some(pid => !Number.isSafeInteger(pid) || pid < 0))
|
|
123
|
+
throw new AitermError("process照会のpidが不正です", 2);
|
|
124
|
+
// 居ないpidは、OSへ直接聞いて先に除く。全部居なければ、processを起こさない。
|
|
125
|
+
// macOSのpsは、範囲の外のpidが1つ混ざると、居るprocessの分も出さずに誤りで終わる。
|
|
126
|
+
const wanted = [...new Set(pids)].filter(pidExists);
|
|
111
127
|
if (wanted.length === 0)
|
|
112
128
|
return [];
|
|
113
|
-
if (wanted.some(pid => !Number.isSafeInteger(pid) || pid < 0))
|
|
114
|
-
throw new AitermError("process照会のpidが不正です", 2);
|
|
115
129
|
if (!isWin) {
|
|
116
130
|
const result = spawnSync(POSIX_PS, [...POSIX_IDENTITY_ARGS, wanted.join(",")], {
|
|
117
131
|
encoding: "utf8", env: { ...process.env, LC_ALL: "C" }, timeout: 10000,
|
|
@@ -129,11 +143,25 @@ export function readProcessIdentities(pids) {
|
|
|
129
143
|
}
|
|
130
144
|
// CommandLineの読めないprocess(別の権限のserviceなど)も返す。pidが使い回された先を「別のprocess」と確かめるのに要る。
|
|
131
145
|
// 開始時刻を読めない行も、pidがprocess表にある印として返す。
|
|
146
|
+
// WMI(Win32_Process)は使わない。絞っても1回に0.1〜0.3秒かかり、WMIの側のprocess(wmiprvse)のCPUを使う
|
|
147
|
+
// (process 1300個のWindowsで測った 2026-10-12。ADR 0112)。.NETのprocessの読み(OpenProcessとGetProcessTimes)は1個1.5ミリ秒。
|
|
148
|
+
// 開始時刻は同じ値になる(WMIはマイクロ秒で切り捨てた値を返し、どちらも1ミリ秒で切り捨てて使う。888個のprocessで食い違いは0)。
|
|
149
|
+
// 権限が無くて読めないprocessだけ、WMIへ聞き直す。
|
|
132
150
|
const script = [
|
|
133
151
|
...WINDOWS_PROBE_HEADER,
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
"}
|
|
152
|
+
"$rows=@()",
|
|
153
|
+
`foreach ($id in @(${wanted.join(",")})) {`,
|
|
154
|
+
" try { $p=[Diagnostics.Process]::GetProcessById($id) } catch [ArgumentException] { continue }",
|
|
155
|
+
" $started=$null",
|
|
156
|
+
" try { $started=$p.StartTime.ToUniversalTime().ToString('o') }",
|
|
157
|
+
" catch [InvalidOperationException] { continue }",
|
|
158
|
+
" catch {",
|
|
159
|
+
" $w=Get-CimInstance Win32_Process -Filter \"ProcessId=$id\" -Property CreationDate",
|
|
160
|
+
" if ($null -eq $w) { continue }",
|
|
161
|
+
" if ($null -ne $w.CreationDate) { $started=$w.CreationDate.ToUniversalTime().ToString('o') }",
|
|
162
|
+
" }",
|
|
163
|
+
" $rows += [ordered]@{ pid=[int]$id; started_identity=$started }",
|
|
164
|
+
"}",
|
|
137
165
|
"ConvertTo-Json -Compress -InputObject $rows",
|
|
138
166
|
].join("\n");
|
|
139
167
|
const result = spawnSync(resolveWindowsPowerShell7(), [...WINDOWS_PROBE_ARGS, Buffer.from(script, "utf16le").toString("base64")], {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.58.
|
|
3
|
+
"version": "0.58.4",
|
|
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": [
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
75
|
-
"aiterm-steer-delivery": "^0.4.
|
|
75
|
+
"aiterm-steer-delivery": "^0.4.5",
|
|
76
76
|
"ws": "8.21.3",
|
|
77
77
|
"zod": "^4.4.3"
|
|
78
78
|
},
|