aiterm-mcp 0.47.0 → 0.47.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 +9 -1
- package/README.ja.md +3 -1
- package/README.md +3 -1
- package/dist/core.js +46 -5
- package/dist/parent-delivery.js +65 -9
- package/dist/process-runtime.js +54 -0
- package/docs/DESIGN.md +4 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.47.1] - 2026-10-03
|
|
11
|
+
|
|
12
|
+
### 修正
|
|
13
|
+
|
|
14
|
+
- 混んだ端末で、agentの画面が描かれる前に入力受付の待ち(30秒)が切れ、`agent_launch`が`reason=unrecognized_screen`で初手を送らなかった。画面が起動コマンドの表示のままの時だけ、起動から50秒まで待つ。描かれた後の画面の扱いと、50秒でも描かれない時の未送信の返し方は変わらない。
|
|
15
|
+
- Codex・Claude Code・Cursorを親にしたMCP processが、5秒おきに全processの一覧を2回ずつ取っていた(POSIXは`ps -axww`、WindowsはPowerShell)。processの多い端末でCPUを使い続け、その間MCPの応答も止まっていた。5秒おきの確認は他の持ち主のpidの存在だけを見て、開始時刻の照合は初めて見た持ち主と60秒に1回だけ、対象のpidに絞って行う。配送記録を引き継ぐ条件は変わらない。終了直後に同じpidが再利用された時だけ、引き継ぎが最長60秒遅れる。
|
|
16
|
+
|
|
10
17
|
## [0.47.0] - 2026-10-03
|
|
11
18
|
|
|
12
19
|
### 追加
|
|
@@ -1976,7 +1983,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
1976
1983
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
1977
1984
|
provenance.
|
|
1978
1985
|
|
|
1979
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.47.
|
|
1986
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.47.1...HEAD
|
|
1987
|
+
[0.47.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.47.0...v0.47.1
|
|
1980
1988
|
[0.47.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.46.1...v0.47.0
|
|
1981
1989
|
[0.46.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.46.0...v0.46.1
|
|
1982
1990
|
[0.46.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.45.2...v0.46.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.47.
|
|
212
|
+
**状態:** 開発継続中 · 現行公開版 **v0.47.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -514,6 +514,8 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
|
|
|
514
514
|
進め、入力受付とharness生存を確認して`startup.status="ready"`を返す。指定なしのpromptなし起動は`not_checked`。
|
|
515
515
|
Claude Code初回起動の文字表示テーマ選択では、画面で選択済みの項目を確定して起動を続ける。
|
|
516
516
|
続いてログイン方式の選択が出た場合は`vendor_onboarding_required`を返す。公式の対話型Claude Code CLIで初回設定を完了する。
|
|
517
|
+
入力受付の待ちは30秒。混んだ端末でagentの画面がまだ描かれていない間に切れた時だけ、起動から50秒まで待つ。
|
|
518
|
+
それでも描かれなければ、今までと同じく未送信(`initial_prompt=not_sent`)で返り、sessionは残る。
|
|
517
519
|
WindowsのCodexもhook確認を認識し、npm shim経由の起動を一つのharnessとして識別する。
|
|
518
520
|
初手の`initial_prompt.status`は`not_requested`/`not_sent`/`submitted_unconfirmed`/`started`を区別する。
|
|
519
521
|
未送信・未確認の失敗もsession付きstructuredContentを保持する。未確認のpromptを再送せず、返ったcursorで観測する。
|
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.47.
|
|
226
|
+
**Status:** actively maintained · current public release **v0.47.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
|
|
|
@@ -546,6 +546,8 @@ displayed token count or null. Callers do not need raw argv or pane-text parsing
|
|
|
546
546
|
consent even without a prompt, then verifies input readiness and harness liveness before returning `startup.status="ready"`.
|
|
547
547
|
For Claude Code's first-run text-style menu, it confirms the item already selected on screen before continuing startup.
|
|
548
548
|
If the CLI then requests an account login method, the launch reports `vendor_onboarding_required`; complete that choice in the official interactive Claude Code CLI.
|
|
549
|
+
The input-readiness wait is 30 seconds. Only when it expires while the agent has not drawn anything yet (a busy host), the launch
|
|
550
|
+
waits up to 50 seconds from startup. If nothing is drawn by then, it still returns `initial_prompt=not_sent` and keeps the session.
|
|
549
551
|
A prompt-free launch without this option retains `startup.status="not_checked"`. `initial_prompt.status` distinguishes
|
|
550
552
|
`not_requested`, `not_sent`, `submitted_unconfirmed`, and `started`. Failure responses retain structured session information.
|
|
551
553
|
WindowsのCodexもhook確認を認識し、npm shim経由の起動を一つのharnessとして識別する。
|
package/dist/core.js
CHANGED
|
@@ -51,6 +51,11 @@ const AGENT_DONE_SCREEN_SETTLE_MIN_SAMPLES = 3;
|
|
|
51
51
|
const AGENT_SUBMIT_DELAY_MS = 250;
|
|
52
52
|
const AGENT_METADATA_NEGATIVE_CACHE_TTL_MS = 2_000;
|
|
53
53
|
const AGENT_TUI_READY_TIMEOUT_MS = 30_000;
|
|
54
|
+
// 画面が起動コマンドの表示のまま(agentがまだ何も描いていない)で上の待ちが切れた時に、起動から待つ上限。
|
|
55
|
+
// Codex親の既定のtool timeout(60秒)の内側に収める。越えると親には時間切れに見えたまま子へ初手が送られる。
|
|
56
|
+
const AGENT_TUI_FIRST_DRAW_TIMEOUT_MS = 50_000;
|
|
57
|
+
const LAUNCH_ECHO_TAIL_CHARS = 24;
|
|
58
|
+
const LAUNCH_ECHO_MIN_CHARS = 8;
|
|
54
59
|
const AGENT_TUI_READY_POLL_MS = 500;
|
|
55
60
|
const AGENT_TUI_READY_STABLE_SAMPLES = 11;
|
|
56
61
|
const AGENT_TUI_READY_LINES = 45;
|
|
@@ -1272,6 +1277,7 @@ function closeSessionInternal(name, observeDependency = true) {
|
|
|
1272
1277
|
}
|
|
1273
1278
|
}
|
|
1274
1279
|
cleanupAgentState(name);
|
|
1280
|
+
agentLaunchLines.delete(name);
|
|
1275
1281
|
return `closed ${name}`;
|
|
1276
1282
|
}
|
|
1277
1283
|
export function closeSession(name) {
|
|
@@ -2829,11 +2835,32 @@ function isClaudeBypassPermissionsScreen(screen) {
|
|
|
2829
2835
|
function isClaudeManagedLaunchConfirmation(screen) {
|
|
2830
2836
|
return isClaudeWorkspaceTrustScreen(screen) || isClaudeBypassPermissionsScreen(screen);
|
|
2831
2837
|
}
|
|
2838
|
+
/**
|
|
2839
|
+
* 画面の末尾が、Aitermが打った起動コマンドの表示のままかを見る(純粋関数)。
|
|
2840
|
+
* 混んだ端末ではagentのTUIが描かれる前にready待ちが切れる。画面の最後の文字列が起動コマンドの一部なら、
|
|
2841
|
+
* agentはまだ何も描いていない。空白と行頭の継続prompt(`> `/`>> `)は端末の折返しとshellの表示なので比べない。
|
|
2842
|
+
*/
|
|
2843
|
+
export function launchEchoIsLastOnScreen(screen, launchLine) {
|
|
2844
|
+
const command = launchLine.replace(/\s+/g, "");
|
|
2845
|
+
const lines = screen.split("\n");
|
|
2846
|
+
let tail = "";
|
|
2847
|
+
for (let i = lines.length - 1; i >= 0 && tail.length < LAUNCH_ECHO_TAIL_CHARS; i--) {
|
|
2848
|
+
tail = lines[i].replace(/^\s*>{1,2}(?:\s|$)/, "").replace(/\s+/g, "") + tail;
|
|
2849
|
+
}
|
|
2850
|
+
tail = tail.slice(-LAUNCH_ECHO_TAIL_CHARS);
|
|
2851
|
+
return tail.length >= LAUNCH_ECHO_MIN_CHARS && command.includes(tail);
|
|
2852
|
+
}
|
|
2853
|
+
// 起動コマンドはopenAgentだけが知っている。同じprocess内のready待ちが、TUI未描画の判定に使う。
|
|
2854
|
+
const agentLaunchLines = new Map();
|
|
2832
2855
|
async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
|
|
2833
2856
|
const timeoutMs = opts.timeoutMs ?? AGENT_TUI_READY_TIMEOUT_MS;
|
|
2834
2857
|
const pollMs = opts.pollMs ?? AGENT_TUI_READY_POLL_MS;
|
|
2835
2858
|
const stableSamples = opts.stableSamples ?? agentTuiReadyStableSamplesTestOverride ?? AGENT_TUI_READY_STABLE_SAMPLES;
|
|
2836
|
-
const
|
|
2859
|
+
const now = opts.now ?? (() => performance.now());
|
|
2860
|
+
const start = now();
|
|
2861
|
+
let deadline = start + timeoutMs;
|
|
2862
|
+
// 呼ぶ側が既定より短い待ちを指定した時は、その待ちを守る(延ばさない)。
|
|
2863
|
+
const firstDrawDeadline = timeoutMs >= AGENT_TUI_READY_TIMEOUT_MS ? start + AGENT_TUI_FIRST_DRAW_TIMEOUT_MS : deadline;
|
|
2837
2864
|
let samples = 0;
|
|
2838
2865
|
let readyStreak = 0;
|
|
2839
2866
|
let lastScreen = "";
|
|
@@ -2852,13 +2879,21 @@ async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
|
|
|
2852
2879
|
if (isAgentTuiActionRequired(kind, lastScreen))
|
|
2853
2880
|
return { ready: false, samples, lastScreen };
|
|
2854
2881
|
}
|
|
2855
|
-
if (
|
|
2856
|
-
|
|
2882
|
+
if (now() >= deadline) {
|
|
2883
|
+
// TUIが一度も描かれていない間に切れた時だけ、決めた上限まで待ちを延ばす。描かれた後の画面は今までどおり扱う。
|
|
2884
|
+
if (deadline >= firstDrawDeadline || opts.launchLine === undefined
|
|
2885
|
+
|| !launchEchoIsLastOnScreen(lastScreen, opts.launchLine))
|
|
2886
|
+
return { ready: false, samples, lastScreen };
|
|
2887
|
+
deadline = firstDrawDeadline;
|
|
2888
|
+
}
|
|
2857
2889
|
await sleepFn(pollMs);
|
|
2858
2890
|
}
|
|
2859
2891
|
}
|
|
2860
2892
|
async function waitAgentTuiReady(name, meta, timeoutMs = AGENT_TUI_READY_TIMEOUT_MS) {
|
|
2861
|
-
|
|
2893
|
+
const result = await waitAgentTuiReadyImpl(meta.kind, () => captureScreen(name, AGENT_TUI_READY_LINES), sleep, { timeoutMs, launchLine: agentLaunchLines.get(name) });
|
|
2894
|
+
if (result.ready)
|
|
2895
|
+
agentLaunchLines.delete(name);
|
|
2896
|
+
return result;
|
|
2862
2897
|
}
|
|
2863
2898
|
async function waitAgentTuiReadyByKind(name, kind, timeoutMs = AGENT_TUI_READY_TIMEOUT_MS) {
|
|
2864
2899
|
return waitAgentTuiReadyImpl(kind, () => captureScreen(name, AGENT_TUI_READY_LINES), sleep, { timeoutMs });
|
|
@@ -3036,9 +3071,13 @@ export async function __testWaitAgentTuiReady(kind, samples, opts = {}) {
|
|
|
3036
3071
|
throw new AitermError("agent ready test samples が空です", 2);
|
|
3037
3072
|
let i = 0;
|
|
3038
3073
|
const sleeps = [];
|
|
3074
|
+
// virtualClockは待った分だけ進む時計。実時間を使わずに締切の扱いを確かめる。
|
|
3075
|
+
let clock = 0;
|
|
3076
|
+
const { virtualClock, ...waitOpts } = opts;
|
|
3039
3077
|
const result = await waitAgentTuiReadyImpl(kind, () => samples[Math.min(i++, samples.length - 1)], async (ms) => {
|
|
3040
3078
|
sleeps.push(ms);
|
|
3041
|
-
|
|
3079
|
+
clock += ms;
|
|
3080
|
+
}, virtualClock ? { ...waitOpts, now: () => clock } : waitOpts);
|
|
3042
3081
|
return { ...result, sleeps };
|
|
3043
3082
|
}
|
|
3044
3083
|
export function __testIsAgentTuiIdleReady(kind, screen) {
|
|
@@ -4357,6 +4396,8 @@ export function openAgent(kind, opts = {}) {
|
|
|
4357
4396
|
const envPrefix = agentEnvPrefix(meta, sid, envVars);
|
|
4358
4397
|
full = cwdForCmd ? `cd ${shq(cwdForCmd)} && ${envPrefix}${cmd}` : `${envPrefix}${cmd}`;
|
|
4359
4398
|
}
|
|
4399
|
+
if (meta)
|
|
4400
|
+
agentLaunchLines.set(sid, full);
|
|
4360
4401
|
// force:true はagent sessionへの手動介入を表す。起動コマンド自体はAitermが組み立てて素送信する。
|
|
4361
4402
|
send(sid, full, {
|
|
4362
4403
|
enter: true,
|
package/dist/parent-delivery.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { randomUUID, createHash } from "node:crypto";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { ensureStateRoot, writeJson0600 } from "./agent-shared.js";
|
|
7
|
-
import {
|
|
7
|
+
import { readProcessIdentities } from "./process-runtime.js";
|
|
8
8
|
import { CodexDeliveryError, submitCodexParentAnswer, verifyCodexParent } from "./codex-parent-receiver.js";
|
|
9
9
|
import { claudeParentSchema, ClaudeDeliveryError, bindClaudeParentDelivery, submitClaudeParentAnswer, verifyClaudeParent } from "./claude-parent-receiver.js";
|
|
10
10
|
import { cursorParentSchema, CursorDeliveryError, prepareCursorDelivery, submitCursorParentAnswer, verifyCursorParent } from "./cursor-parent-receiver.js";
|
|
@@ -57,6 +57,22 @@ async function submitParentAnswer(parent, deliveryId, text) {
|
|
|
57
57
|
return submitCursorParentAnswer(parent, deliveryId, text);
|
|
58
58
|
return submitCodexParentAnswer(parent, deliveryId, text);
|
|
59
59
|
}
|
|
60
|
+
// 開始時刻の照会はprocessの起動を伴い、processの多い端末では1回が重い(macOSのpsは-pでも全processを歩く)。
|
|
61
|
+
// 5秒おきの確認はpidの存在だけを見て、開始時刻は初めて見た持ち主とこの間隔でだけ照合する。
|
|
62
|
+
// 持ち主が終了した直後に同じpidが再利用された時だけ、回収が最長でこの間隔だけ遅れる。生きている持ち主の記録は引き取らない。
|
|
63
|
+
const OWNER_IDENTITY_RECHECK_MS = 60_000;
|
|
64
|
+
function processExists(pid) {
|
|
65
|
+
// 0以下はprocess groupを指す。持ち主のpidにはならない。
|
|
66
|
+
if (pid <= 0)
|
|
67
|
+
return false;
|
|
68
|
+
try {
|
|
69
|
+
process.kill(pid, 0);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return error.code === "EPERM";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
60
76
|
function readRecord(file) {
|
|
61
77
|
try {
|
|
62
78
|
const record = recordSchema.parse(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
@@ -98,11 +114,14 @@ export class ParentDeliveryManager {
|
|
|
98
114
|
registering = new Map();
|
|
99
115
|
timer;
|
|
100
116
|
recovering = null;
|
|
117
|
+
// 他の持ち主の開始時刻を最後に照合した時刻(持ち主の保存場所ごと)。
|
|
118
|
+
verifiedOwners = new Map();
|
|
101
119
|
serviceError = null;
|
|
102
120
|
closing = false;
|
|
103
121
|
constructor(options = {}) {
|
|
104
122
|
this.deps = { observe: observeAnywhere, answer: answerAnywhere, submit: submitParentAnswer,
|
|
105
|
-
verify: verifyParent, processes:
|
|
123
|
+
verify: verifyParent, processes: readProcessIdentities, exists: processExists,
|
|
124
|
+
now: () => performance.now(), ...options.dependencies };
|
|
106
125
|
const stateRoot = ensureStateRoot();
|
|
107
126
|
// 別端末の子の記録はremote-で始まる保存場所へ分け、boundary.remoteを知らない旧版のreaderへ渡さない。
|
|
108
127
|
const prefix = options.remote ? "remote-" : "";
|
|
@@ -118,7 +137,7 @@ export class ParentDeliveryManager {
|
|
|
118
137
|
this.active = path.join(this.root, "active");
|
|
119
138
|
this.results = path.join(this.root, "results");
|
|
120
139
|
this.claims = path.join(options.root ?? path.join(stateRoot, prefix + "parent-deliveries"), "claims");
|
|
121
|
-
const ownProcess = this.deps.processes().find((entry) => entry.pid === process.pid);
|
|
140
|
+
const ownProcess = this.deps.processes([process.pid]).find((entry) => entry.pid === process.pid);
|
|
122
141
|
if (!ownProcess)
|
|
123
142
|
throw new AitermError("PARENT_DELIVERY_OWNER_UNKNOWN: 配送processを識別できません", 2);
|
|
124
143
|
this.owner = { pid: process.pid, started_identity: ownProcess.started_identity, closed: false };
|
|
@@ -375,13 +394,49 @@ export class ParentDeliveryManager {
|
|
|
375
394
|
return this.recovering;
|
|
376
395
|
}
|
|
377
396
|
async recoverOrphans() {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
397
|
+
// 生存を確かめる相手は、closeしていない他の持ち主だけ。居なければOSへ問い合わせない。
|
|
398
|
+
// この処理は5秒おきに全MCP processで回る。全processの一覧を取ると、processの多い端末で負荷になる。
|
|
399
|
+
const directories = fs.readdirSync(this.active, { withFileTypes: true })
|
|
400
|
+
.filter((directory) => directory.isDirectory() && path.join(this.active, directory.name) !== this.ownerDir);
|
|
401
|
+
const now = this.deps.now();
|
|
402
|
+
const alive = new Map();
|
|
403
|
+
const unverified = [];
|
|
404
|
+
for (const directory of directories) {
|
|
405
|
+
let owner;
|
|
406
|
+
try {
|
|
407
|
+
owner = JSON.parse(fs.readFileSync(path.join(this.active, directory.name, "owner.json"), "utf8"));
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
continue; /* 形式の検査と失敗の扱いは下の回収で行う */
|
|
411
|
+
}
|
|
412
|
+
if (owner?.closed !== false || !Number.isSafeInteger(owner.pid) || typeof owner.started_identity !== "string")
|
|
381
413
|
continue;
|
|
382
|
-
const
|
|
383
|
-
if (
|
|
414
|
+
const pid = owner.pid;
|
|
415
|
+
if (!this.deps.exists(pid)) {
|
|
416
|
+
alive.set(directory.name, false);
|
|
384
417
|
continue;
|
|
418
|
+
}
|
|
419
|
+
const verified = this.verifiedOwners.get(directory.name);
|
|
420
|
+
if (verified && verified.pid === pid && verified.started_identity === owner.started_identity
|
|
421
|
+
&& now - verified.at < OWNER_IDENTITY_RECHECK_MS)
|
|
422
|
+
alive.set(directory.name, true);
|
|
423
|
+
else
|
|
424
|
+
unverified.push({ name: directory.name, pid, started_identity: owner.started_identity });
|
|
425
|
+
}
|
|
426
|
+
if (unverified.length > 0) {
|
|
427
|
+
const processes = this.deps.processes([...new Set(unverified.map((entry) => entry.pid))]);
|
|
428
|
+
for (const entry of unverified) {
|
|
429
|
+
const same = processes.some((row) => row.pid === entry.pid && row.started_identity === entry.started_identity);
|
|
430
|
+
alive.set(entry.name, same);
|
|
431
|
+
if (same)
|
|
432
|
+
this.verifiedOwners.set(entry.name, { pid: entry.pid, started_identity: entry.started_identity, at: now });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (const name of this.verifiedOwners.keys())
|
|
436
|
+
if (alive.get(name) !== true)
|
|
437
|
+
this.verifiedOwners.delete(name);
|
|
438
|
+
for (const directory of directories) {
|
|
439
|
+
const oldDir = path.join(this.active, directory.name);
|
|
385
440
|
let owner;
|
|
386
441
|
try {
|
|
387
442
|
owner = JSON.parse(fs.readFileSync(path.join(oldDir, "owner.json"), "utf8"));
|
|
@@ -394,7 +449,8 @@ export class ParentDeliveryManager {
|
|
|
394
449
|
if (!Number.isSafeInteger(owner.pid) || typeof owner.started_identity !== "string" || typeof owner.closed !== "boolean") {
|
|
395
450
|
throw new AitermError("PARENT_DELIVERY_OWNER_INVALID: 配送所有者の形式が不正です", 2);
|
|
396
451
|
}
|
|
397
|
-
|
|
452
|
+
// 生死を確かめた後に現れた持ち主は、次の回で扱う。生きている持ち主の記録は引き取らない。
|
|
453
|
+
if (!owner.closed && alive.get(directory.name) !== false)
|
|
398
454
|
continue;
|
|
399
455
|
for (const name of fs.readdirSync(oldDir).filter((name) => name !== "owner.json" && name.endsWith(".json"))) {
|
|
400
456
|
const oldFile = path.join(oldDir, name);
|
package/dist/process-runtime.js
CHANGED
|
@@ -75,6 +75,60 @@ export function readRuntimeProcesses() {
|
|
|
75
75
|
};
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* 指定したpidの開始時刻だけを引く。readRuntimeProcessesと同じ開始時刻を返し、存在しないpidは結果に含めない。
|
|
80
|
+
* 全processのargvを読む一覧取得は、processの多い端末で重い(定期実行から呼ばない)。
|
|
81
|
+
*/
|
|
82
|
+
export function readProcessIdentities(pids) {
|
|
83
|
+
const wanted = [...new Set(pids)];
|
|
84
|
+
if (wanted.length === 0)
|
|
85
|
+
return [];
|
|
86
|
+
if (wanted.some(pid => !Number.isSafeInteger(pid) || pid < 0))
|
|
87
|
+
throw new AitermError("process照会のpidが不正です", 2);
|
|
88
|
+
if (!isWin) {
|
|
89
|
+
const result = spawnSync("/bin/ps", ["-o", "pid=,lstart=", "-p", wanted.join(",")], {
|
|
90
|
+
encoding: "utf8", env: { ...process.env, LC_ALL: "C" }, timeout: 10000,
|
|
91
|
+
});
|
|
92
|
+
// 該当するprocessが一つも無い時、psは何も出さずstatus 1で終わる。
|
|
93
|
+
if (result.error || (result.status !== 0 && !(result.status === 1 && !result.stdout.trim()))) {
|
|
94
|
+
throw new AitermError("OSのprocess一覧を取得できません", 2);
|
|
95
|
+
}
|
|
96
|
+
return result.stdout.split("\n").filter(line => line.trim()).map(line => {
|
|
97
|
+
const match = /^\s*(\d+)\s+(\S+\s+\S+\s+\d+\s+\S+\s+\d+)\s*$/.exec(line);
|
|
98
|
+
if (!match)
|
|
99
|
+
throw new AitermError("process一覧の形式を認識できません", 2);
|
|
100
|
+
return { pid: Number(match[1]), started_identity: match[2].trim() };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const script = [
|
|
104
|
+
"$ErrorActionPreference='Stop'",
|
|
105
|
+
"[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)",
|
|
106
|
+
`$rows=@(Get-CimInstance Win32_Process -Filter "${wanted.map(pid => `ProcessId=${pid}`).join(" OR ")}" | Where-Object { $null -ne $_.CreationDate -and $null -ne $_.CommandLine } | ForEach-Object {`,
|
|
107
|
+
"[ordered]@{ pid=[int]$_.ProcessId; started_identity=$_.CreationDate.ToUniversalTime().ToString('o') }",
|
|
108
|
+
"})",
|
|
109
|
+
"ConvertTo-Json -Compress -InputObject $rows",
|
|
110
|
+
].join("\n");
|
|
111
|
+
const result = spawnSync(resolveWindowsPowerShell7(), ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")], {
|
|
112
|
+
encoding: "utf8", timeout: 15000, windowsHide: true,
|
|
113
|
+
});
|
|
114
|
+
if (result.error || result.status !== 0)
|
|
115
|
+
throw new AitermError("Windowsのnative process一覧を取得できません", 2);
|
|
116
|
+
let rows;
|
|
117
|
+
try {
|
|
118
|
+
rows = JSON.parse(result.stdout);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
throw new AitermError("Windows process一覧のJSONを読めません", 2);
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(rows))
|
|
124
|
+
throw new AitermError("Windows process一覧が配列ではありません", 2);
|
|
125
|
+
return rows.map(row => {
|
|
126
|
+
if (!row || !Number.isSafeInteger(row.pid) || typeof row.started_identity !== "string"
|
|
127
|
+
|| !Number.isFinite(Date.parse(row.started_identity)))
|
|
128
|
+
throw new AitermError("Windows process一覧のfieldが不正です", 2);
|
|
129
|
+
return { pid: row.pid, started_identity: new Date(row.started_identity).toISOString() };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
78
132
|
// Windowsの親PIDは親の終了後も残り、別processへ再利用される。子より後に始まったprocessは
|
|
79
133
|
// 本当の親ではないので、親子関係として辿らない(辿ると循環や無関係なprocessの混入が起きる)。
|
|
80
134
|
export function parentProcess(row, byPid) {
|
package/docs/DESIGN.md
CHANGED
|
@@ -136,6 +136,8 @@ Codexの配送記録と本文はAiterm stateの`parent-deliveries`へ保存す
|
|
|
136
136
|
再接続後は終了したownerの記録だけを原子的に引き継ぐ。`waiting`は同じ境界から観測を再開し、
|
|
137
137
|
`ready`は保存した本文を送る。送信中断は`unknown`として本文を残し、自動再送しない。
|
|
138
138
|
受信口が明示拒否した場合は`failed`、子の異常終了はそのoutcomeを配送する。
|
|
139
|
+
生存の確認は5秒おきにpidの存在だけをOSへ聞き、開始識別子の照合は初めて見たownerと60秒に1回だけ行う。
|
|
140
|
+
全processの一覧は取らない。終了直後に同じpidが再利用された時だけ、引き継ぎが最長60秒遅れる(ADR 0078)。
|
|
139
141
|
このstateは既存のPTY/harness stateと独立し、旧版は配送を再開しない。
|
|
140
142
|
|
|
141
143
|
### Claude Code親への自動配送
|
|
@@ -195,6 +197,8 @@ Claude Codeの初回テーマ選択は選択済みの項目を確定し、後続
|
|
|
195
197
|
ログイン方式の選択はユーザーのアカウント設定として扱い、composerと誤認せず`vendor_onboarding_required`で止める。
|
|
196
198
|
promptなしでも入力受付とharness生存を確認して`startup.ready`を返す。指定なしのpromptなし起動は
|
|
197
199
|
従来どおり`startup.not_checked`で返す。初手receiptは未要求・未送信・送信済み未確認・開始確認を分ける。
|
|
200
|
+
入力受付の待ちは30秒。画面が起動コマンドの表示のまま(agentがまだ何も描いていない)で切れた時だけ、
|
|
201
|
+
起動から50秒まで待つ。Codex親の既定のtool timeout(60秒)の内側に収める(ADR 0078)。
|
|
198
202
|
開始の証拠は送信後の実行表示、実行中の既知承認、または同じcursor以降の完了だけとし、残存なしでは代用しない。
|
|
199
203
|
Codexの設定エラー等でCLIが終了した場合は、残った画面へ送らず未送信で止める。
|
|
200
204
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.47.
|
|
3
|
+
"version": "0.47.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": [
|