aiterm-mcp 0.51.0 → 0.51.2
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 +28 -1
- package/README.ja.md +3 -2
- package/README.md +4 -2
- package/dist/core.js +56 -8
- package/dist/parent-delivery.js +101 -18
- package/dist/process-runtime.js +31 -9
- package/docs/DESIGN.md +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.51.2] - 2026-10-04
|
|
11
|
+
|
|
12
|
+
### 修正
|
|
13
|
+
|
|
14
|
+
- 親配送の回収が、終了した持ち主の保存場所を残し続け、使い回されたpidを5秒おきにOSへ問い合わせていた(ADR 0084)。
|
|
15
|
+
Windowsでは問い合わせがPowerShellの起動で、MCP process 1つが約2.5秒に1回起こし続けていた(実物で、長く動く4つが85秒に約260回。
|
|
16
|
+
保存場所は約1,620)。macOSでは`ps`。記録を引き取り終えた保存場所は、自分で閉じた持ち主とpidの無い持ち主のものを消す。
|
|
17
|
+
pidが別のprocessへ使い回された持ち主は`owner.json`を`closed`へ書き換え、どの版の回収にも照合を繰り返させない。
|
|
18
|
+
開始時刻を照合できない持ち主は60秒に1回だけ問い合わせる。この版より前のMCP processが同じ置き場で生きている間は、保存場所を消さない。
|
|
19
|
+
`owner.json`に`removal_safe`と`closed_reason`が増える。前の版へ戻しても、そのまま動く(前の版はこの2つを読まない)。
|
|
20
|
+
- Windowsの開始時刻の照会が、CommandLineを読めないprocessを結果から落としていた。pidがserviceへ使い回された時に、
|
|
21
|
+
別のprocessと確かめられなかった。
|
|
22
|
+
- `pty_observe`の`activity.post_startup_process_count`で、Aitermがprocessの一覧を引くために起こすprocess
|
|
23
|
+
(`ps`、WindowsのPowerShellとそのconsole host)を数えない。Windowsで、何もしていない席の数が0と2を行き来していた。
|
|
24
|
+
|
|
25
|
+
## [0.51.1] - 2026-10-04
|
|
26
|
+
|
|
27
|
+
### 修正
|
|
28
|
+
|
|
29
|
+
- `pty_observe`の`activity.post_startup_process_count`で、harnessが立て直した起動時のprocessを数えない(ADR 0083)。
|
|
30
|
+
Cursorは起動時にstdioのMCPを立て、最初のつなぎが済むと止めて立て直す。つながるのが遅いMCPがあると、立て直しが
|
|
31
|
+
起動完了の後になり、何もしていない席で数が残っていた(実物で、直結3・中継ごし4)。起動時のprocessが終了していて、
|
|
32
|
+
同じ親の下に同じargvで立っているprocessは、その立て直しとして扱う。その下で動くprocessは今までどおり数える。
|
|
33
|
+
この版より前のAitermが起動したagentは、今までどおり数える。
|
|
34
|
+
|
|
10
35
|
## [0.51.0] - 2026-10-04
|
|
11
36
|
|
|
12
37
|
### 追加
|
|
@@ -2050,7 +2075,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2050
2075
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2051
2076
|
provenance.
|
|
2052
2077
|
|
|
2053
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.
|
|
2078
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.2...HEAD
|
|
2079
|
+
[0.51.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.1...v0.51.2
|
|
2080
|
+
[0.51.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.0...v0.51.1
|
|
2054
2081
|
[0.51.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.50.0...v0.51.0
|
|
2055
2082
|
[0.50.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.49.0...v0.50.0
|
|
2056
2083
|
[0.49.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.48.0...v0.49.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.51.
|
|
212
|
+
**状態:** 開発継続中 · 現行公開版 **v0.51.2** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -519,7 +519,8 @@ sessionへ登録する名前の配列で、`pty_list({ env_keys: ["JOB_OWNER"] }
|
|
|
519
519
|
agent sessionを閉じると失うものは、次の2項目で分かる。`activity.post_startup_process_count`は、`agent_launch`の起動準備が
|
|
520
520
|
完了した時点(初手を送る前)に居なかったprocessの数。起動直後の1分に始めた裏の作業も数える。数えないものは2つ。Codex自身の
|
|
521
521
|
補助process(`codex-code-mode-host`)と、中継`mcp-lazy`の直接の子(初めて使う時に起こすMCPの本体と、先行起動の判定)。
|
|
522
|
-
その下で動くprocess
|
|
522
|
+
その下で動くprocessは数える。harnessが立て直した起動時のprocess(終了した起動時のprocessと同じ親の下に、同じargvで立ったもの。
|
|
523
|
+
つなぎ直したMCPなど)も数えない。Aitermがprocessの一覧を引くために起こすprocess(`ps`、WindowsではPowerShellとそのconsole host)も数えない。`pending_child_deliveries`は、そのsessionが親として待っていて、まだ届け終えていない
|
|
523
524
|
子の結果の数。呼び出した側が誰でも付く。どちらも、分からない時はnull(通常PTY、harnessのprocessを特定できない時。
|
|
524
525
|
processの数は0.48.0以前が起動したagentも)。別端末が旧版の時は項目ごと無い。nullと項目なしは「分からない」で、0ではない。
|
|
525
526
|
|
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.51.
|
|
226
|
+
**Status:** actively maintained · current public release **v0.51.2** · 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
|
|
|
@@ -552,7 +552,9 @@ Two fields tell a caller what closing an agent session would lose. `activity.pos
|
|
|
552
552
|
processes in the session that did not exist when `agent_launch` finished startup (before the first prompt), so background work
|
|
553
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
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.
|
|
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, and neither are the processes Aiterm itself
|
|
557
|
+
starts to read the process table (`ps`, or PowerShell and its console host on Windows). `pending_child_deliveries`
|
|
556
558
|
is the number of sub-agent results this session is still waiting for as a parent, whoever the caller is. Both are null when
|
|
557
559
|
Aiterm cannot tell (ordinary terminals, agents launched by 0.48.0 or earlier for the process count, or an unresolved harness
|
|
558
560
|
process), and may be absent when a remote host runs an older Aiterm. Treat null or absent as unknown, not as zero.
|
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, lazyRelayProcess } from "./process-runtime.js";
|
|
19
|
+
import { readRuntimeProcesses, processSubtree, parentProcess, processIdentity, backgroundProcesses, lazyRelayProcess, aitermProcessProbe, windowsConsoleHost } 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,17 +1077,61 @@ export function selectHarnessProcesses(meta, rows, subtree) {
|
|
|
1077
1077
|
});
|
|
1078
1078
|
return roots;
|
|
1079
1079
|
}
|
|
1080
|
-
|
|
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・中継・Aitermが自分のために立てるprocessは、利用者の作業ではないので数えない。
|
|
1081
1094
|
// その下で動くprocessは数える。
|
|
1082
|
-
|
|
1095
|
+
// harnessは起動の足場(MCP、language server)を自分で立て直す。控えた後に立て直されると、控えに無いprocessとして残る。
|
|
1096
|
+
// 控えの物が終了していて、同じ親の下に同じargvで立ったprocessは、その立て直しとして数えない。
|
|
1097
|
+
// 親も立て直されている時は、立て直された親の下を見る。終了した控え1つにつき、数えないのは1つまで。
|
|
1098
|
+
export function postStartupProcessCount(kind, activityRows, rows, startup, shapes = []) {
|
|
1083
1099
|
const byPid = new Map(rows.map(row => [row.pid, row]));
|
|
1084
1100
|
const relayChild = (row) => {
|
|
1085
1101
|
const parent = parentProcess(row, byPid);
|
|
1086
1102
|
return parent !== undefined && lazyRelayProcess(parent.command);
|
|
1087
1103
|
};
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1104
|
+
// 席の中のAitermも、配送の持ち主の生存確認でprocess表を引く。その問い合わせと、付いて立つconsole hostは数えない。
|
|
1105
|
+
// 親の問い合わせが先に終わって残ったconsole hostも同じ扱いにする(console hostは最後のconsole processと一緒に消える)。
|
|
1106
|
+
const ownProbe = (row) => {
|
|
1107
|
+
if (aitermProcessProbe(row.command))
|
|
1108
|
+
return true;
|
|
1109
|
+
const parent = parentProcess(row, byPid);
|
|
1110
|
+
return windowsConsoleHost(row) && (parent === undefined || aitermProcessProbe(parent.command));
|
|
1111
|
+
};
|
|
1112
|
+
const present = new Set(activityRows.map(processKey));
|
|
1113
|
+
const added = new Map(activityRows
|
|
1114
|
+
.filter(row => !startup.has(processKey(row)) && !(kind === "codex" && codexHelperProcess(row.command)) && !relayChild(row) && !ownProbe(row))
|
|
1115
|
+
.map(row => [processKey(row), row]));
|
|
1116
|
+
const gone = shapes.filter(shape => startup.has(shape.identity) && !present.has(shape.identity));
|
|
1117
|
+
const replacedBy = new Map();
|
|
1118
|
+
for (let changed = gone.length > 0; changed;) {
|
|
1119
|
+
changed = false;
|
|
1120
|
+
for (const [key, row] of added) {
|
|
1121
|
+
const parent = parentProcess(row, byPid);
|
|
1122
|
+
if (!parent)
|
|
1123
|
+
continue;
|
|
1124
|
+
const parentKey = processKey(parent);
|
|
1125
|
+
const original = gone.find(shape => !replacedBy.has(shape.identity) && shape.argv_digest === row.argv_digest
|
|
1126
|
+
&& shape.parent !== null && (shape.parent === parentKey || replacedBy.get(shape.parent) === parentKey));
|
|
1127
|
+
if (!original)
|
|
1128
|
+
continue;
|
|
1129
|
+
replacedBy.set(original.identity, key);
|
|
1130
|
+
added.delete(key);
|
|
1131
|
+
changed = true;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
return added.size;
|
|
1091
1135
|
}
|
|
1092
1136
|
export function observeSession(name, cursor) {
|
|
1093
1137
|
assertSessionName(name);
|
|
@@ -1190,7 +1234,8 @@ export function observeSession(name, cursor) {
|
|
|
1190
1234
|
background_cpu_seconds: Object.values(backgroundCpu).reduce((sum, cpu) => sum + cpu, 0),
|
|
1191
1235
|
background_cpu_delta_seconds: comparable ? Object.entries(backgroundCpu).reduce((sum, [identity, cpu]) => sum + cpu - (previous.background_processes[identity] ?? 0), 0) : null,
|
|
1192
1236
|
background_cpu_delta_complete: comparable ? Object.keys(previous.background_processes).every(identity => identity in backgroundCpu) : null,
|
|
1193
|
-
post_startup_process_count: startup
|
|
1237
|
+
post_startup_process_count: startup
|
|
1238
|
+
? postStartupProcessCount(meta.kind, activityRows, rows, startup, meta.startup_process_shapes) : null,
|
|
1194
1239
|
};
|
|
1195
1240
|
return result;
|
|
1196
1241
|
}
|
|
@@ -2013,8 +2058,10 @@ function loadAgentMetadata(name) {
|
|
|
2013
2058
|
initial_prompt_delivery: m.initial_prompt_delivery, initial_prompt_cursor: m.initial_prompt_cursor,
|
|
2014
2059
|
};
|
|
2015
2060
|
const executableFields = m.agent_executable === undefined ? {} : { agent_executable: m.agent_executable };
|
|
2061
|
+
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")
|
|
2062
|
+
? { startup_process_shapes: m.startup_process_shapes } : {};
|
|
2016
2063
|
const startupFields = Array.isArray(m.startup_processes) && m.startup_processes.every((identity) => typeof identity === "string")
|
|
2017
|
-
? { startup_processes: m.startup_processes } : {};
|
|
2064
|
+
? { startup_processes: m.startup_processes, ...startupShapes } : {};
|
|
2018
2065
|
if (m.kind === "claude") {
|
|
2019
2066
|
const expectedSettings = agentManagedClaudeSettingsPath(name, m.launch_id);
|
|
2020
2067
|
const expectedResult = agentClaudeResultPath(name, m.launch_id);
|
|
@@ -3235,6 +3282,7 @@ async function prepareAgentInput(name, meta, options) {
|
|
|
3235
3282
|
// ここまでに居るprocessは起動時の足場(harness・MCP・起動時hook)。初手を送る前に控える。
|
|
3236
3283
|
if (live.activity.cursor !== null) {
|
|
3237
3284
|
meta.startup_processes = Object.keys(decodeActivityCursor(live.activity.cursor, name).processes);
|
|
3285
|
+
meta.startup_process_shapes = startupProcessShapes(meta.startup_processes, readRuntimeProcesses());
|
|
3238
3286
|
writeAgentMetadata(meta);
|
|
3239
3287
|
}
|
|
3240
3288
|
return { status: "ready", reason: "composer_ready" };
|
package/dist/parent-delivery.js
CHANGED
|
@@ -62,6 +62,8 @@ async function submitParentAnswer(parent, deliveryId, text) {
|
|
|
62
62
|
// 5秒おきの確認はpidの存在だけを見て、開始時刻は初めて見た持ち主とこの間隔でだけ照合する。
|
|
63
63
|
// 持ち主が終了した直後に同じpidが再利用された時だけ、回収が最長でこの間隔だけ遅れる。生きている持ち主の記録は引き取らない。
|
|
64
64
|
const OWNER_IDENTITY_RECHECK_MS = 60_000;
|
|
65
|
+
// owner.jsonの無い保存場所を、作っている途中の持ち主として扱う時間。
|
|
66
|
+
const OWNER_SETUP_GRACE_MS = 60_000;
|
|
65
67
|
function processExists(pid) {
|
|
66
68
|
// 0以下はprocess groupを指す。持ち主のpidにはならない。
|
|
67
69
|
if (pid <= 0)
|
|
@@ -74,6 +76,14 @@ function processExists(pid) {
|
|
|
74
76
|
return error.code === "EPERM";
|
|
75
77
|
}
|
|
76
78
|
}
|
|
79
|
+
function ownerSetupInProgress(directory) {
|
|
80
|
+
try {
|
|
81
|
+
return Date.now() - fs.statSync(directory).mtimeMs < OWNER_SETUP_GRACE_MS;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
77
87
|
function readRecord(file) {
|
|
78
88
|
try {
|
|
79
89
|
const record = recordSchema.parse(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
@@ -115,7 +125,7 @@ export class ParentDeliveryManager {
|
|
|
115
125
|
registering = new Map();
|
|
116
126
|
timer;
|
|
117
127
|
recovering = null;
|
|
118
|
-
//
|
|
128
|
+
// 他の持ち主の開始時刻を最後に照合した時刻と結果(持ち主の保存場所ごと)。照合できなかった結果も覚える。
|
|
119
129
|
verifiedOwners = new Map();
|
|
120
130
|
serviceError = null;
|
|
121
131
|
closing = false;
|
|
@@ -141,7 +151,7 @@ export class ParentDeliveryManager {
|
|
|
141
151
|
const ownProcess = this.deps.processes([process.pid]).find((entry) => entry.pid === process.pid);
|
|
142
152
|
if (!ownProcess)
|
|
143
153
|
throw new AitermError("PARENT_DELIVERY_OWNER_UNKNOWN: 配送processを識別できません", 2);
|
|
144
|
-
this.owner = { pid: process.pid, started_identity: ownProcess.started_identity, closed: false };
|
|
154
|
+
this.owner = { pid: process.pid, started_identity: ownProcess.started_identity, closed: false, removal_safe: true };
|
|
145
155
|
this.ownerDir = path.join(this.active, `${deliveryOwnerPrefix(this.owner)}${randomUUID()}`);
|
|
146
156
|
fs.mkdirSync(this.ownerDir, { recursive: true, mode: 0o700 });
|
|
147
157
|
fs.mkdirSync(this.results, { recursive: true, mode: 0o700 });
|
|
@@ -361,7 +371,17 @@ export class ParentDeliveryManager {
|
|
|
361
371
|
if (!owner.isDirectory())
|
|
362
372
|
continue;
|
|
363
373
|
const directory = path.join(active, owner.name);
|
|
364
|
-
|
|
374
|
+
let names;
|
|
375
|
+
// 一覧を取った後に、回収が終了した持ち主の保存場所を消す事がある。
|
|
376
|
+
try {
|
|
377
|
+
names = fs.readdirSync(directory);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
if (error.code === "ENOENT")
|
|
381
|
+
continue;
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
files.push(...names.filter((name) => name !== "owner.json" && name.endsWith(".json")).map((name) => path.join(directory, name)));
|
|
365
385
|
}
|
|
366
386
|
}
|
|
367
387
|
return files;
|
|
@@ -399,7 +419,7 @@ export class ParentDeliveryManager {
|
|
|
399
419
|
const directories = fs.readdirSync(this.active, { withFileTypes: true })
|
|
400
420
|
.filter((directory) => directory.isDirectory() && path.join(this.active, directory.name) !== this.ownerDir);
|
|
401
421
|
const now = this.deps.now();
|
|
402
|
-
const
|
|
422
|
+
const fate = new Map();
|
|
403
423
|
const unverified = [];
|
|
404
424
|
for (const directory of directories) {
|
|
405
425
|
let owner;
|
|
@@ -413,46 +433,77 @@ export class ParentDeliveryManager {
|
|
|
413
433
|
continue;
|
|
414
434
|
const pid = owner.pid;
|
|
415
435
|
if (!this.deps.exists(pid)) {
|
|
416
|
-
|
|
436
|
+
fate.set(directory.name, "gone");
|
|
417
437
|
continue;
|
|
418
438
|
}
|
|
419
439
|
const verified = this.verifiedOwners.get(directory.name);
|
|
420
440
|
if (verified && verified.pid === pid && verified.started_identity === owner.started_identity
|
|
421
441
|
&& now - verified.at < OWNER_IDENTITY_RECHECK_MS)
|
|
422
|
-
|
|
442
|
+
fate.set(directory.name, verified.alive ? "alive" : "unverified");
|
|
423
443
|
else
|
|
424
444
|
unverified.push({ name: directory.name, pid, started_identity: owner.started_identity });
|
|
425
445
|
}
|
|
426
446
|
if (unverified.length > 0) {
|
|
427
447
|
const processes = this.deps.processes([...new Set(unverified.map((entry) => entry.pid))]);
|
|
428
448
|
for (const entry of unverified) {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
449
|
+
const row = processes.find((candidate) => candidate.pid === entry.pid);
|
|
450
|
+
const state = !row ? "unverified" : row.started_identity === entry.started_identity ? "alive" : "reused";
|
|
451
|
+
fate.set(entry.name, state);
|
|
452
|
+
// 別のprocessと分かった持ち主は下で閉じるので、照合の結果を覚えない。
|
|
453
|
+
if (state !== "reused")
|
|
454
|
+
this.verifiedOwners.set(entry.name, { pid: entry.pid, started_identity: entry.started_identity, at: now, alive: state === "alive" });
|
|
433
455
|
}
|
|
434
456
|
}
|
|
435
|
-
for (const name of this.verifiedOwners.keys())
|
|
436
|
-
|
|
457
|
+
for (const name of this.verifiedOwners.keys()) {
|
|
458
|
+
const state = fate.get(name);
|
|
459
|
+
if (state !== "alive" && state !== "unverified")
|
|
437
460
|
this.verifiedOwners.delete(name);
|
|
461
|
+
}
|
|
462
|
+
// 記録を引き取り終えた、終了した持ち主の保存場所。残すと、回収のたびに全部を読み直す。
|
|
463
|
+
const finished = [];
|
|
464
|
+
// owner.jsonの無い、古い保存場所。
|
|
465
|
+
const abandoned = [];
|
|
466
|
+
// 旧版は、読んでいる最中に他の持ち主の保存場所が消えると回収が止まる。印の無い持ち主が生きている間は消さない。
|
|
467
|
+
let removalBlocked = false;
|
|
438
468
|
for (const directory of directories) {
|
|
439
469
|
const oldDir = path.join(this.active, directory.name);
|
|
470
|
+
const ownerFile = path.join(oldDir, "owner.json");
|
|
440
471
|
let owner;
|
|
441
472
|
try {
|
|
442
|
-
owner = JSON.parse(fs.readFileSync(
|
|
473
|
+
owner = JSON.parse(fs.readFileSync(ownerFile, "utf8"));
|
|
443
474
|
}
|
|
444
475
|
catch (error) {
|
|
445
|
-
if (error.code
|
|
446
|
-
|
|
447
|
-
|
|
476
|
+
if (error.code !== "ENOENT")
|
|
477
|
+
throw new AitermError("PARENT_DELIVERY_OWNER_INVALID: 配送所有者の記録を読めません", 2);
|
|
478
|
+
// 作っている途中の持ち主は版が分からない。
|
|
479
|
+
if (ownerSetupInProgress(oldDir))
|
|
480
|
+
removalBlocked = true;
|
|
481
|
+
else
|
|
482
|
+
abandoned.push(oldDir);
|
|
483
|
+
continue;
|
|
448
484
|
}
|
|
449
485
|
if (!Number.isSafeInteger(owner.pid) || typeof owner.started_identity !== "string" || typeof owner.closed !== "boolean") {
|
|
450
486
|
throw new AitermError("PARENT_DELIVERY_OWNER_INVALID: 配送所有者の形式が不正です", 2);
|
|
451
487
|
}
|
|
488
|
+
const state = fate.get(directory.name);
|
|
452
489
|
// 生死を確かめた後に現れた持ち主は、次の回で扱う。生きている持ち主の記録は引き取らない。
|
|
453
|
-
if (!owner.closed &&
|
|
490
|
+
if (!owner.closed && (state === undefined || state === "alive")) {
|
|
491
|
+
if (owner.removal_safe !== true)
|
|
492
|
+
removalBlocked = true;
|
|
454
493
|
continue;
|
|
455
|
-
|
|
494
|
+
}
|
|
495
|
+
if (!owner.closed && state === "unverified" && owner.removal_safe !== true)
|
|
496
|
+
removalBlocked = true;
|
|
497
|
+
let names;
|
|
498
|
+
try {
|
|
499
|
+
names = fs.readdirSync(oldDir);
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
if (error.code === "ENOENT")
|
|
503
|
+
continue;
|
|
504
|
+
throw error;
|
|
505
|
+
}
|
|
506
|
+
for (const name of names.filter((name) => name !== "owner.json" && name.endsWith(".json"))) {
|
|
456
507
|
const oldFile = path.join(oldDir, name);
|
|
457
508
|
const file = path.join(this.ownerDir, name);
|
|
458
509
|
// 元のpathが消えるrenameを使うため、同じ旧recordを二つのprocessで回収できない。
|
|
@@ -490,9 +541,41 @@ export class ParentDeliveryManager {
|
|
|
490
541
|
else
|
|
491
542
|
this.finish(job);
|
|
492
543
|
}
|
|
544
|
+
if (!owner.closed && state === "reused") {
|
|
545
|
+
// 同じpidと開始時刻の組は二度と現れない。閉じた事にして、どの版の回収にも照合を繰り返させない。
|
|
546
|
+
// 書けなかった時は次の回でもう一度照合する。配送には影響しない。
|
|
547
|
+
try {
|
|
548
|
+
writeJson0600(ownerFile, { ...owner, closed: true, closed_reason: "pid_reused" });
|
|
549
|
+
}
|
|
550
|
+
catch { /* 次の回 */ }
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
// 持ち主が自分で閉じた保存場所と、pidがもう無い持ち主の保存場所だけを消す。
|
|
554
|
+
// 開始時刻の違いで閉じた保存場所は、そのpidが空くまで残す。
|
|
555
|
+
const ended = owner.closed ? owner.closed_reason === undefined || !this.deps.exists(owner.pid) : state === "gone";
|
|
556
|
+
if (ended)
|
|
557
|
+
finished.push(oldDir);
|
|
558
|
+
}
|
|
559
|
+
if (removalBlocked)
|
|
560
|
+
return;
|
|
561
|
+
// 片付けの失敗は配送に影響しない。残った保存場所は次の回でもう一度消す。
|
|
562
|
+
for (const directory of finished) {
|
|
563
|
+
try {
|
|
564
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
565
|
+
}
|
|
566
|
+
catch { /* 次の回 */ }
|
|
567
|
+
}
|
|
568
|
+
for (const directory of abandoned) {
|
|
569
|
+
try {
|
|
570
|
+
fs.rmdirSync(directory);
|
|
571
|
+
}
|
|
572
|
+
catch { /* 空でない、または既に無い */ }
|
|
493
573
|
}
|
|
494
574
|
}
|
|
495
575
|
async close() {
|
|
576
|
+
// 閉じた後の保存場所は、記録を引き取った他の持ち主が消す。二度目のcloseは何も書かない。
|
|
577
|
+
if (this.owner.closed)
|
|
578
|
+
return;
|
|
496
579
|
this.closing = true;
|
|
497
580
|
clearInterval(this.timer);
|
|
498
581
|
await this.recovering;
|
package/dist/process-runtime.js
CHANGED
|
@@ -32,9 +32,32 @@ export function parsePosixProcessTable(text) {
|
|
|
32
32
|
};
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
// AitermがOSのprocess表を引く時のコマンドの形。席のprocessを数える側が、これを利用者の作業と取り違えないために使う。
|
|
36
|
+
const POSIX_PS = "/bin/ps";
|
|
37
|
+
const POSIX_TABLE_ARGS = ["-axww", "-o", "pid=,ppid=,pgid=,stat=,lstart=,time=,command="];
|
|
38
|
+
const POSIX_IDENTITY_ARGS = ["-o", "pid=,lstart=", "-p"];
|
|
39
|
+
const WINDOWS_PROBE_HEADER = ["$ErrorActionPreference='Stop'", "[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)"];
|
|
40
|
+
const WINDOWS_PROBE_ARGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"];
|
|
41
|
+
// 先頭2行だけのbase64。3 byteの区切りで切り、後ろの行に左右されない前方一致にする。
|
|
42
|
+
const WINDOWS_PROBE_PREFIX = (() => {
|
|
43
|
+
const bytes = Buffer.from(WINDOWS_PROBE_HEADER.join("\n") + "\n", "utf16le");
|
|
44
|
+
return bytes.subarray(0, bytes.length - bytes.length % 3).toString("base64");
|
|
45
|
+
})();
|
|
46
|
+
/** Aitermがprocess表を引くために起こしたprocessか。引く側のAitermは、数えられる席の中のMCP processでもある。 */
|
|
47
|
+
export function aitermProcessProbe(command) {
|
|
48
|
+
if (command.includes(` ${WINDOWS_PROBE_ARGS.join(" ")} ${WINDOWS_PROBE_PREFIX}`))
|
|
49
|
+
return true;
|
|
50
|
+
return command === `${POSIX_PS} ${POSIX_TABLE_ARGS.join(" ")}` || command.startsWith(`${POSIX_PS} ${POSIX_IDENTITY_ARGS.join(" ")} `);
|
|
51
|
+
}
|
|
52
|
+
/** Windowsのconsole host。console processの起動に付いて立ち、最後のconsole processが終わると消える。 */
|
|
53
|
+
export function windowsConsoleHost(row) {
|
|
54
|
+
const first = /^(?:"([^"]+)"|(\S+))/.exec(row.command);
|
|
55
|
+
const name = (row.executable || first?.[1] || first?.[2] || "").replace(/\\/g, "/");
|
|
56
|
+
return path.posix.basename(name).toLowerCase() === "conhost.exe";
|
|
57
|
+
}
|
|
35
58
|
export function readRuntimeProcesses() {
|
|
36
59
|
if (!isWin) {
|
|
37
|
-
const result = spawnSync(
|
|
60
|
+
const result = spawnSync(POSIX_PS, POSIX_TABLE_ARGS, {
|
|
38
61
|
encoding: "utf8", env: { ...process.env, LC_ALL: "C" }, timeout: 10000, maxBuffer: 16 * 1024 * 1024,
|
|
39
62
|
});
|
|
40
63
|
if (result.error || result.status !== 0)
|
|
@@ -42,13 +65,12 @@ export function readRuntimeProcesses() {
|
|
|
42
65
|
return parsePosixProcessTable(result.stdout);
|
|
43
66
|
}
|
|
44
67
|
const script = [
|
|
45
|
-
|
|
46
|
-
"[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)",
|
|
68
|
+
...WINDOWS_PROBE_HEADER,
|
|
47
69
|
"@(Get-CimInstance Win32_Process | Where-Object { $null -ne $_.CreationDate -and $null -ne $_.CommandLine } | ForEach-Object {",
|
|
48
70
|
"[ordered]@{ pid=[int]$_.ProcessId; parent_pid=[int]$_.ParentProcessId; executable=[string]$_.ExecutablePath; started_identity=$_.CreationDate.ToUniversalTime().ToString('o'); command=$_.CommandLine; cpu_seconds=([double]$_.KernelModeTime+[double]$_.UserModeTime)/10000000 }",
|
|
49
71
|
"}) | ConvertTo-Json -Compress",
|
|
50
72
|
].join("\n");
|
|
51
|
-
const result = spawnSync(resolveWindowsPowerShell7(), [
|
|
73
|
+
const result = spawnSync(resolveWindowsPowerShell7(), [...WINDOWS_PROBE_ARGS, Buffer.from(script, "utf16le").toString("base64")], {
|
|
52
74
|
encoding: "utf8", timeout: 15000, maxBuffer: 16 * 1024 * 1024, windowsHide: true,
|
|
53
75
|
});
|
|
54
76
|
if (result.error || result.status !== 0)
|
|
@@ -87,7 +109,7 @@ export function readProcessIdentities(pids) {
|
|
|
87
109
|
if (wanted.some(pid => !Number.isSafeInteger(pid) || pid < 0))
|
|
88
110
|
throw new AitermError("process照会のpidが不正です", 2);
|
|
89
111
|
if (!isWin) {
|
|
90
|
-
const result = spawnSync(
|
|
112
|
+
const result = spawnSync(POSIX_PS, [...POSIX_IDENTITY_ARGS, wanted.join(",")], {
|
|
91
113
|
encoding: "utf8", env: { ...process.env, LC_ALL: "C" }, timeout: 10000,
|
|
92
114
|
});
|
|
93
115
|
// 該当するprocessが一つも無い時、psは何も出さずstatus 1で終わる。
|
|
@@ -101,15 +123,15 @@ export function readProcessIdentities(pids) {
|
|
|
101
123
|
return { pid: Number(match[1]), started_identity: match[2].trim() };
|
|
102
124
|
});
|
|
103
125
|
}
|
|
126
|
+
// CommandLineの読めないprocess(別の権限のserviceなど)も返す。pidが使い回された先を「別のprocess」と確かめるのに要る。
|
|
104
127
|
const script = [
|
|
105
|
-
|
|
106
|
-
"
|
|
107
|
-
`$rows=@(Get-CimInstance Win32_Process -Filter "${wanted.map(pid => `ProcessId=${pid}`).join(" OR ")}" | Where-Object { $null -ne $_.CreationDate -and $null -ne $_.CommandLine } | ForEach-Object {`,
|
|
128
|
+
...WINDOWS_PROBE_HEADER,
|
|
129
|
+
`$rows=@(Get-CimInstance Win32_Process -Filter "${wanted.map(pid => `ProcessId=${pid}`).join(" OR ")}" | Where-Object { $null -ne $_.CreationDate } | ForEach-Object {`,
|
|
108
130
|
"[ordered]@{ pid=[int]$_.ProcessId; started_identity=$_.CreationDate.ToUniversalTime().ToString('o') }",
|
|
109
131
|
"})",
|
|
110
132
|
"ConvertTo-Json -Compress -InputObject $rows",
|
|
111
133
|
].join("\n");
|
|
112
|
-
const result = spawnSync(resolveWindowsPowerShell7(), [
|
|
134
|
+
const result = spawnSync(resolveWindowsPowerShell7(), [...WINDOWS_PROBE_ARGS, Buffer.from(script, "utf16le").toString("base64")], {
|
|
113
135
|
encoding: "utf8", timeout: 15000, windowsHide: true,
|
|
114
136
|
});
|
|
115
137
|
if (result.error || result.status !== 0)
|
package/docs/DESIGN.md
CHANGED
|
@@ -55,6 +55,8 @@ POSIXの停止状態はOSのprocess表から取得し、SIGSTOP中は残画面
|
|
|
55
55
|
sessionを閉じると失うものは数で返す(ADR 0080)。`activity.post_startup_process_count`は起動完了の時点に居なかったprocessの数、
|
|
56
56
|
`pending_child_deliveries`はそのsessionが親として待つ未配送の数。数えられない時はnullで、0と区別する。
|
|
57
57
|
harnessと中継が自分のために立てるprocessは数えない(Codexの`codex-code-mode-host`、中継`mcp-lazy`の直接の子。ADR 0082)。その下は数える。
|
|
58
|
+
harnessが同じ親の下に同じargvで立て直した起動時のprocessも数えない(ADR 0083)。
|
|
59
|
+
Aitermがprocess表を引くために起こしたprocess(`ps`、WindowsのPowerShellと付いて立つconsole host)も数えない(ADR 0084)。
|
|
58
60
|
中継の後ろで眠っている本体を配送の引き取りのために起こすかは、`aiterm-delivery-wake`が返す(ADR 0082)。
|
|
59
61
|
Claude Codeの入力待ちは、起動時の見出しが取得範囲から流れ出た後も、入力欄の形(`❯`行の上下の罫線)で読む。
|
|
60
62
|
画面本文とargv本文は返さず、活動cursorには画面digestとprocess別CPUだけを持たせる。
|
|
@@ -150,6 +152,9 @@ Codexの配送記録と本文はAiterm stateの`parent-deliveries`へ保存す
|
|
|
150
152
|
受信口が明示拒否した場合は`failed`、子の異常終了はそのoutcomeを配送する。
|
|
151
153
|
生存の確認は5秒おきにpidの存在だけをOSへ聞き、開始識別子の照合は初めて見たownerと60秒に1回だけ行う。
|
|
152
154
|
全processの一覧は取らない。終了直後に同じpidが再利用された時だけ、引き継ぎが最長60秒遅れる(ADR 0078)。
|
|
155
|
+
pidが別のprocessへ使い回されたownerは、回収側が`owner.json`を`closed`へ書き換え、照合を繰り返さない。照合できないownerは60秒に1回だけ問い合わせる。
|
|
156
|
+
記録を引き継ぎ終えた保存場所は、自分で閉じたownerとpidの無いownerのものを消す。保存場所が消えても読める印(`removal_safe`)の無いownerが
|
|
157
|
+
同じ置き場で生きている間は消さない(ADR 0084)。
|
|
153
158
|
このstateは既存のPTY/harness stateと独立し、旧版は配送を再開しない。
|
|
154
159
|
|
|
155
160
|
### Claude Code親への自動配送
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.2",
|
|
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": [
|