aiterm-mcp 0.52.1 → 0.52.3
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 +43 -1
- package/README.ja.md +2 -2
- package/README.md +2 -2
- package/dist/core.js +73 -14
- package/dist/parent-hook-diagnostic.js +5 -11
- package/dist/process-runtime.js +39 -1
- package/docs/DESIGN.md +7 -0
- package/docs/RELEASE.md +3 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.52.3] - 2026-10-05
|
|
11
|
+
|
|
12
|
+
### 修正
|
|
13
|
+
|
|
14
|
+
- Claude Code親の配送hookの登録を、`args`を使わずshellを通す1行で書く(aiterm-steer-delivery 0.2.0、ADR 0089)。
|
|
15
|
+
Grokは共有の`~/.claude/settings.json`のhookも動かすが、`args`を落として`command`だけを動かす。
|
|
16
|
+
0.52.2までの`command=node, args=[claude-parent-hook.js]`の形では、Grokがnodeだけを起こし、nodeがhookの入力を
|
|
17
|
+
scriptとして読んで、会話の終わりのたびに`[stdin]:1`で失敗の記録を残していた(Grok 1.0.46、LinuxとWindowsで再現)。
|
|
18
|
+
POSIXは`exec '<node>' '<hook>'`、Windowsは`& '<node>' '<hook>'; exit (Get-Variable LASTEXITCODE -ValueOnly)`と
|
|
19
|
+
`shell: "powershell"`。`aiterm-setup`(`--hooks-only`も)を流すと、0.52.2までの登録をこの形へ置き換える。
|
|
20
|
+
Grokから起こされたhookは何もせず0で終わる。PreToolUseのhookは、間に挟まるshellではなくClaude Code本体を
|
|
21
|
+
依頼元として記録する。
|
|
22
|
+
- Windowsで、Claude Code親へ子の回答が届いているのに、配送が`CLAUDE_PARENT_HOOK_CLOSED`(結果不明)で終わる事があった。
|
|
23
|
+
process表を読む間(約1秒)に受信hookが本文を出し終えて終わると、出し終えた後のhookを「途中で終わった」と数えていた。
|
|
24
|
+
- `diagnostics`の親配送hookの状態は、0.52.2までの登録と新しい登録のどちらも「登録済み」と読む。
|
|
25
|
+
|
|
26
|
+
### 注意
|
|
27
|
+
|
|
28
|
+
- 0.52.2以前へ戻す時は、旧版のinstall前に`aiterm-setup --remove-claude-parent-hooks`を実行する。
|
|
29
|
+
0.52.2以前は新しい形の登録を自分の物と数えず、もう1組を足す。
|
|
30
|
+
- Windowsでは、子の回答を待っているhook1つにつき、Claude Codeが起こすPowerShellのprocessが1つ残る。
|
|
31
|
+
|
|
32
|
+
## [0.52.2] - 2026-10-05
|
|
33
|
+
|
|
34
|
+
### 修正
|
|
35
|
+
|
|
36
|
+
- 0.52.1で、socketの置き場(`claude-tmux-sockets`)ごと無くなったagent sessionへの送信が、
|
|
37
|
+
`ENOENT: … .agent-send.lock`という生のエラーを返していた。0.52.0までは
|
|
38
|
+
「入力受付状態になりません。文字列は送信していません。」の断りだった。連携元が「文字列は送信していません」で
|
|
39
|
+
未送信を見分けている時、0.52.1ではこの場合だけ見分けられなかった。送信の順番を守るlock(ADR 0088)を
|
|
40
|
+
置き場に作れない時は、lockを取らずに進め、今まで通りの断りを返す。置き場が無ければsessionにも届かず、
|
|
41
|
+
守る相手が無い。sessionだけが無くなった時(置き場は残る)の断りは、0.52.1でも変わっていない。
|
|
42
|
+
- 0.52.1で、送信の途中で終了したprocessのpidを別の生きたprocessが使い回すと、そのagent sessionへの送信が
|
|
43
|
+
`AGENT_SEND_BUSY`で断られ続けた。lockの持ち主をpidだけで見ていたため。`pty_close`してもlockが残り、
|
|
44
|
+
閉じて同じIDで起動し直しても同じ断りだった。生きて見える持ち主は、processが始まってからの経過と
|
|
45
|
+
lockを作ってからの時間を比べて本人かを確かめ、lockより後に始まった別のprocessなら片付ける。
|
|
46
|
+
片付けの途中で残った印は、5秒より古ければ、pidが生きて見えても残った物と読む(その時の断りは今までと同じ文面)。
|
|
47
|
+
`pty_close`は、使い回されたpidのlockと古い印も片付ける。OSへの照会は、送信が実際に重なった時と、
|
|
48
|
+
閉じる時にlockが残っていた時だけ行う。
|
|
49
|
+
|
|
10
50
|
## [0.52.1] - 2026-10-05
|
|
11
51
|
|
|
12
52
|
### 修正
|
|
@@ -2117,7 +2157,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2117
2157
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2118
2158
|
provenance.
|
|
2119
2159
|
|
|
2120
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.52.
|
|
2160
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.52.3...HEAD
|
|
2161
|
+
[0.52.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.52.2...v0.52.3
|
|
2162
|
+
[0.52.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.52.1...v0.52.2
|
|
2121
2163
|
[0.52.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.52.0...v0.52.1
|
|
2122
2164
|
[0.52.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.3...v0.52.0
|
|
2123
2165
|
[0.51.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.2...v0.51.3
|
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.52.
|
|
212
|
+
**状態:** 開発継続中 · 現行公開版 **v0.52.3** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -655,7 +655,7 @@ Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が
|
|
|
655
655
|
|
|
656
656
|
`/clear`などで会話を終了すると未送信の旧回答の配送を止め、本文は保存する。受信hookの上限は24時間で、終了や出力失敗を成功扱いしない。hookが無効な場合は送信前に明示errorにし、waiterへ黙って切り替えない。Claude Desktopのチャット、Web、`agent_id`付きの会話(`--agent`起動とnative subagent)はこの受信契約に含めない。
|
|
657
657
|
|
|
658
|
-
hook
|
|
658
|
+
hookを持たない旧版、または0.52.2以前へ戻す時は、install前に`aiterm-setup --remove-claude-parent-hooks`を実行する。Aiterm専用hookだけを解除し、他製品のhookと設定は保持する。
|
|
659
659
|
|
|
660
660
|
Cursor親(`clientInfo.name`が`cursor-vscode`)も同じ完了観測と本文保存を使う。`aiterm-setup`が`~/.cursor/hooks.json`へ`afterMCPExecution`と`postToolUse`を追加し、他製品のhookと順序は保持する。hookが無い場合は子への送信前に`CURSOR_PARENT_HOOK_UNAVAILABLE`で止める。作業を続けていれば次のツール返りへ`additional_context`で本文が差し込まれ、ターンを終える前にreceiptの`wait_process`を背景で起動しておけばidle中の完了でも起きられる。`wait_command`はnull。`submitted`はhookまたは受け口が本文を受け取った状態であり、modelの読了ではない。24時間以内に受け取りが無ければ`failed`とし、本文は残して自動再送しない。解除は`aiterm-setup --remove-cursor-parent-hooks`。Cursor Cloud Agent/Background Agentはこの受信契約に含めない。
|
|
661
661
|
|
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.52.
|
|
226
|
+
**Status:** actively maintained · current public release **v0.52.3** · 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
|
|
|
@@ -696,7 +696,7 @@ Claude Codeは2.1.259以上の対話sessionに対応する。`aiterm-setup`が
|
|
|
696
696
|
|
|
697
697
|
`/clear`等の会話終了後は未送信の旧回答を送らず、本文を保存する。受信hookの上限は24時間。hookの終了・出力失敗・無効化を成功扱いせず、別の待機経路へ黙って切り替えない。Claude Desktopのチャット、Web、`agent_id`付きの会話(`--agent`起動とnative subagent)はこの受信契約に含めない。
|
|
698
698
|
|
|
699
|
-
hook
|
|
699
|
+
hookを持たない旧版、または0.52.2以前へ戻す時は、install前に`aiterm-setup --remove-claude-parent-hooks`を実行する。Aiterm専用hookだけを解除し、他製品のhookと設定は保持する。
|
|
700
700
|
|
|
701
701
|
Cursor parents (`clientInfo.name` of `cursor-vscode`) use the same completion capture. `aiterm-setup` adds `afterMCPExecution` and `postToolUse` to `~/.cursor/hooks.json` and keeps every other hook and its position. A missing registration fails the dispatch with `CURSOR_PARENT_HOOK_UNAVAILABLE` before the child is sent. While the parent keeps calling tools, the answer is injected through `additional_context` on the next tool result. If the parent ends the turn, start the receipt `wait_process` in the background first; that receiver exits when the answer arrives. `wait_command` is null. `submitted` means the hook or the receiver claimed the text, not that the model has read it. No claim within 24 hours is `failed`, the text is kept, and nothing is resent. Remove only Aiterm's entries with `aiterm-setup --remove-cursor-parent-hooks`. Cursor Cloud Agents and Background Agents are outside this contract.
|
|
702
702
|
|
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, aitermProcessProbe, windowsConsoleHost } from "./process-runtime.js";
|
|
19
|
+
import { readRuntimeProcesses, processSubtree, parentProcess, processIdentity, backgroundProcesses, lazyRelayProcess, aitermProcessProbe, windowsConsoleHost, processElapsedSeconds } 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_RATE_LIMIT_POLL_MS, pollGate, AGENT_EVENT_MAX_BYTES, assertSessionName, agentsDir, agentEventPath, agentMetadataPath, writeAgentMetadata, AGENT_EVENT_TAIL_BYTES, agentLabel, agentHarness, subagentInstruction, agentLineageFields, } from "./agent-shared.js";
|
|
@@ -1860,6 +1860,7 @@ function isPidAlive(pid) {
|
|
|
1860
1860
|
function probeWaitLock(p) {
|
|
1861
1861
|
let pid = null;
|
|
1862
1862
|
let at = null;
|
|
1863
|
+
let token = null;
|
|
1863
1864
|
let ageMs = 0;
|
|
1864
1865
|
try {
|
|
1865
1866
|
const st = fs.lstatSync(p);
|
|
@@ -1871,15 +1872,17 @@ function probeWaitLock(p) {
|
|
|
1871
1872
|
pid = v.pid;
|
|
1872
1873
|
if (typeof v.at === "string")
|
|
1873
1874
|
at = v.at;
|
|
1875
|
+
if (typeof v.token === "string")
|
|
1876
|
+
token = v.token;
|
|
1874
1877
|
}
|
|
1875
1878
|
catch {
|
|
1876
1879
|
/* pid 不明のまま鮮度判定に落ちる */
|
|
1877
1880
|
}
|
|
1878
1881
|
if (pid == null)
|
|
1879
|
-
return { pid: null, at, live: ageMs < WAIT_LOCK_FRESH_MS };
|
|
1882
|
+
return { pid: null, at, token, ageMs, live: ageMs < WAIT_LOCK_FRESH_MS };
|
|
1880
1883
|
if (pid === process.pid)
|
|
1881
|
-
return { pid, at, live: false };
|
|
1882
|
-
return { pid, at, live: isPidAlive(pid) };
|
|
1884
|
+
return { pid, at, token, ageMs, live: false };
|
|
1885
|
+
return { pid, at, token, ageMs, live: isPidAlive(pid) };
|
|
1883
1886
|
}
|
|
1884
1887
|
function unlinkStaleWaitLock(p) {
|
|
1885
1888
|
try {
|
|
@@ -2034,16 +2037,42 @@ function probeAgentSendLock(name) {
|
|
|
2034
2037
|
const probe = probeWaitLock(agentSendLockPath(name));
|
|
2035
2038
|
return probe.pid === process.pid && agentSendHeld.has(name) ? { ...probe, live: true } : probe;
|
|
2036
2039
|
}
|
|
2040
|
+
// 持ち主のpidが生きて見えても、lockより後に始まったprocessなら、終了した持ち主のpidを使い回した別のprocessである。
|
|
2041
|
+
// 読みの粗さ(psの経過は1秒刻み)と小さな時計合わせで本人を別人と読まないよう、この秒数を越えて若い時だけ別人とする。
|
|
2042
|
+
const AGENT_SEND_LOCK_OWNER_SLACK_S = 5;
|
|
2043
|
+
function agentSendLockOwnerReplaced(probe) {
|
|
2044
|
+
if (!probe.live || probe.pid == null || probe.pid === process.pid)
|
|
2045
|
+
return false;
|
|
2046
|
+
const created = probe.at === null ? Number.NaN : Date.parse(probe.at);
|
|
2047
|
+
const lockAgeS = (Number.isFinite(created) ? Date.now() - created : probe.ageMs ?? 0) / 1000;
|
|
2048
|
+
let elapsed;
|
|
2049
|
+
try {
|
|
2050
|
+
elapsed = processElapsedSeconds(probe.pid);
|
|
2051
|
+
}
|
|
2052
|
+
catch {
|
|
2053
|
+
// 確かめられない時は、今まで通り生きていると読む(誤って片付けるより断る方に倒す)。
|
|
2054
|
+
return false;
|
|
2055
|
+
}
|
|
2056
|
+
return elapsed === null || elapsed + AGENT_SEND_LOCK_OWNER_SLACK_S < lockAgeS;
|
|
2057
|
+
}
|
|
2058
|
+
// 片付け役が印を持つのは一瞬。持ち主が終了しているか、鮮度猶予より古い印は、片付けの途中で残った物と読む。
|
|
2059
|
+
// 古さでも読むのは、終了した片付け役のpidを別のprocessが使い回すと、pidだけでは生きて見えるため。
|
|
2060
|
+
function agentSendReapMarkStuck(guard) {
|
|
2061
|
+
const probe = probeWaitLock(guard);
|
|
2062
|
+
return !probe.live || (probe.ageMs ?? 0) >= WAIT_LOCK_FRESH_MS;
|
|
2063
|
+
}
|
|
2037
2064
|
// 持ち主が終了したlockを片付ける。確かめてから消すまでの間に別のprocessが新しいlockを作ると生きたlockを消すので、
|
|
2038
2065
|
// 消す役を`.reap`の`O_EXCL`で1つに絞り、その中でもう一度確かめてから消す。lockが在る間は誰も新しいlockを作れない。
|
|
2039
|
-
//
|
|
2040
|
-
|
|
2066
|
+
// replacedTokenは、持ち主が別人と確かめたlockの印。pidが生きて見えるので、確かめた時と同じlockの時だけ消す。
|
|
2067
|
+
// done=片付けた(または片付ける物が無かった)/busy=別のprocessが片付け中/stuck=片付けの印が残っている
|
|
2068
|
+
function reapDeadAgentSendLock(name, replacedToken = null) {
|
|
2041
2069
|
const guard = agentSendReapPath(name);
|
|
2042
2070
|
const token = tryCreateOwnedLock(guard);
|
|
2043
2071
|
if (token === null)
|
|
2044
|
-
return
|
|
2072
|
+
return agentSendReapMarkStuck(guard) ? "stuck" : "busy";
|
|
2045
2073
|
try {
|
|
2046
|
-
|
|
2074
|
+
const probe = probeAgentSendLock(name);
|
|
2075
|
+
if (!probe.live || (replacedToken !== null && probe.token === replacedToken)) {
|
|
2047
2076
|
try {
|
|
2048
2077
|
fs.unlinkSync(agentSendLockPath(name));
|
|
2049
2078
|
}
|
|
@@ -2057,24 +2086,51 @@ function reapDeadAgentSendLock(name) {
|
|
|
2057
2086
|
}
|
|
2058
2087
|
return "done";
|
|
2059
2088
|
}
|
|
2089
|
+
// BellTeamのため: 先頭の「AGENT_SEND_BUSY: agent session '<名前>' は」と、文中の「文字列は送信していません。」で
|
|
2090
|
+
// 未送信の断りを見分けている(2026-10-05)。この2か所を変える時は、変える前に知らせる。
|
|
2060
2091
|
function agentSendBusyError(name, pid, waitMs) {
|
|
2061
2092
|
return new AitermError(`AGENT_SEND_BUSY: agent session '${name}' は${pid != null ? `別プロセス(pid ${pid})` : "先に受け付けた送信"}の処理中です。` +
|
|
2062
2093
|
`${waitMs}ms待っても順番が来ませんでした。文字列は送信していません。少し後で再度 pty_send してください。`, 2);
|
|
2063
2094
|
}
|
|
2064
2095
|
async function acquireAgentSendFileLock(name, deadline, waitMs) {
|
|
2065
2096
|
const p = agentSendLockPath(name);
|
|
2097
|
+
let checkedOwner = null;
|
|
2098
|
+
let checkedOwnerReplaced = false;
|
|
2066
2099
|
for (;;) {
|
|
2067
|
-
|
|
2100
|
+
let token;
|
|
2101
|
+
try {
|
|
2102
|
+
token = tryCreateOwnedLock(p);
|
|
2103
|
+
}
|
|
2104
|
+
catch (e) {
|
|
2105
|
+
// socketの置き場が無ければ、守る相手のsessionにも届かない。lockを取らずに進め、送信側の今まで通りの断りを返す。
|
|
2106
|
+
if (e.code === "ENOENT")
|
|
2107
|
+
return () => { };
|
|
2108
|
+
throw e;
|
|
2109
|
+
}
|
|
2068
2110
|
if (token !== null)
|
|
2069
2111
|
return () => releaseOwnedLock(p, token);
|
|
2070
2112
|
const probe = probeAgentSendLock(name);
|
|
2071
|
-
|
|
2113
|
+
let dead = !probe.live;
|
|
2114
|
+
let replacedToken = null;
|
|
2115
|
+
// 生きて見える持ち主は、初めて見た時に1回、本人かを確かめる。結果は覚え、待つ間にOSへ照会し直さない。
|
|
2116
|
+
if (!dead && probe.pid != null) {
|
|
2117
|
+
const owner = `${probe.pid}@${probe.at}`;
|
|
2118
|
+
if (owner !== checkedOwner) {
|
|
2119
|
+
checkedOwner = owner;
|
|
2120
|
+
checkedOwnerReplaced = agentSendLockOwnerReplaced(probe);
|
|
2121
|
+
}
|
|
2122
|
+
if (checkedOwnerReplaced) {
|
|
2123
|
+
dead = true;
|
|
2124
|
+
replacedToken = probe.token ?? null;
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (dead && reapDeadAgentSendLock(name, replacedToken) === "stuck") {
|
|
2072
2128
|
throw new AitermError(`agent session '${name}' に、終了した送信のlockを片付ける途中で残った印があります。` +
|
|
2073
2129
|
`自動回収は並行送信の混線を招くため行いません。文字列は送信していません。` +
|
|
2074
2130
|
`pty_listで対象を確認し、pty_closeでsessionを閉じてから同じIDで再作成してください`, 2);
|
|
2075
2131
|
}
|
|
2076
2132
|
if (performance.now() >= deadline)
|
|
2077
|
-
throw agentSendBusyError(name,
|
|
2133
|
+
throw agentSendBusyError(name, dead ? null : probe.pid, waitMs);
|
|
2078
2134
|
await sleep(AGENT_SEND_LOCK_POLL_MS);
|
|
2079
2135
|
}
|
|
2080
2136
|
}
|
|
@@ -2120,13 +2176,16 @@ async function withAgentSendLock(name, run) {
|
|
|
2120
2176
|
}
|
|
2121
2177
|
}
|
|
2122
2178
|
// pty_closeの掃除。生きた持ち主のlockは残す(その送信はsessionが無くなった所で失敗し、自分のlockを外す)。
|
|
2179
|
+
// 持ち主が終了したlock、pidを使い回されて生きて見えるlock、残った片付けの印は片付ける。
|
|
2123
2180
|
function cleanupAgentSendLock(name) {
|
|
2124
2181
|
const guard = agentSendReapPath(name);
|
|
2125
2182
|
try {
|
|
2126
|
-
if (fs.existsSync(guard) &&
|
|
2183
|
+
if (fs.existsSync(guard) && agentSendReapMarkStuck(guard))
|
|
2127
2184
|
fs.unlinkSync(guard);
|
|
2128
|
-
if (fs.existsSync(agentSendLockPath(name)))
|
|
2129
|
-
|
|
2185
|
+
if (!fs.existsSync(agentSendLockPath(name)))
|
|
2186
|
+
return;
|
|
2187
|
+
const probe = probeAgentSendLock(name);
|
|
2188
|
+
reapDeadAgentSendLock(name, agentSendLockOwnerReplaced(probe) ? probe.token ?? null : null);
|
|
2130
2189
|
}
|
|
2131
2190
|
catch {
|
|
2132
2191
|
/* 掃除の失敗で閉じる処理を止めない */
|
|
@@ -40,21 +40,15 @@ export function claudeParentHookDiagnostic(file) {
|
|
|
40
40
|
return unreadable;
|
|
41
41
|
if (settings.disableAllHooks === true)
|
|
42
42
|
return required("hooks_disabled");
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const scripts = [];
|
|
46
|
-
for (const event of Object.keys(steer.claudeParentHookEntries(AITERM_PROFILE, { command: "", script: entry }))) {
|
|
43
|
+
// 登録の形(0.52.2までの`args`形式と、今のshellを通す1行)は配送ライブラリが読む。ここで`args`を直接読まない。
|
|
44
|
+
for (const event of Object.keys(steer.claudeParentHookEntries(AITERM_PROFILE, { command: "", script: AITERM_PROFILE.hooks.claude }))) {
|
|
47
45
|
const groups = settings.hooks[event];
|
|
48
46
|
if (groups !== undefined && !Array.isArray(groups))
|
|
49
47
|
return unreadable;
|
|
50
|
-
const found = (groups ?? []).flatMap(group => record(group) && Array.isArray(group.hooks) ? group.hooks : [])
|
|
51
|
-
.filter(hook => record(hook) && hook.type === "command" && Array.isArray(hook.args) && typeof hook.args[0] === "string" && owned(hook.args[0]))
|
|
52
|
-
.map(hook => hook.args[0]);
|
|
53
|
-
if (found.length === 0)
|
|
54
|
-
return required("hooks_not_registered");
|
|
55
|
-
scripts.push(...found);
|
|
56
48
|
}
|
|
57
|
-
|
|
49
|
+
if (!steer.claudeParentHooksRegistered(AITERM_PROFILE, settings))
|
|
50
|
+
return required("hooks_not_registered");
|
|
51
|
+
return steer.claudeParentHookScripts(AITERM_PROFILE, settings).every(script => existsSync(script)) ? ready : required("hook_script_missing");
|
|
58
52
|
}
|
|
59
53
|
export function cursorParentHookDiagnostic(file) {
|
|
60
54
|
const document = readSettings(file);
|
package/dist/process-runtime.js
CHANGED
|
@@ -36,6 +36,7 @@ export function parsePosixProcessTable(text) {
|
|
|
36
36
|
const POSIX_PS = "/bin/ps";
|
|
37
37
|
const POSIX_TABLE_ARGS = ["-axww", "-o", "pid=,ppid=,pgid=,stat=,lstart=,time=,command="];
|
|
38
38
|
const POSIX_IDENTITY_ARGS = ["-o", "pid=,lstart=", "-p"];
|
|
39
|
+
const POSIX_ELAPSED_ARGS = ["-o", "etime=", "-p"];
|
|
39
40
|
const WINDOWS_PROBE_HEADER = ["$ErrorActionPreference='Stop'", "[Console]::OutputEncoding=[Text.UTF8Encoding]::new($false)"];
|
|
40
41
|
const WINDOWS_PROBE_ARGS = ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand"];
|
|
41
42
|
// 先頭2行だけのbase64。3 byteの区切りで切り、後ろの行に左右されない前方一致にする。
|
|
@@ -47,7 +48,8 @@ const WINDOWS_PROBE_PREFIX = (() => {
|
|
|
47
48
|
export function aitermProcessProbe(command) {
|
|
48
49
|
if (command.includes(` ${WINDOWS_PROBE_ARGS.join(" ")} ${WINDOWS_PROBE_PREFIX}`))
|
|
49
50
|
return true;
|
|
50
|
-
return command === `${POSIX_PS} ${POSIX_TABLE_ARGS.join(" ")}` || command.startsWith(`${POSIX_PS} ${POSIX_IDENTITY_ARGS.join(" ")} `)
|
|
51
|
+
return command === `${POSIX_PS} ${POSIX_TABLE_ARGS.join(" ")}` || command.startsWith(`${POSIX_PS} ${POSIX_IDENTITY_ARGS.join(" ")} `)
|
|
52
|
+
|| command.startsWith(`${POSIX_PS} ${POSIX_ELAPSED_ARGS.join(" ")} `);
|
|
51
53
|
}
|
|
52
54
|
/** Windowsのconsole host。console processの起動に付いて立ち、最後のconsole processが終わると消える。 */
|
|
53
55
|
export function windowsConsoleHost(row) {
|
|
@@ -157,6 +159,42 @@ export function readProcessIdentities(pids) {
|
|
|
157
159
|
return { pid: row.pid, started_identity: new Date(row.started_identity).toISOString() };
|
|
158
160
|
});
|
|
159
161
|
}
|
|
162
|
+
/** `ps -o etime=`の「[[日-]時:]分:秒」を秒にする。 */
|
|
163
|
+
export function parsePosixElapsed(value) {
|
|
164
|
+
const match = /^(?:(?:(\d+)-)?(\d+):)?(\d+):(\d+)$/.exec(value.trim());
|
|
165
|
+
if (!match)
|
|
166
|
+
return null;
|
|
167
|
+
return Number(match[1] ?? 0) * 86400 + Number(match[2] ?? 0) * 3600 + Number(match[3]) * 60 + Number(match[4]);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* processが始まってからの秒数。process表に無ければnull。OSへ照会できなかった時は例外にする。
|
|
171
|
+
* 開始時刻でなく経過で返す。POSIXのpsが出す開始時刻は、time zoneの設定と時計合わせで読みが変わる。
|
|
172
|
+
*/
|
|
173
|
+
export function processElapsedSeconds(pid) {
|
|
174
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
175
|
+
throw new AitermError("process照会のpidが不正です", 2);
|
|
176
|
+
if (isWin) {
|
|
177
|
+
const row = readProcessIdentities([pid])[0];
|
|
178
|
+
if (!row)
|
|
179
|
+
return null;
|
|
180
|
+
if (row.started_identity === null)
|
|
181
|
+
throw new AitermError("processの開始時刻を取得できません", 2);
|
|
182
|
+
return Math.max(0, (Date.now() - Date.parse(row.started_identity)) / 1000);
|
|
183
|
+
}
|
|
184
|
+
const result = spawnSync(POSIX_PS, [...POSIX_ELAPSED_ARGS, String(pid)], {
|
|
185
|
+
encoding: "utf8", env: { ...process.env, LC_ALL: "C" }, timeout: 10000,
|
|
186
|
+
});
|
|
187
|
+
const text = (result.stdout ?? "").trim();
|
|
188
|
+
// 該当するprocessが無い時、psは何も出さずstatus 1で終わる。
|
|
189
|
+
if (result.error || (result.status !== 0 && !(result.status === 1 && !text)))
|
|
190
|
+
throw new AitermError("OSのprocess一覧を取得できません", 2);
|
|
191
|
+
if (!text)
|
|
192
|
+
return null;
|
|
193
|
+
const elapsed = parsePosixElapsed(text);
|
|
194
|
+
if (elapsed === null)
|
|
195
|
+
throw new AitermError("processの経過時間の形式を認識できません", 2);
|
|
196
|
+
return elapsed;
|
|
197
|
+
}
|
|
160
198
|
// Windowsの親PIDは親の終了後も残り、別processへ再利用される。子より後に始まったprocessは
|
|
161
199
|
// 本当の親ではないので、親子関係として辿らない(辿ると循環や無関係なprocessの混入が起きる)。
|
|
162
200
|
export function parentProcess(row, byPid) {
|
package/docs/DESIGN.md
CHANGED
|
@@ -186,6 +186,11 @@ hookはNodeの実行ファイルと引数配列で直接起動し、shellやWind
|
|
|
186
186
|
子の予約は既存の`parent-deliveries/claims`で共有し、親の種類をまたぐ並行送信を防ぐ。
|
|
187
187
|
hookとの受け渡しはAiterm stateの`claude-parent-hooks`へ置く。
|
|
188
188
|
|
|
189
|
+
hookの登録は、`args`を使わずshellを通す1行で書く(ADR 0089)。POSIXは`exec '<node>' '<hook>'`、Windowsは
|
|
190
|
+
`& '<node>' '<hook>'; exit (Get-Variable LASTEXITCODE -ValueOnly)`と`shell: "powershell"`。同じ設定をGrokも読み、
|
|
191
|
+
`args`を落として`command`だけを動かすためである。Grokから起こされたhookは何もせず0で終わる。
|
|
192
|
+
PreToolUseのhookは、間に挟まるshellを飛ばして、Claude Code本体を依頼元のprocessとして記録する。
|
|
193
|
+
|
|
189
194
|
`PostToolUse`は`asyncRewake:true`で待機し、保存済み本文をstderrへ出してexit 2を返す。
|
|
190
195
|
exit 2はClaudeが規定する再開信号であり、子の成功・失敗は本文の`outcome`で区別する。
|
|
191
196
|
親は待機中も別作業や次のturnへ進める。Claudeの画面では`Stop hook feedback`として届く。
|
|
@@ -314,6 +319,8 @@ stale send lockは並行processとのABAを避けるため自動削除せず、
|
|
|
314
319
|
同じIDで再作成する。全session一括停止は公開しない。
|
|
315
320
|
agent sessionへの送信1本を守る`<name>.agent-send.lock`は持つ時間が長いので、持ち主が終了した残骸を次の送信が片付ける。消す役を`<name>.agent-send.lock.reap`の排他作成で1つに絞り、
|
|
316
321
|
その中でもう一度確かめてから消すので、確かめた後で別のlockに入れ替わらない。消す役の印そのものが残った時だけは自動で消さず、`pty_close`を案内する。
|
|
322
|
+
終了した持ち主のpidを別のprocessが使い回すと、pidだけでは生きて見える。生きて見える持ち主は、processが始まってからの経過とlockを作ってからの時間を比べ、
|
|
323
|
+
lockより後に始まったprocessなら別人と読んで片付ける。消す役の印は、5秒より古ければ残った物と読む。`pty_close`は、どちらも片付ける。
|
|
317
324
|
|
|
318
325
|
Grokのread-only sandbox起動拒否は、`src/harnesses/grok.ts`の
|
|
319
326
|
`assertGrokSandboxNotRejected`がCLIのエラー表示から検出する。`src/core.ts`の共通入力受付待機は
|
package/docs/RELEASE.md
CHANGED
|
@@ -83,8 +83,9 @@ aiterm-setup --json
|
|
|
83
83
|
互換launcherを共有していた場合はAitermのSteer選択だけを解除し、所有外の起動設定を保持する。
|
|
84
84
|
回答record schemaは従来と共通であり、新方式のSteer相当配送も`queued_submission_id`を保持する。
|
|
85
85
|
|
|
86
|
-
Claude親配送hook
|
|
87
|
-
実行する。Aiterm専用の3 hookだけを解除し、他製品のhookと設定を保持する。
|
|
86
|
+
Claude親配送hookのない旧版、または0.52.2以前へ戻す場合は、旧版のinstall前に`aiterm-setup --remove-claude-parent-hooks`を
|
|
87
|
+
実行する。Aiterm専用の3 hookだけを解除し、他製品のhookと設定を保持する。0.52.2以前は、shellを通す1行の登録(ADR 0089)を
|
|
88
|
+
自分の物と数えず、もう1組を足す。
|
|
88
89
|
|
|
89
90
|
```bash
|
|
90
91
|
npm install -g "aiterm-mcp@<known-good-version>"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.3",
|
|
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": [
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
},
|
|
72
72
|
"dependencies": {
|
|
73
73
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
74
|
-
"aiterm-steer-delivery": "^0.
|
|
74
|
+
"aiterm-steer-delivery": "^0.2.0",
|
|
75
75
|
"ws": "8.21.3",
|
|
76
76
|
"zod": "^4.4.3"
|
|
77
77
|
},
|