aiterm-mcp 0.52.0 → 0.52.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 +35 -1
- package/README.ja.md +3 -1
- package/README.md +3 -1
- package/dist/core.js +251 -8
- package/dist/process-runtime.js +39 -1
- package/docs/DESIGN.md +7 -0
- package/package.json +1 -1
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.52.2] - 2026-10-05
|
|
11
|
+
|
|
12
|
+
### 修正
|
|
13
|
+
|
|
14
|
+
- 0.52.1で、socketの置き場(`claude-tmux-sockets`)ごと無くなったagent sessionへの送信が、
|
|
15
|
+
`ENOENT: … .agent-send.lock`という生のエラーを返していた。0.52.0までは
|
|
16
|
+
「入力受付状態になりません。文字列は送信していません。」の断りだった。連携元が「文字列は送信していません」で
|
|
17
|
+
未送信を見分けている時、0.52.1ではこの場合だけ見分けられなかった。送信の順番を守るlock(ADR 0088)を
|
|
18
|
+
置き場に作れない時は、lockを取らずに進め、今まで通りの断りを返す。置き場が無ければsessionにも届かず、
|
|
19
|
+
守る相手が無い。sessionだけが無くなった時(置き場は残る)の断りは、0.52.1でも変わっていない。
|
|
20
|
+
- 0.52.1で、送信の途中で終了したprocessのpidを別の生きたprocessが使い回すと、そのagent sessionへの送信が
|
|
21
|
+
`AGENT_SEND_BUSY`で断られ続けた。lockの持ち主をpidだけで見ていたため。`pty_close`してもlockが残り、
|
|
22
|
+
閉じて同じIDで起動し直しても同じ断りだった。生きて見える持ち主は、processが始まってからの経過と
|
|
23
|
+
lockを作ってからの時間を比べて本人かを確かめ、lockより後に始まった別のprocessなら片付ける。
|
|
24
|
+
片付けの途中で残った印は、5秒より古ければ、pidが生きて見えても残った物と読む(その時の断りは今までと同じ文面)。
|
|
25
|
+
`pty_close`は、使い回されたpidのlockと古い印も片付ける。OSへの照会は、送信が実際に重なった時と、
|
|
26
|
+
閉じる時にlockが残っていた時だけ行う。
|
|
27
|
+
|
|
28
|
+
## [0.52.1] - 2026-10-05
|
|
29
|
+
|
|
30
|
+
### 修正
|
|
31
|
+
|
|
32
|
+
- 同じagent sessionへの`pty_send`が重なると、後の1通が断られる事があった(ADR 0088)。Aitermは送る時点の子の状態を見て
|
|
33
|
+
差し込みか新しいturnかを決めるが、見てから送り終えるまでの間に起動直後の入力受付待ち(最長30秒)が挟まり、
|
|
34
|
+
その区間を守るものが無かった。Claude Codeでは2通ともturnの印が無いと読み、後の1通が
|
|
35
|
+
「operation_idなしのClaude turn が未解決です」で断られた(文は打たれない)。Codex・Grok・Cursorでは断りにならず、
|
|
36
|
+
同じ入力欄へ2通が続けて貼られ、1行につながって届いた。同じsessionへの送信(`pty_send`、`claude_turn issue`、起動時prompt)を
|
|
37
|
+
1本ずつ通し、後の1通は前の1通が終わってから振り分け直す。順に送った時と同じ結果になり、Claude Codeへ重なった後の1通は
|
|
38
|
+
`mode=agent_steer`で返る。前の1通を待つ分だけ返りが遅れる。待つ上限(POSIX 60秒、Windows 180秒)を越えたら、
|
|
39
|
+
打鍵前に`AGENT_SEND_BUSY`と「文字列は送信していません」を返す。送信の途中で終了したprocessのlockは、次の送信が片付ける。
|
|
40
|
+
公開toolの返却形式とstate schemaは変えていない。旧版へ戻す時の手当ては要らない。
|
|
41
|
+
|
|
10
42
|
## [0.52.0] - 2026-10-04
|
|
11
43
|
|
|
12
44
|
### 修正
|
|
@@ -2103,7 +2135,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2103
2135
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2104
2136
|
provenance.
|
|
2105
2137
|
|
|
2106
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.52.
|
|
2138
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.52.2...HEAD
|
|
2139
|
+
[0.52.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.52.1...v0.52.2
|
|
2140
|
+
[0.52.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.52.0...v0.52.1
|
|
2107
2141
|
[0.52.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.3...v0.52.0
|
|
2108
2142
|
[0.51.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.2...v0.51.3
|
|
2109
2143
|
[0.51.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.51.1...v0.51.2
|
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.2** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -261,6 +261,8 @@ ClaudeがAPIエラーや安全判定の拒否で終了した時は、Stop hook
|
|
|
261
261
|
|
|
262
262
|
agent配送だけを期待する連携では、`pty_send`に`require_agent:true`を指定する。agent登録が無ければ、打鍵前に`AGENT_SESSION_REQUIRED`と「文字列は送信していません」を返す。省略時は通常PTY送信を維持する。`force:true`との併用は未送信で拒否する。
|
|
263
263
|
|
|
264
|
+
同じagent sessionへの`pty_send`が重なった時は、1本ずつ通す。後の1通は、前の1通を送り終えてから振り分け直すので、順に送った時と同じ結果になる(起動直後のClaude Codeへ重なった後の1通は`mode=agent_steer`で返る)。前の1通が起動直後の入力受付待ちなら、その分だけ返りが遅れる。待つ上限(POSIX 60秒、Windows 180秒)を越えた時は、打鍵前に`AGENT_SEND_BUSY`と「文字列は送信していません」を返す。送信の途中で終了したprocessのlockは、次の送信が片付けて進む。
|
|
265
|
+
|
|
264
266
|
`agent_launch`・`pty_send`(agent session宛て)は任意の`image`(画像ファイルの絶対パスの配列。png/jpg/jpeg/gif/webp)を受ける。aitermが本文末尾へ添付行を付け、どのharnessも自分のfile読取toolでそのpathを画像として開く。呼出し側はharness別の添付手順を覚えない。不正なpathは送信前に拒否する。
|
|
265
267
|
|
|
266
268
|
`agent_launch`は任意の`write_scope`も受ける。Codex/Grokのread-onlyは`--sandbox read-only`、Cursorは公式`--mode ask`で実効化する。path説明は同等CLI引数がないためdeclaration-only。
|
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.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
|
|
|
@@ -282,6 +282,8 @@ The same primitive hosts another agent's TUI. `agent_launch` starts a selected e
|
|
|
282
282
|
|
|
283
283
|
Set `require_agent:true` on `pty_send` when the integration requires agent delivery. If the agent registration is missing, Aiterm refuses before sending any text and returns `AGENT_SESSION_REQUIRED` with an explicit unsent message. The default preserves ordinary PTY sends; combining it with `force:true` is rejected before sending.
|
|
284
284
|
|
|
285
|
+
Overlapping `pty_send` calls to the same agent session are handled one at a time. A later call picks its route only after the earlier one has been sent, so the result matches calls made in sequence (a call that overlaps the first send to a freshly started Claude Code session returns `mode=agent_steer`). It returns later by however long the earlier call waits for the session to accept input. If its turn does not come within the limit (60 seconds on POSIX, 180 seconds on Windows), Aiterm refuses before sending any text and returns `AGENT_SEND_BUSY` with an explicit unsent message. A lock left by a process that exited mid-send is cleared by the next send.
|
|
286
|
+
|
|
285
287
|
`agent_launch` and `pty_send` (to an agent session) accept an optional `image`: an array of absolute paths to image files (png/jpg/jpeg/gif/webp). Aiterm appends an attachment block to the prompt, and every harness opens the path with its own file-reading tool and sees the image; the caller never learns harness-specific attachment tricks. Invalid paths are rejected before anything is sent.
|
|
286
288
|
|
|
287
289
|
`agent_launch` accepts an optional `write_scope`: either `"read-only"` or a human-readable description of writable paths. Codex/Grok use `--sandbox read-only`; Cursor uses its official read-only `--mode ask`. A path description remains declaration-only because these CLI launch surfaces provide no equivalent path allowlist flag.
|
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";
|
|
@@ -86,6 +86,10 @@ const PTY_PASTE_CHUNK_PAUSE_MS = 10;
|
|
|
86
86
|
const PTY_PASTE_PAUSE_BUFFER = new Int32Array(new SharedArrayBuffer(4));
|
|
87
87
|
const SESSION_SEND_LOCK_WAIT_MS = isWin ? 130_000 : 10_000;
|
|
88
88
|
const SESSION_SEND_LOCK_POLL_MS = 25;
|
|
89
|
+
// 同じagent sessionへの送信が重なった時に、前の1通を待つ上限(ADR 0088)。
|
|
90
|
+
// 起動から最初の描画を待つ上限と、貼り付けの上限の和。前の1通が正常に進んでいる間は待ち切れる長さにする。
|
|
91
|
+
const AGENT_SEND_LOCK_WAIT_MS = AGENT_TUI_FIRST_DRAW_TIMEOUT_MS + SESSION_SEND_LOCK_WAIT_MS;
|
|
92
|
+
const AGENT_SEND_LOCK_POLL_MS = 50;
|
|
89
93
|
// CSI/OSC/ESC エスケープ・制御文字
|
|
90
94
|
// CSI / OSC(BEL or ST 終端) / DCS・PM・APC・SOS(ESC P/^/_/X … BEL or ST 終端。ペイロード本文ごと除去=B10) / 残る2文字エスケープ
|
|
91
95
|
const ANSI_RE = /\x1b\[[0-9;?]*[ -\/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[P^_X][\s\S]*?(?:\x07|\x1b\\)|\x1b[@-_]/g;
|
|
@@ -349,6 +353,15 @@ function sendLockPath(name) {
|
|
|
349
353
|
assertSessionName(name);
|
|
350
354
|
return path.join(SOCKDIR, name + ".send.lock");
|
|
351
355
|
}
|
|
356
|
+
// agent sessionへの送信1本(振り分け〜Enterの確認)を守るlock。貼り付け1回ぶんの`.send.lock`とは別に持つ。
|
|
357
|
+
function agentSendLockPath(name) {
|
|
358
|
+
assertSessionName(name);
|
|
359
|
+
return path.join(SOCKDIR, name + ".agent-send.lock");
|
|
360
|
+
}
|
|
361
|
+
// 持ち主が終了した`.agent-send.lock`を片付ける役の印。
|
|
362
|
+
function agentSendReapPath(name) {
|
|
363
|
+
return agentSendLockPath(name) + ".reap";
|
|
364
|
+
}
|
|
352
365
|
function readOffset(name) {
|
|
353
366
|
try {
|
|
354
367
|
const n = parseInt(fs.readFileSync(offsetpath(name), "utf8").trim(), 10);
|
|
@@ -1345,6 +1358,7 @@ function closeSessionInternal(name, observeDependency = true) {
|
|
|
1345
1358
|
/* noop */
|
|
1346
1359
|
}
|
|
1347
1360
|
}
|
|
1361
|
+
cleanupAgentSendLock(name);
|
|
1348
1362
|
cleanupAgentState(name);
|
|
1349
1363
|
agentLaunchLines.delete(name);
|
|
1350
1364
|
return `closed ${name}`;
|
|
@@ -1390,7 +1404,7 @@ export function killAll() {
|
|
|
1390
1404
|
/* socketの置き場が無い */
|
|
1391
1405
|
}
|
|
1392
1406
|
for (const file of socketFiles) {
|
|
1393
|
-
const name = file.replace(/\.(?:log|offset|lastcmd|mark|send\.lock)$/, "");
|
|
1407
|
+
const name = file.replace(/\.(?:log|offset|lastcmd|mark|send\.lock|agent-send\.lock(?:\.reap)?)$/, "");
|
|
1394
1408
|
if (name !== file && /^[A-Za-z0-9_-]{1,64}$/.test(name))
|
|
1395
1409
|
sessions.add(name);
|
|
1396
1410
|
}
|
|
@@ -1413,10 +1427,10 @@ export function killAll() {
|
|
|
1413
1427
|
if (killed.code !== 0 && !/no server running|No such file or directory|failed to connect/i.test(killed.stderr)) {
|
|
1414
1428
|
throw new AitermError("このsocketのserverを終了できません: " + killed.stderr.trim(), 2);
|
|
1415
1429
|
}
|
|
1416
|
-
// B9: SOCKDIR 内の .log/.offset/.lastcmd/.mark/.send.lock 残骸も掃除する。
|
|
1430
|
+
// B9: SOCKDIR 内の .log/.offset/.lastcmd/.mark/.send.lock/.agent-send.lock 残骸も掃除する。
|
|
1417
1431
|
try {
|
|
1418
1432
|
for (const f of fs.readdirSync(SOCKDIR)) {
|
|
1419
|
-
if (/\.(log|offset|lastcmd|mark)$/.test(f) || f.endsWith(".send.lock")) {
|
|
1433
|
+
if (/\.(log|offset|lastcmd|mark)$/.test(f) || f.endsWith(".send.lock") || /\.agent-send\.lock(?:\.reap)?$/.test(f)) {
|
|
1420
1434
|
try {
|
|
1421
1435
|
fs.unlinkSync(path.join(SOCKDIR, f));
|
|
1422
1436
|
}
|
|
@@ -1846,6 +1860,7 @@ function isPidAlive(pid) {
|
|
|
1846
1860
|
function probeWaitLock(p) {
|
|
1847
1861
|
let pid = null;
|
|
1848
1862
|
let at = null;
|
|
1863
|
+
let token = null;
|
|
1849
1864
|
let ageMs = 0;
|
|
1850
1865
|
try {
|
|
1851
1866
|
const st = fs.lstatSync(p);
|
|
@@ -1857,15 +1872,17 @@ function probeWaitLock(p) {
|
|
|
1857
1872
|
pid = v.pid;
|
|
1858
1873
|
if (typeof v.at === "string")
|
|
1859
1874
|
at = v.at;
|
|
1875
|
+
if (typeof v.token === "string")
|
|
1876
|
+
token = v.token;
|
|
1860
1877
|
}
|
|
1861
1878
|
catch {
|
|
1862
1879
|
/* pid 不明のまま鮮度判定に落ちる */
|
|
1863
1880
|
}
|
|
1864
1881
|
if (pid == null)
|
|
1865
|
-
return { pid: null, at, live: ageMs < WAIT_LOCK_FRESH_MS };
|
|
1882
|
+
return { pid: null, at, token, ageMs, live: ageMs < WAIT_LOCK_FRESH_MS };
|
|
1866
1883
|
if (pid === process.pid)
|
|
1867
|
-
return { pid, at, live: false };
|
|
1868
|
-
return { pid, at, live: isPidAlive(pid) };
|
|
1884
|
+
return { pid, at, token, ageMs, live: false };
|
|
1885
|
+
return { pid, at, token, ageMs, live: isPidAlive(pid) };
|
|
1869
1886
|
}
|
|
1870
1887
|
function unlinkStaleWaitLock(p) {
|
|
1871
1888
|
try {
|
|
@@ -1965,6 +1982,218 @@ function acquireSessionSendFileLock(name) {
|
|
|
1965
1982
|
}
|
|
1966
1983
|
};
|
|
1967
1984
|
}
|
|
1985
|
+
// ── 同じagent sessionへの送信を1本ずつ通す(ADR 0088) ────
|
|
1986
|
+
// 振り分け(差し込みか新しいturnか)から送り終えるまでの間に、入力受付待ちなどの待ちが挟まる。
|
|
1987
|
+
// その間に次の1通が同じ状態を読むと、Claudeはturnの印の取り合いで後の1通が断られ、他のharnessは同じ入力欄へ2回貼る。
|
|
1988
|
+
// 同じprocessの中は着いた順の待ち行列、別のprocessの間はfileで排他し、後の1通は前の1通が終わってから振り分け直す。
|
|
1989
|
+
const agentSendQueues = new Map();
|
|
1990
|
+
const agentSendHeld = new Set();
|
|
1991
|
+
let agentSendLockWaitMs = AGENT_SEND_LOCK_WAIT_MS;
|
|
1992
|
+
export function __testSetAgentSendLockWaitMs(value) {
|
|
1993
|
+
if (value !== null && (!Number.isSafeInteger(value) || value < 0))
|
|
1994
|
+
throw new Error("wait must be a non-negative integer");
|
|
1995
|
+
agentSendLockWaitMs = value ?? AGENT_SEND_LOCK_WAIT_MS;
|
|
1996
|
+
}
|
|
1997
|
+
// `O_EXCL`で作れた時だけ持ち主になる。既に在れば null。
|
|
1998
|
+
function tryCreateOwnedLock(p) {
|
|
1999
|
+
const nofollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
2000
|
+
let fd;
|
|
2001
|
+
try {
|
|
2002
|
+
fd = fs.openSync(p, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | nofollow, 0o600);
|
|
2003
|
+
}
|
|
2004
|
+
catch (e) {
|
|
2005
|
+
const code = e.code;
|
|
2006
|
+
// Windowsは、消している途中のfile(他のprocessがまだ開いている)と同じ名前を作るとEPERM/EACCESになる。
|
|
2007
|
+
if (code === "EEXIST" || (isWin && (code === "EPERM" || code === "EACCES")))
|
|
2008
|
+
return null;
|
|
2009
|
+
throw e;
|
|
2010
|
+
}
|
|
2011
|
+
const token = randomBytes(16).toString("hex");
|
|
2012
|
+
try {
|
|
2013
|
+
// 書けなかったlockは中身が読めないまま残り、鮮度猶予の後に持ち主なしとして片付く。
|
|
2014
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, at: new Date().toISOString(), token }) + "\n", "utf8");
|
|
2015
|
+
}
|
|
2016
|
+
finally {
|
|
2017
|
+
fs.closeSync(fd);
|
|
2018
|
+
}
|
|
2019
|
+
return token;
|
|
2020
|
+
}
|
|
2021
|
+
function releaseOwnedLock(p, token) {
|
|
2022
|
+
try {
|
|
2023
|
+
const st = fs.lstatSync(p);
|
|
2024
|
+
if (!st.isFile() || st.isSymbolicLink())
|
|
2025
|
+
return;
|
|
2026
|
+
const current = JSON.parse(fs.readFileSync(p, "utf8").split("\n", 1)[0]);
|
|
2027
|
+
if (current.token === token)
|
|
2028
|
+
fs.unlinkSync(p);
|
|
2029
|
+
}
|
|
2030
|
+
catch {
|
|
2031
|
+
/* 別の持ち主のlockや置換済みpathは消さない */
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
// 同じprocessの送信は待ち行列で1本になっているので、自分のpidのlockは外し損ねた残骸と読める(probeWaitLock)。
|
|
2035
|
+
// このprocessが持っている最中のlockだけは、その読みから外す。
|
|
2036
|
+
function probeAgentSendLock(name) {
|
|
2037
|
+
const probe = probeWaitLock(agentSendLockPath(name));
|
|
2038
|
+
return probe.pid === process.pid && agentSendHeld.has(name) ? { ...probe, live: true } : probe;
|
|
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
|
+
}
|
|
2064
|
+
// 持ち主が終了したlockを片付ける。確かめてから消すまでの間に別のprocessが新しいlockを作ると生きたlockを消すので、
|
|
2065
|
+
// 消す役を`.reap`の`O_EXCL`で1つに絞り、その中でもう一度確かめてから消す。lockが在る間は誰も新しいlockを作れない。
|
|
2066
|
+
// replacedTokenは、持ち主が別人と確かめたlockの印。pidが生きて見えるので、確かめた時と同じlockの時だけ消す。
|
|
2067
|
+
// done=片付けた(または片付ける物が無かった)/busy=別のprocessが片付け中/stuck=片付けの印が残っている
|
|
2068
|
+
function reapDeadAgentSendLock(name, replacedToken = null) {
|
|
2069
|
+
const guard = agentSendReapPath(name);
|
|
2070
|
+
const token = tryCreateOwnedLock(guard);
|
|
2071
|
+
if (token === null)
|
|
2072
|
+
return agentSendReapMarkStuck(guard) ? "stuck" : "busy";
|
|
2073
|
+
try {
|
|
2074
|
+
const probe = probeAgentSendLock(name);
|
|
2075
|
+
if (!probe.live || (replacedToken !== null && probe.token === replacedToken)) {
|
|
2076
|
+
try {
|
|
2077
|
+
fs.unlinkSync(agentSendLockPath(name));
|
|
2078
|
+
}
|
|
2079
|
+
catch {
|
|
2080
|
+
/* 既に無い */
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
finally {
|
|
2085
|
+
releaseOwnedLock(guard, token);
|
|
2086
|
+
}
|
|
2087
|
+
return "done";
|
|
2088
|
+
}
|
|
2089
|
+
// BellTeamのため: 先頭の「AGENT_SEND_BUSY: agent session '<名前>' は」と、文中の「文字列は送信していません。」で
|
|
2090
|
+
// 未送信の断りを見分けている(2026-10-05)。この2か所を変える時は、変える前に知らせる。
|
|
2091
|
+
function agentSendBusyError(name, pid, waitMs) {
|
|
2092
|
+
return new AitermError(`AGENT_SEND_BUSY: agent session '${name}' は${pid != null ? `別プロセス(pid ${pid})` : "先に受け付けた送信"}の処理中です。` +
|
|
2093
|
+
`${waitMs}ms待っても順番が来ませんでした。文字列は送信していません。少し後で再度 pty_send してください。`, 2);
|
|
2094
|
+
}
|
|
2095
|
+
async function acquireAgentSendFileLock(name, deadline, waitMs) {
|
|
2096
|
+
const p = agentSendLockPath(name);
|
|
2097
|
+
let checkedOwner = null;
|
|
2098
|
+
let checkedOwnerReplaced = false;
|
|
2099
|
+
for (;;) {
|
|
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
|
+
}
|
|
2110
|
+
if (token !== null)
|
|
2111
|
+
return () => releaseOwnedLock(p, token);
|
|
2112
|
+
const probe = probeAgentSendLock(name);
|
|
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") {
|
|
2128
|
+
// BellTeamのため: 先頭の「agent session '<名前>' に、終了した送信のlockを片付ける途中で残った印があります。」と、
|
|
2129
|
+
// 文中の「文字列は送信していません。」で見分け、pty_close→起動し直しへ回している(2026-10-05)。
|
|
2130
|
+
// この文面と、pty_closeがこの印を片付ける事を変える時は、変える前に知らせる。
|
|
2131
|
+
throw new AitermError(`agent session '${name}' に、終了した送信のlockを片付ける途中で残った印があります。` +
|
|
2132
|
+
`自動回収は並行送信の混線を招くため行いません。文字列は送信していません。` +
|
|
2133
|
+
`pty_listで対象を確認し、pty_closeでsessionを閉じてから同じIDで再作成してください`, 2);
|
|
2134
|
+
}
|
|
2135
|
+
if (performance.now() >= deadline)
|
|
2136
|
+
throw agentSendBusyError(name, dead ? null : probe.pid, waitMs);
|
|
2137
|
+
await sleep(AGENT_SEND_LOCK_POLL_MS);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
async function withAgentSendLock(name, run) {
|
|
2141
|
+
const waitMs = agentSendLockWaitMs;
|
|
2142
|
+
const deadline = performance.now() + waitMs;
|
|
2143
|
+
const previous = agentSendQueues.get(name) ?? Promise.resolve();
|
|
2144
|
+
let leave;
|
|
2145
|
+
const mine = new Promise((resolve) => {
|
|
2146
|
+
leave = resolve;
|
|
2147
|
+
});
|
|
2148
|
+
// 待ち切れずに抜けた送信の後ろも、前の送信が終わるまで待つ。行列の控えは、ここまでの送信が全部終わってから消す。
|
|
2149
|
+
const tail = previous.then(() => mine);
|
|
2150
|
+
agentSendQueues.set(name, tail);
|
|
2151
|
+
void tail.then(() => {
|
|
2152
|
+
if (agentSendQueues.get(name) === tail)
|
|
2153
|
+
agentSendQueues.delete(name);
|
|
2154
|
+
});
|
|
2155
|
+
let timer;
|
|
2156
|
+
try {
|
|
2157
|
+
const myTurn = await Promise.race([
|
|
2158
|
+
previous.then(() => true),
|
|
2159
|
+
new Promise((resolve) => {
|
|
2160
|
+
timer = setTimeout(() => resolve(false), waitMs);
|
|
2161
|
+
}),
|
|
2162
|
+
]);
|
|
2163
|
+
clearTimeout(timer);
|
|
2164
|
+
if (!myTurn)
|
|
2165
|
+
throw agentSendBusyError(name, null, waitMs);
|
|
2166
|
+
const release = await acquireAgentSendFileLock(name, deadline, waitMs);
|
|
2167
|
+
agentSendHeld.add(name);
|
|
2168
|
+
try {
|
|
2169
|
+
return await run();
|
|
2170
|
+
}
|
|
2171
|
+
finally {
|
|
2172
|
+
agentSendHeld.delete(name);
|
|
2173
|
+
release();
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
finally {
|
|
2177
|
+
clearTimeout(timer);
|
|
2178
|
+
leave();
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
// pty_closeの掃除。生きた持ち主のlockは残す(その送信はsessionが無くなった所で失敗し、自分のlockを外す)。
|
|
2182
|
+
// 持ち主が終了したlock、pidを使い回されて生きて見えるlock、残った片付けの印は片付ける。
|
|
2183
|
+
function cleanupAgentSendLock(name) {
|
|
2184
|
+
const guard = agentSendReapPath(name);
|
|
2185
|
+
try {
|
|
2186
|
+
if (fs.existsSync(guard) && agentSendReapMarkStuck(guard))
|
|
2187
|
+
fs.unlinkSync(guard);
|
|
2188
|
+
if (!fs.existsSync(agentSendLockPath(name)))
|
|
2189
|
+
return;
|
|
2190
|
+
const probe = probeAgentSendLock(name);
|
|
2191
|
+
reapDeadAgentSendLock(name, agentSendLockOwnerReplaced(probe) ? probe.token ?? null : null);
|
|
2192
|
+
}
|
|
2193
|
+
catch {
|
|
2194
|
+
/* 掃除の失敗で閉じる処理を止めない */
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
1968
2197
|
// close/killAll 用: 生きた別プロセス待機の wait lock を列挙する(stale 残骸は数えない)。
|
|
1969
2198
|
function liveWaitLocks(name) {
|
|
1970
2199
|
const dir = existingAgentsDir();
|
|
@@ -3291,6 +3520,11 @@ async function prepareAgentInput(name, meta, options) {
|
|
|
3291
3520
|
}
|
|
3292
3521
|
export async function sendInitialAgentPrompt(name, text, o = {}) {
|
|
3293
3522
|
assertSessionName(name);
|
|
3523
|
+
loadAgentMetadata(name);
|
|
3524
|
+
// 起動時promptの準備中に届いたpty_sendは、起動時promptを送り終えてから振り分ける(ADR 0088)。
|
|
3525
|
+
return withAgentSendLock(name, () => sendInitialAgentPromptInLock(name, text, o));
|
|
3526
|
+
}
|
|
3527
|
+
async function sendInitialAgentPromptInLock(name, text, o) {
|
|
3294
3528
|
const meta = loadAgentMetadata(name);
|
|
3295
3529
|
if (meta.initial_prompt === "done") {
|
|
3296
3530
|
throw new AitermError(`agent session '${name}' の起動時 prompt は既に完了しています`, 2);
|
|
@@ -3893,6 +4127,10 @@ async function assertCursorPromptNotHookBlocked(name) {
|
|
|
3893
4127
|
}
|
|
3894
4128
|
export async function dispatchAgentTurn(name, text, o = {}) {
|
|
3895
4129
|
assertSessionName(name);
|
|
4130
|
+
loadAgentMetadata(name);
|
|
4131
|
+
return withAgentSendLock(name, () => dispatchAgentTurnInLock(name, text, o));
|
|
4132
|
+
}
|
|
4133
|
+
async function dispatchAgentTurnInLock(name, text, o) {
|
|
3896
4134
|
const meta = loadAgentMetadata(name);
|
|
3897
4135
|
const operationId = o.operation_id == null ? null : validateOperationId(o.operation_id);
|
|
3898
4136
|
if (operationId && meta.kind !== "claude") {
|
|
@@ -4040,9 +4278,14 @@ export function __testSteerQueued(kind, screen) {
|
|
|
4040
4278
|
* agent sessionへの唯一の送信口。呼び出し側は子の状態を知らないまま呼び、Aitermがこの時点の画面で振り分ける。
|
|
4041
4279
|
* 実行中なら現在のturnへ差し込み(steer)、そうでなければ新しいturnとしてdispatchする。
|
|
4042
4280
|
* 振り分けを呼び出し側へ任せると、状態を見てから呼ぶまでの間に子のturnが変わり、選んだ入口が外れる。
|
|
4281
|
+
* 同じsessionへの送信は1本ずつ通し、重なった後の1通は前の1通が終わってから振り分け直す(ADR 0088)。
|
|
4043
4282
|
*/
|
|
4044
4283
|
export async function sendAgentMessage(name, text, o = {}) {
|
|
4045
4284
|
assertSessionName(name);
|
|
4285
|
+
loadAgentMetadata(name);
|
|
4286
|
+
return withAgentSendLock(name, () => sendAgentMessageInLock(name, text, o));
|
|
4287
|
+
}
|
|
4288
|
+
async function sendAgentMessageInLock(name, text, o) {
|
|
4046
4289
|
const meta = loadAgentMetadata(name);
|
|
4047
4290
|
// 前面回復は busy 判定より先(bash 前面のままだと画面の実行中マーカーを読んでも打鍵が届かない)。
|
|
4048
4291
|
const paneInputRecovery = await ensureAgentOwnsPaneInput(name, meta.kind);
|
|
@@ -4062,7 +4305,7 @@ export async function sendAgentMessage(name, text, o = {}) {
|
|
|
4062
4305
|
if (running) {
|
|
4063
4306
|
return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery });
|
|
4064
4307
|
}
|
|
4065
|
-
return
|
|
4308
|
+
return dispatchAgentTurnInLock(name, text, { raw: o.raw, before_send: o.before_send, pane_input_recovery: paneInputRecovery });
|
|
4066
4309
|
}
|
|
4067
4310
|
/**
|
|
4068
4311
|
* 実行中turnへ追加の指示を渡す。各harnessの標準操作だけを使い、完了境界は1つに保つ。
|
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
|
@@ -95,6 +95,9 @@ agent sessionへの送信口は`pty_send`だけとする。子の状態は呼び
|
|
|
95
95
|
実行中なら現在のturnへ追加textを差し込み、新しい完了境界と配送は作らない。完了境界は差し込み後も1つに保つ。
|
|
96
96
|
Claude Codeはtool処理中に画面の実行中表示が消え、Stop hookの実行中には表示が残る。turnの印を実行中判定の正本とする。Stopが発火しないAPIエラー終了では、次の送信時に印の作成後の会話記録にある`isApiErrorMessage`を確認し、終了したturnの印だけを解除する。過去のエラーで新しい印を解除しない。waiterは印を変更せず、読取専用のままエラーを返す。
|
|
97
97
|
それ以外は新しいturnとしてdispatchする。
|
|
98
|
+
振り分けから送り終えるまでの間には、起動直後の入力受付待ちなどの待ちが挟まる。同じsessionへの送信(`pty_send`、`claude_turn issue`、起動時prompt)は1本ずつ通し、
|
|
99
|
+
重なった後の1通は前の1通が終わってから振り分け直す。同じprocessの中は着いた順の待ち行列、別のprocessの間はsocketの置き場の`<name>.agent-send.lock`で排他する。
|
|
100
|
+
待つ上限は、起動から最初の描画を待つ上限と貼り付けの上限の和で、越えたら`AGENT_SEND_BUSY`で未送信を返す。判断の記録はADR 0088(`docs/adr/0088-one-agent-send-per-session.md`)。
|
|
98
101
|
CodexとClaude Codeは次のtool境界で同じturnへ取り込む。Cursorは「follow-ups」枠へ入った文を「enter steer」で現在turnへ移し、`turn_ended`は最後に1回書く。
|
|
99
102
|
待ち行列の表示は5秒待ち、子のturnが続いている間だけ30秒まで延ばす(表示が遅い端末がある。turnが終わっていれば、文は新しいturnとして始まっている)。
|
|
100
103
|
Grokは待ち行列へ入れた後に「send now」を押す。旧turnは`cancelled`(`cancellation_context.trigger=send_now`)で閉じ、
|
|
@@ -309,6 +312,10 @@ ADR 0074(repositoryの`docs/adr/0074-agent-model-catalog.md`)。
|
|
|
309
312
|
境界失敗は明示errorにする。retry、別model、別harness、別backendへ自動fallbackしない。
|
|
310
313
|
stale send lockは並行processとのABAを避けるため自動削除せず、公開APIでは対象sessionを`pty_close`して
|
|
311
314
|
同じIDで再作成する。全session一括停止は公開しない。
|
|
315
|
+
agent sessionへの送信1本を守る`<name>.agent-send.lock`は持つ時間が長いので、持ち主が終了した残骸を次の送信が片付ける。消す役を`<name>.agent-send.lock.reap`の排他作成で1つに絞り、
|
|
316
|
+
その中でもう一度確かめてから消すので、確かめた後で別のlockに入れ替わらない。消す役の印そのものが残った時だけは自動で消さず、`pty_close`を案内する。
|
|
317
|
+
終了した持ち主のpidを別のprocessが使い回すと、pidだけでは生きて見える。生きて見える持ち主は、processが始まってからの経過とlockを作ってからの時間を比べ、
|
|
318
|
+
lockより後に始まったprocessなら別人と読んで片付ける。消す役の印は、5秒より古ければ残った物と読む。`pty_close`は、どちらも片付ける。
|
|
312
319
|
|
|
313
320
|
Grokのread-only sandbox起動拒否は、`src/harnesses/grok.ts`の
|
|
314
321
|
`assertGrokSandboxNotRejected`がCLIのエラー表示から検出する。`src/core.ts`の共通入力受付待機は
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.52.
|
|
3
|
+
"version": "0.52.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": [
|