aiterm-mcp 0.48.0 → 0.50.0
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 +37 -1
- package/README.ja.md +16 -4
- package/README.md +21 -9
- package/dist/core.js +52 -11
- package/dist/harnesses/claude.js +11 -1
- package/dist/harnesses/codex.js +6 -0
- package/dist/index.js +4 -2
- package/dist/parent-delivery-owners.js +42 -0
- package/dist/parent-delivery.js +3 -3
- package/dist/runtime-error-report.js +4 -1
- package/dist/tmux-runtime.js +98 -13
- package/docs/DESIGN.md +10 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.50.0] - 2026-10-04
|
|
11
|
+
|
|
12
|
+
### 変更
|
|
13
|
+
|
|
14
|
+
- 新しい端末の環境を、その端末を開いたMCP processの環境にした(ADR 0081。POSIXのtmux。Windowsのpsmuxは元からこの動き)。
|
|
15
|
+
今までは、tmuxのserverを最初に起こした呼び出し元の環境が、後から開く全部の端末へ入っていた。別の呼び出し元の
|
|
16
|
+
`PATH`や`npm_config_prefix`、その呼び出し元だけが持つ変数が、名指ししていなくても端末に入る。
|
|
17
|
+
- harnessがMCP processへ渡す環境が少なければ、端末の環境もその分だけになる。Codexは既定で`HOME`・`PATH`・`SHELL`・`TERM`の
|
|
18
|
+
4つしか渡さない。要る変数は`~/.codex/config.toml`の`[mcp_servers.aiterm]`に`env_vars`で名前を並べる。
|
|
19
|
+
- `AITERM_SESSION_ID`・`AITERM_AGENT_*`・`TMUX`・`TMUX_PANE`は継がない(今までどおり、端末ごとに付く)。
|
|
20
|
+
- `env_vars`は今までどおり使える。`pty_list`の`env_keys`で読めるのは、`env_vars`で名指しした値と`AITERM_SESSION_ID`だけ。
|
|
21
|
+
- 同じtmuxのserverを、前の版のMCP processと並んで使える。前の版が開く端末は今までどおり動く。
|
|
22
|
+
|
|
23
|
+
### 修正
|
|
24
|
+
|
|
25
|
+
- 利用者の`~/.tmux.conf`を読んでいた。空のconfigを指す`-f`を`new-session`の引数として渡していて、tmux本体には届いていなかった。
|
|
26
|
+
READMEの「`-f /dev/null`で起動し、`~/.tmux.conf`を読まない」のとおりにした。
|
|
27
|
+
- tmux 3.2未満で端末を1つも開けなかった(`new-session -f`を知らず「unknown option」で落ちる)。3.1cで開けることを確かめた。
|
|
28
|
+
|
|
29
|
+
## [0.49.0] - 2026-10-03
|
|
30
|
+
|
|
31
|
+
### 追加
|
|
32
|
+
|
|
33
|
+
- `pty_observe`に、agent sessionを閉じると失うものを数える2項目を足した(ADR 0080)。`activity.post_startup_process_count`は、起動準備が完了した時点に居なかったprocessの数(裏で動かしている作業。起動直後の1分に始めたものも数える)。`pending_child_deliveries`は、そのsessionが親として待っている、まだ届け終えていない子の結果の数。数えられない時はnullで、0と区別する。
|
|
34
|
+
|
|
35
|
+
### 修正
|
|
36
|
+
|
|
37
|
+
- 会話が長いClaude Codeのsessionが、入力待ちでも`pty_observe`で`unknown / unrecognized_screen`と出ていた。起動時の見出し「Claude Code」が画面の取得範囲から流れ出ると読めなかった。見出しが無い時は入力欄の形で読む。
|
|
38
|
+
- `aiterm-setup`(`aiterm-update`の中でも走る)と`--remove-cursor-parent-hooks`が、gpt-connectorのCursor hookを`~/.cursor/hooks.json`から消していた。自分のhookを名前の部分一致で探し、`cursor-parent-hook.js`が`gpt-connector-cursor-parent-hook.js`にも当たっていた。file名の境目まで見る(aiterm-steer-delivery 0.1.10)。
|
|
39
|
+
- `aiterm-setup`が、Claude Codeの`settings.json`を中身が同じでも書き換えることがあった。他の道具が後からhookを足していると、Aitermのentryを末尾へ移していた。同じ中身で登録済みなら並びを保つ(aiterm-steer-delivery 0.1.10)。
|
|
40
|
+
- `pty_send`が「TUI が入力受付状態になりません」で断る時、その時の画面の読み(`state=… reason=…`)を文の末尾へ付ける。承認や確認のmodalの時は待たずに断るが、どの画面だったかが残らなかった。
|
|
41
|
+
- BugHubへの報告で、`observed_at`を秒へ切り捨てていた。記録の時刻はミリ秒まで持つので、エラーを記録した直後(同じ秒の中)の自動送信は、受け口に「記録が観測より後」として断られる作りだった(記録は失われず、次の送信の機会まで届くのが遅れる)。切り捨てず、載せる記録のどの時刻よりも前にしない。
|
|
42
|
+
- 実行中のCursor・Grokへ文を差し込む時、待ち行列の表示が5秒より遅い端末で`STEER_NOT_QUEUED`になっていた(Windowsの実機で、送ってから表示まで8〜9秒)。文は待ち行列に入っていて、今のturnの後に別のturnとして動いていた。子のturnが続いている間は、表示を30秒まで待つ。
|
|
43
|
+
|
|
10
44
|
## [0.48.0] - 2026-10-03
|
|
11
45
|
|
|
12
46
|
### 追加
|
|
@@ -1994,7 +2028,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
1994
2028
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
1995
2029
|
provenance.
|
|
1996
2030
|
|
|
1997
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.
|
|
2031
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.50.0...HEAD
|
|
2032
|
+
[0.50.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.49.0...v0.50.0
|
|
2033
|
+
[0.49.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.48.0...v0.49.0
|
|
1998
2034
|
[0.48.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.47.1...v0.48.0
|
|
1999
2035
|
[0.47.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.47.0...v0.47.1
|
|
2000
2036
|
[0.47.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.46.1...v0.47.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.
|
|
212
|
+
**状態:** 開発継続中 · 現行公開版 **v0.50.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -299,10 +299,15 @@ $ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeo
|
|
|
299
299
|
|
|
300
300
|
Cursorの`model`は`gpt-5.6-luna`のようなbase model、`reasoning_effort`は`high`のように別指定する。adapterは現行`model-effort` IDを`cursor-agent models`へ照合し、起動中変更はCursor標準model pickerのparameter editorを使う。不在時は別modelへfallbackしない。
|
|
301
301
|
|
|
302
|
+
新しい端末の環境は、その端末を開いたMCP processの環境で決まる(v0.50.0から。ADR 0081)。tmuxのserverを
|
|
303
|
+
先に起こした別の呼び出し元の値は入らない。Aitermが端末ごとに付け直す`AITERM_SESSION_ID`・`AITERM_AGENT_*`は
|
|
304
|
+
継がない。harnessがMCP processへ渡す環境が少なければ、端末の環境もその分だけになる(Codexは既定で
|
|
305
|
+
`HOME`・`PATH`・`SHELL`・`TERM`。要る変数は`[mcp_servers.aiterm]`の`env_vars`で渡す)。
|
|
306
|
+
|
|
302
307
|
`env_vars`は環境変数の**名前**だけを並べるallowlistであり、name/value mapではない。aitermは
|
|
303
308
|
launcher起動時に現在のMCP processから各名前を読み、存在する値をshell quoteして、その1回のvendor
|
|
304
309
|
起動コマンドへ入れる。未設定名は省略し、shell変数名として不正な名前はsession作成前に失敗する。
|
|
305
|
-
|
|
310
|
+
名指しした値だけが`pty_list`の`env_keys`で読める。値はMCP tool引数には入らないが、
|
|
306
311
|
PTYの起動コマンドとして送られ、sessionの`.lastcmd`にも保持されるため、起動先vendorと同じOS userへ
|
|
307
312
|
到達する。秘密転送路ではなく、席identityやworkflow用の非secret変数だけに使う。
|
|
308
313
|
|
|
@@ -492,8 +497,9 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
|
|
|
492
497
|
### セッション観測と起動準備
|
|
493
498
|
|
|
494
499
|
`pty_open`の既定shellはPOSIXでbash、WindowsでPowerShell 7。通常PTYとagentの内側では
|
|
495
|
-
`AITERM_SESSION_ID`で自分のsession
|
|
496
|
-
|
|
500
|
+
`AITERM_SESSION_ID`で自分のsessionを識別できる。端末は開いたMCP processの環境を継ぐ。`env_vars`は、そのうち
|
|
501
|
+
sessionへ登録する名前の配列で、`pty_list({ env_keys: ["JOB_OWNER"] })`は登録した非秘密キーだけを`environment`へ返す。
|
|
502
|
+
登録していない値と未設定値はnull。
|
|
497
503
|
一覧の`aiterm.pty-list-result.v1`は`observed_at`と`sessions`を持ち、各行に`session_id`、`current_command`、
|
|
498
504
|
`attached`、`width`、`height`、`harness`、`environment`を返す。従来のtextも維持する。
|
|
499
505
|
|
|
@@ -510,6 +516,12 @@ aiterm は同じ核心の洞察——端末を出会いの場にする——を
|
|
|
510
516
|
`background_cpu_delta_complete`はpane開始から60秒以降に生成された子孫だけの同じ観測で、起動時MCPを除外する。
|
|
511
517
|
`token_hint`は画面の直近token表示値またはnull。画面本文や生argvを解析する必要はない。
|
|
512
518
|
|
|
519
|
+
agent sessionを閉じると失うものは、次の2項目で分かる。`activity.post_startup_process_count`は、`agent_launch`の起動準備が
|
|
520
|
+
完了した時点(初手を送る前)に居なかったprocessの数。起動直後の1分に始めた裏の作業も数える。Codex自身の補助process
|
|
521
|
+
(`codex-code-mode-host`)は数えない。`pending_child_deliveries`は、そのsessionが親として待っていて、まだ届け終えていない
|
|
522
|
+
子の結果の数。呼び出した側が誰でも付く。どちらも、分からない時はnull(通常PTY、harnessのprocessを特定できない時。
|
|
523
|
+
processの数は0.48.0以前が起動したagentも)。別端末が旧版の時は項目ごと無い。nullと項目なしは「分からない」で、0ではない。
|
|
524
|
+
|
|
513
525
|
`agent_launch({ harness, cwd, trust_project: true })`はpromptなしでも既知のworkspace・project hooks・MCP初期同意を
|
|
514
526
|
進め、入力受付とharness生存を確認して`startup.status="ready"`を返す。指定なしのpromptなし起動は`not_checked`。
|
|
515
527
|
Claude Code初回起動の文字表示テーマ選択では、画面で選択済みの項目を確定して起動を続ける。
|
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.
|
|
226
|
+
**Status:** actively maintained · current public release **v0.50.0** · 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
|
|
|
@@ -321,11 +321,17 @@ The canonical harness choices are:
|
|
|
321
321
|
| `grok-cli` | Grok Build CLI | Grok model selected with `model`; live catalog check |
|
|
322
322
|
| `cursor-cli` | Cursor Agent CLI | GPT, Claude, Grok, or another Cursor catalog model; normal transcript completion |
|
|
323
323
|
|
|
324
|
+
A new terminal gets the environment of the MCP process that opened it (since v0.50.0, ADR 0081).
|
|
325
|
+
Values from another caller that happened to start the tmux server first no longer appear in it.
|
|
326
|
+
`AITERM_SESSION_ID` and `AITERM_AGENT_*` are not inherited; aiterm sets them per terminal. If a harness
|
|
327
|
+
passes only a few variables to its MCP process, the terminal has only those (Codex passes `HOME`, `PATH`,
|
|
328
|
+
`SHELL`, and `TERM` by default; forward more with `env_vars` under `[mcp_servers.aiterm]`).
|
|
329
|
+
|
|
324
330
|
`env_vars` is an allowlist of environment-variable **names**, not a name/value map. At launch,
|
|
325
331
|
aiterm reads each valid name from its current MCP process, shell-quotes present values, and places
|
|
326
332
|
them on that one harness launch command. Missing names are omitted; invalid shell variable names
|
|
327
|
-
fail before session creation.
|
|
328
|
-
|
|
333
|
+
fail before session creation. Only the named values can be read back with `env_keys` in `pty_list`.
|
|
334
|
+
Values do not enter the MCP tool arguments, but they are delivered through the
|
|
329
335
|
PTY launch command and retained in aiterm's per-session `.lastcmd`; the launched harness and other
|
|
330
336
|
processes with access to the same OS user may read them. Use this for non-secret seat identity and
|
|
331
337
|
workflow variables, not as a secret transport.
|
|
@@ -523,8 +529,8 @@ On top of that sits a productized layer a raw tmux bridge doesn't have: **token-
|
|
|
523
529
|
### Session observation and startup
|
|
524
530
|
|
|
525
531
|
`pty_open` defaults to bash on POSIX and PowerShell 7 on Windows. Ordinary terminals and agents receive
|
|
526
|
-
`AITERM_SESSION_ID`.
|
|
527
|
-
`pty_list({ env_keys: ["JOB_OWNER"] })` returns only
|
|
532
|
+
`AITERM_SESSION_ID`. A terminal inherits the environment of the MCP process that opened it. Pass names in `env_vars` to register them on the session.
|
|
533
|
+
`pty_list({ env_keys: ["JOB_OWNER"] })` returns only registered non-secret values in `environment`; unregistered and missing values are null.
|
|
528
534
|
Its `aiterm.pty-list-result.v1` receipt contains `observed_at` and `sessions`, whose entries include `session_id`,
|
|
529
535
|
`current_command`, `attached`, `width`, `height`, `harness`, and `environment`. Existing text remains available.
|
|
530
536
|
|
|
@@ -542,6 +548,13 @@ between observations, the delta covers only observed increments and `cpu_delta_c
|
|
|
542
548
|
only to descendants created at least 60 seconds after the pane, excluding startup MCP processes. `token_hint` is the latest
|
|
543
549
|
displayed token count or null. Callers do not need raw argv or pane-text parsing.
|
|
544
550
|
|
|
551
|
+
Two fields tell a caller what closing an agent session would lose. `activity.post_startup_process_count` is the number of
|
|
552
|
+
processes in the session that did not exist when `agent_launch` finished startup (before the first prompt), so background work
|
|
553
|
+
started in the first minute is counted too; Codex's own `codex-code-mode-host` helper is not counted. `pending_child_deliveries`
|
|
554
|
+
is the number of sub-agent results this session is still waiting for as a parent, whoever the caller is. Both are null when
|
|
555
|
+
Aiterm cannot tell (ordinary terminals, agents launched by 0.48.0 or earlier for the process count, or an unresolved harness
|
|
556
|
+
process), and may be absent when a remote host runs an older Aiterm. Treat null or absent as unknown, not as zero.
|
|
557
|
+
|
|
545
558
|
`agent_launch({ harness, cwd, trust_project: true })` completes known workspace, project-hook, and project-MCP startup
|
|
546
559
|
consent even without a prompt, then verifies input readiness and harness liveness before returning `startup.status="ready"`.
|
|
547
560
|
For Claude Code's first-run text-style menu, it confirms the item already selected on screen before continuing startup.
|
|
@@ -777,10 +790,9 @@ symlink, filter, or replace harness configuration, authentication, MCP, plugin,
|
|
|
777
790
|
trust, memory, or history stores. Cleanup removes only aiterm-owned launch metadata and completion
|
|
778
791
|
correlation files.
|
|
779
792
|
|
|
780
|
-
The ordinary environment
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
per-launch overlay, not a replacement environment or configuration snapshot.
|
|
793
|
+
The ordinary environment comes from the MCP process that opened the terminal, not from whichever caller
|
|
794
|
+
started the persistent multiplexer server (since v0.50.0). Every harness still accepts
|
|
795
|
+
`env_vars: ["NAME", ...]`; those names are placed on the launch command and registered on the session.
|
|
784
796
|
|
|
785
797
|
## License
|
|
786
798
|
|
package/dist/core.js
CHANGED
|
@@ -15,13 +15,14 @@ import { fileURLToPath } from "node:url";
|
|
|
15
15
|
import * as rtk from "./rtk.js";
|
|
16
16
|
import { isCursorMcpClient } from "aiterm-steer-delivery";
|
|
17
17
|
import { paneTokenHint } from "./harnesses/pane-tokens.js";
|
|
18
|
+
import { unfinishedDeliveriesOwnedBy } from "./parent-delivery-owners.js";
|
|
18
19
|
import { readRuntimeProcesses, processSubtree, parentProcess, processIdentity, backgroundProcesses } from "./process-runtime.js";
|
|
19
20
|
import { AitermError, telemetryOwnedFailure, ownTelemetryFailure } from "./errors.js";
|
|
20
|
-
import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs,
|
|
21
|
+
import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, pasteBufferBaseArgs, tmuxNewSession, registeredSessionEnvironment, attachCommand, normalizePaneCommand, atomicShellMultiline, appendMarkSentinel, markShellCommand, settlePaneLog, paneCwdArgument, sessionEnvironmentLaunch, } from "./tmux-runtime.js";
|
|
21
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";
|
|
22
23
|
import { catalogUnavailable } from "./model-catalog.js";
|
|
23
24
|
import { grokAuthPlan, grokAuthStatus, grokAuthPane, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokModelChoices, grokEventsTranscript, latestGrokCompletion, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
|
|
24
|
-
import { codexAuthPlan, codexAuthStatus, codexAuthPane, bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, } from "./harnesses/codex.js";
|
|
25
|
+
import { codexAuthPlan, codexAuthStatus, codexAuthPane, bindCodexTranscriptSession, latestCodexCompletion, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexHelperProcess, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, } from "./harnesses/codex.js";
|
|
25
26
|
import { claudeAuthPlan, claudeAuthStatus, claudeAuthPane, OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, claudeModelChoices, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeUsageLimit, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, claudeApiErrorAfter, } from "./harnesses/claude.js";
|
|
26
27
|
import { cursorAuthPlan, cursorAuthStatus, cursorAuthPane, bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, cursorModelChoices, buildCursorAgentCmd, cursorAgentArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, cursorPromptHooksRunning, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
|
|
27
28
|
import { readInterimWords, recordInterimBoundary } from "./interim-words.js";
|
|
@@ -671,11 +672,10 @@ export function openSession(name, shell = "bash", envVars = []) {
|
|
|
671
672
|
throw new AitermError(`session '${nm}' は既に存在します(list で確認)`, 2);
|
|
672
673
|
}
|
|
673
674
|
else {
|
|
674
|
-
// -f は端末個人の設定ファイルを読まないための空 config。
|
|
675
675
|
const environment = [...banner, ...envVars.filter(key => key !== "AITERM_SESSION_ID" && process.env[key] !== undefined)
|
|
676
676
|
.map(key => `${key}=${process.env[key]}`), `AITERM_SESSION_ID=${nm}`];
|
|
677
677
|
const launch = sessionEnvironmentLaunch(shell, environment);
|
|
678
|
-
const r =
|
|
678
|
+
const r = tmuxNewSession(true, nm, launch, environment.map(entry => entry.slice(0, entry.indexOf("="))));
|
|
679
679
|
if (r.code === 0) {
|
|
680
680
|
if (launch.register_after_start)
|
|
681
681
|
for (const entry of environment) {
|
|
@@ -1085,9 +1085,10 @@ export function observeSession(name, cursor) {
|
|
|
1085
1085
|
schema: "aiterm.pty-observe-result.v1", session_id: name, observed_at: new Date().toISOString(),
|
|
1086
1086
|
exists: !!listed, harness: null, launch_id: null, state: "missing", reason: "session_missing",
|
|
1087
1087
|
pane_alive: false, harness_alive: null, pane_process: null, harness_process: null, process_identity: null,
|
|
1088
|
-
token_hint: null,
|
|
1088
|
+
token_hint: null, pending_child_deliveries: null,
|
|
1089
1089
|
activity: { cursor: null, output_changed: null, cpu_seconds: null, cpu_delta_seconds: null, cpu_delta_complete: null,
|
|
1090
|
-
background_cpu_seconds: null, background_cpu_delta_seconds: null, background_cpu_delta_complete: null
|
|
1090
|
+
background_cpu_seconds: null, background_cpu_delta_seconds: null, background_cpu_delta_complete: null,
|
|
1091
|
+
post_startup_process_count: null },
|
|
1091
1092
|
};
|
|
1092
1093
|
if (!listed)
|
|
1093
1094
|
return result;
|
|
@@ -1164,6 +1165,10 @@ export function observeSession(name, cursor) {
|
|
|
1164
1165
|
screen_digest: createHash("sha256").update(screen).digest("hex"), processes, background_processes: backgroundCpu,
|
|
1165
1166
|
};
|
|
1166
1167
|
const comparable = previous?.pane_identity === sample.pane_identity;
|
|
1168
|
+
// 起動完了の控えが無いsession(通常PTY、控える前の版で起動したagent)とharnessを特定できない時は数えない。
|
|
1169
|
+
const startup = agent && Array.isArray(meta?.startup_processes) ? new Set(meta.startup_processes) : null;
|
|
1170
|
+
// このsessionが親として待っている子の結果。持ち主はharnessの下で動くAitermのMCP process。
|
|
1171
|
+
result.pending_child_deliveries = agent ? unfinishedDeliveriesOwnedBy(activityRows) : null;
|
|
1167
1172
|
result.activity = {
|
|
1168
1173
|
cursor: Buffer.from(JSON.stringify(sample)).toString("base64url"),
|
|
1169
1174
|
output_changed: comparable ? previous.screen_digest !== sample.screen_digest : null,
|
|
@@ -1173,6 +1178,9 @@ export function observeSession(name, cursor) {
|
|
|
1173
1178
|
background_cpu_seconds: Object.values(backgroundCpu).reduce((sum, cpu) => sum + cpu, 0),
|
|
1174
1179
|
background_cpu_delta_seconds: comparable ? Object.entries(backgroundCpu).reduce((sum, [identity, cpu]) => sum + cpu - (previous.background_processes[identity] ?? 0), 0) : null,
|
|
1175
1180
|
background_cpu_delta_complete: comparable ? Object.keys(previous.background_processes).every(identity => identity in backgroundCpu) : null,
|
|
1181
|
+
post_startup_process_count: startup ? new Set(activityRows
|
|
1182
|
+
.filter(row => !(meta?.kind === "codex" && codexHelperProcess(row.command)))
|
|
1183
|
+
.map(row => `${row.pid}:${row.started_identity}`).filter(identity => !startup.has(identity))).size : null,
|
|
1176
1184
|
};
|
|
1177
1185
|
return result;
|
|
1178
1186
|
}
|
|
@@ -1190,7 +1198,13 @@ export function listSessionsResult(envKeys = []) {
|
|
|
1190
1198
|
const [name, command, attached, width, height] = line.split("\t");
|
|
1191
1199
|
assertSessionName(name);
|
|
1192
1200
|
const environment = {};
|
|
1201
|
+
const registered = envKeys.length ? registeredSessionEnvironment(name) : null;
|
|
1193
1202
|
for (const key of envKeys) {
|
|
1203
|
+
// sessionの表には開いたprocessの環境も入っている。返すのは名指しで登録した値だけ。
|
|
1204
|
+
if (registered && !registered.includes(key)) {
|
|
1205
|
+
environment[key] = null;
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1194
1208
|
const value = tmux("show-environment", "-t", name, key);
|
|
1195
1209
|
if (value.code !== 0 && !/unknown variable|not found|not set/i.test(value.stderr))
|
|
1196
1210
|
throw new AitermError("session環境変数を照会できません", 2);
|
|
@@ -1989,6 +2003,8 @@ function loadAgentMetadata(name) {
|
|
|
1989
2003
|
initial_prompt_delivery: m.initial_prompt_delivery, initial_prompt_cursor: m.initial_prompt_cursor,
|
|
1990
2004
|
};
|
|
1991
2005
|
const executableFields = m.agent_executable === undefined ? {} : { agent_executable: m.agent_executable };
|
|
2006
|
+
const startupFields = Array.isArray(m.startup_processes) && m.startup_processes.every((identity) => typeof identity === "string")
|
|
2007
|
+
? { startup_processes: m.startup_processes } : {};
|
|
1992
2008
|
if (m.kind === "claude") {
|
|
1993
2009
|
const expectedSettings = agentManagedClaudeSettingsPath(name, m.launch_id);
|
|
1994
2010
|
const expectedResult = agentClaudeResultPath(name, m.launch_id);
|
|
@@ -2016,6 +2032,7 @@ function loadAgentMetadata(name) {
|
|
|
2016
2032
|
initial_prompt: normalizeInitialPromptState(m.initial_prompt),
|
|
2017
2033
|
...deliveryFields,
|
|
2018
2034
|
...executableFields,
|
|
2035
|
+
...startupFields,
|
|
2019
2036
|
launch_operation_id: launchOperationId,
|
|
2020
2037
|
launch_request_digest: launchRequestDigest,
|
|
2021
2038
|
hook_route: "shared_claude_settings",
|
|
@@ -2047,6 +2064,7 @@ function loadAgentMetadata(name) {
|
|
|
2047
2064
|
initial_prompt: normalizeInitialPromptState(m.initial_prompt),
|
|
2048
2065
|
...deliveryFields,
|
|
2049
2066
|
...executableFields,
|
|
2067
|
+
...startupFields,
|
|
2050
2068
|
hook_route: "shared_codex_home",
|
|
2051
2069
|
completion_route: "codex_transcript",
|
|
2052
2070
|
...loadAgentLineageFields(m, true),
|
|
@@ -2074,6 +2092,7 @@ function loadAgentMetadata(name) {
|
|
|
2074
2092
|
initial_prompt: normalizeInitialPromptState(m.initial_prompt),
|
|
2075
2093
|
...deliveryFields,
|
|
2076
2094
|
...executableFields,
|
|
2095
|
+
...startupFields,
|
|
2077
2096
|
hook_route: "shared_cursor_home",
|
|
2078
2097
|
completion_route: "cursor_transcript",
|
|
2079
2098
|
...loadAgentLineageFields(m, true),
|
|
@@ -2109,6 +2128,7 @@ function loadAgentMetadata(name) {
|
|
|
2109
2128
|
initial_prompt: normalizeInitialPromptState(m.initial_prompt),
|
|
2110
2129
|
...deliveryFields,
|
|
2111
2130
|
...executableFields,
|
|
2131
|
+
...startupFields,
|
|
2112
2132
|
hook_route: "shared_grok_home",
|
|
2113
2133
|
completion_route: "grok_transcript",
|
|
2114
2134
|
...loadAgentLineageFields(m, true),
|
|
@@ -3202,6 +3222,11 @@ async function prepareAgentInput(name, meta, options) {
|
|
|
3202
3222
|
const live = observeSession(name);
|
|
3203
3223
|
if (live.harness_alive !== true || live.harness_process === null || live.state !== "idle")
|
|
3204
3224
|
return { status: "blocked", reason: live.reason };
|
|
3225
|
+
// ここまでに居るprocessは起動時の足場(harness・MCP・起動時hook)。初手を送る前に控える。
|
|
3226
|
+
if (live.activity.cursor !== null) {
|
|
3227
|
+
meta.startup_processes = Object.keys(decodeActivityCursor(live.activity.cursor, name).processes);
|
|
3228
|
+
writeAgentMetadata(meta);
|
|
3229
|
+
}
|
|
3205
3230
|
return { status: "ready", reason: "composer_ready" };
|
|
3206
3231
|
}
|
|
3207
3232
|
export async function sendInitialAgentPrompt(name, text, o = {}) {
|
|
@@ -3849,8 +3874,13 @@ export async function dispatchAgentTurn(name, text, o = {}) {
|
|
|
3849
3874
|
const reason = grokPaneObservation(ready.lastScreen).reason;
|
|
3850
3875
|
throw new AitermError(`GROK_RATE_LIMIT_RECOVERY_FAILED: ${reason}。上限パネル解除後の入力受付を確認できません。今回の文字列は送信していません。`, 2);
|
|
3851
3876
|
}
|
|
3877
|
+
// 待たずに断るのは、応答が要る既知の画面(承認・確認のmodal)の時。どの画面で断ったかを残す。
|
|
3878
|
+
const state = meta.kind === "grok" ? grokPaneObservation(ready.lastScreen)
|
|
3879
|
+
: meta.kind === "codex" ? codexPaneObservation(ready.lastScreen)
|
|
3880
|
+
: meta.kind === "claude" ? claudePaneObservation(ready.lastScreen) : cursorPaneObservation(ready.lastScreen);
|
|
3852
3881
|
throw new AitermError(`agent session '${name}' の ${agentLabel(meta.kind)} TUI が入力受付状態になりません。文字列は送信していません。` +
|
|
3853
|
-
"少し後で pty_read(screen:true) を確認し、TUI が起動済みなら再度 pty_send してください。"
|
|
3882
|
+
"少し後で pty_read(screen:true) を確認し、TUI が起動済みなら再度 pty_send してください。" +
|
|
3883
|
+
`\nstate=${state.state} reason=${state.reason}`, 2);
|
|
3854
3884
|
}
|
|
3855
3885
|
}
|
|
3856
3886
|
if (codexRateLimitModelSwitch)
|
|
@@ -3928,10 +3958,17 @@ const CURSOR_FOLLOW_UP_STEER_RE = /enter steer · /;
|
|
|
3928
3958
|
const cursorSteerQueued = (screen) => CURSOR_FOLLOW_UP_STEER_RE.test(screen.split("\n").filter(line => line.trim()).slice(-30).join("\n"));
|
|
3929
3959
|
const STEER_SCREEN_POLL_MS = 100;
|
|
3930
3960
|
const STEER_SCREEN_MAX_SAMPLES = 50;
|
|
3931
|
-
|
|
3932
|
-
|
|
3961
|
+
// 待ち行列の表示が遅い端末がある(fox 2026-10-03: Cursorへ送ってから表示まで8〜9秒。5秒で打ち切って3回とも失敗した)。
|
|
3962
|
+
// 子のturnが続いている間は、文は待ち行列へ入るしかないので、ここまで待つ。
|
|
3963
|
+
const STEER_SCREEN_RUNNING_MAX_SAMPLES = 300;
|
|
3964
|
+
const STEER_RUNNING_CHECK_SAMPLES = 10;
|
|
3965
|
+
async function waitSteerScreen(name, predicate, stillRunning) {
|
|
3966
|
+
for (let i = 0; i < STEER_SCREEN_RUNNING_MAX_SAMPLES; i++) {
|
|
3933
3967
|
if (predicate(captureScreen(name, AGENT_TUI_READY_LINES)))
|
|
3934
3968
|
return true;
|
|
3969
|
+
// 短い待ちを過ぎたら、子のturnが続いている間だけ待ちを延ばす。終わっていれば、これ以上待っても表示は変わらない。
|
|
3970
|
+
if (i >= STEER_SCREEN_MAX_SAMPLES && i % STEER_RUNNING_CHECK_SAMPLES === 0 && !stillRunning())
|
|
3971
|
+
return false;
|
|
3935
3972
|
await sleep(STEER_SCREEN_POLL_MS);
|
|
3936
3973
|
}
|
|
3937
3974
|
return false;
|
|
@@ -4012,13 +4049,17 @@ async function steerRunningTurn(name, meta, text, o) {
|
|
|
4012
4049
|
: meta.kind === "cursor" ? cursorSteerQueued : null;
|
|
4013
4050
|
if (queued) {
|
|
4014
4051
|
const label = meta.kind === "cursor" ? "Cursor" : "Grok";
|
|
4015
|
-
|
|
4052
|
+
// Cursorはturn_endedを記録へ書いた後も画面にbusy表示を残すので、記録で見る。Grokは画面で見る。
|
|
4053
|
+
const stillRunning = () => meta.kind === "cursor"
|
|
4054
|
+
? latestCursorCompletion(meta, readTranscriptLines) === null
|
|
4055
|
+
: isAgentTuiBusy(meta.kind, captureScreen(name, AGENT_TUI_READY_LINES));
|
|
4056
|
+
if (!await waitSteerScreen(name, queued, stillRunning)) {
|
|
4016
4057
|
throw new AitermError(`STEER_NOT_QUEUED vendor=${meta.kind} session=${name}\n` +
|
|
4017
4058
|
`差し込む文が${label}の待ち行列へ入ったことを確認できません。子のturnが送る直前に終わっていた時は、` +
|
|
4018
4059
|
`文は新しいturnとして始まっており、その完了は通知されません。再送する前にpty_read(screen:true)で確かめてください。`, 2);
|
|
4019
4060
|
}
|
|
4020
4061
|
sendKey(name, "Enter");
|
|
4021
|
-
if (!await waitSteerScreen(name, screen => !queued(screen))) {
|
|
4062
|
+
if (!await waitSteerScreen(name, screen => !queued(screen), stillRunning)) {
|
|
4022
4063
|
throw new AitermError(`STEER_STILL_QUEUED vendor=${meta.kind} session=${name}\n` +
|
|
4023
4064
|
`差し込む文が${label}の待ち行列に残っています。現在のturnが終わると別turnとして実行されます。`, 2);
|
|
4024
4065
|
}
|
package/dist/harnesses/claude.js
CHANGED
|
@@ -258,8 +258,18 @@ export function claudeLaunchNote(model, effort, meta) {
|
|
|
258
258
|
const writeScopeNote = writeScopeLaunchNote("claude", meta?.write_scope);
|
|
259
259
|
return `起動設定: model=${model ?? "CLI既定"} effort=${effort ?? "CLI既定"}。${writeScopeNote}`;
|
|
260
260
|
}
|
|
261
|
+
// 会話が長くなると、起動時の見出し「Claude Code」は取得範囲から流れ出る。入力欄は❯行のすぐ上と下を罫線で挟む形で、
|
|
262
|
+
// 起動時の確認画面や通常shellの❯とは見分けられる。見出しが無い時はこの形でClaude CodeのTUIと判断する。
|
|
263
|
+
function claudeComposerBox(screen) {
|
|
264
|
+
const lines = screen.split(/\r?\n/u);
|
|
265
|
+
const rule = /^\s*─{8,}\s*$/u;
|
|
266
|
+
let marker = lines.length - 1;
|
|
267
|
+
while (marker >= 0 && !/^\s*❯/u.test(lines[marker]))
|
|
268
|
+
marker--;
|
|
269
|
+
return marker > 0 && rule.test(lines[marker - 1]) && lines.slice(marker + 1).some(line => rule.test(line));
|
|
270
|
+
}
|
|
261
271
|
export function claudeTuiReady(screen) {
|
|
262
|
-
if (!screen.includes("Claude Code"))
|
|
272
|
+
if (!screen.includes("Claude Code") && !claudeComposerBox(screen))
|
|
263
273
|
return false;
|
|
264
274
|
if (claudeLoginMethodMenu(screen))
|
|
265
275
|
return false;
|
package/dist/harnesses/codex.js
CHANGED
|
@@ -427,6 +427,12 @@ function currentCodexDialog(screen) {
|
|
|
427
427
|
const heading = [...current.matchAll(CODEX_DIALOG_HEADING)].at(-1);
|
|
428
428
|
return heading ? current.slice(heading.index) : current;
|
|
429
429
|
}
|
|
430
|
+
// Codexは道具を初めて使う時に補助process(code-mode-host)を立て、sessionの終わりまで残す。
|
|
431
|
+
// harnessの一部であって利用者の作業ではない。この下で動くprocessは作業として数える。
|
|
432
|
+
export function codexHelperProcess(command) {
|
|
433
|
+
const first = /^(?:"([^"]+)"|(\S+))/.exec(command);
|
|
434
|
+
return path.posix.basename((first?.[1] ?? first?.[2] ?? "").replace(/\\/g, "/")).replace(/\.exe$/i, "") === "codex-code-mode-host";
|
|
435
|
+
}
|
|
430
436
|
export function codexPaneObservation(screen) {
|
|
431
437
|
const failure = codexStartupFailure(screen);
|
|
432
438
|
if (failure)
|
package/dist/index.js
CHANGED
|
@@ -253,7 +253,7 @@ registerRemoteAwareTool("pty_open", {
|
|
|
253
253
|
inputSchema: {
|
|
254
254
|
name: z.string().nullish().describe("セッション名(省略時は t1, t2... を自動採番)"),
|
|
255
255
|
shell: z.string().default(DEFAULT_PTY_SHELL).describe(`起動シェル(既定 ${DEFAULT_PTY_SHELL})`),
|
|
256
|
-
env_vars: z.array(z.string()).optional().describe("
|
|
256
|
+
env_vars: z.array(z.string()).optional().describe("sessionへ登録する環境変数名(値は現在のMCP processから読む。pty_listのenv_keysで読める)。端末の環境は、名指ししなくても現在のMCP processの環境を継ぐ"),
|
|
257
257
|
},
|
|
258
258
|
}, async ({ name, shell, env_vars }) => {
|
|
259
259
|
try {
|
|
@@ -597,10 +597,12 @@ registerRemoteAwareTool("pty_observe", {
|
|
|
597
597
|
pane_process: nativeProcessIdentitySchema.nullable(), harness_process: nativeProcessIdentitySchema.nullable(),
|
|
598
598
|
process_identity: nativeProcessIdentitySchema.nullable(),
|
|
599
599
|
token_hint: z.number().nullable(),
|
|
600
|
+
pending_child_deliveries: z.number().nullable().optional(),
|
|
600
601
|
parent_deliveries: z.array(parentDeliveryOutputSchema).optional(),
|
|
601
602
|
activity: z.object({ cursor: z.string().nullable(), output_changed: z.boolean().nullable(),
|
|
602
603
|
cpu_seconds: z.number().nullable(), cpu_delta_seconds: z.number().nullable(), cpu_delta_complete: z.boolean().nullable(),
|
|
603
|
-
background_cpu_seconds: z.number().nullable(), background_cpu_delta_seconds: z.number().nullable(), background_cpu_delta_complete: z.boolean().nullable()
|
|
604
|
+
background_cpu_seconds: z.number().nullable(), background_cpu_delta_seconds: z.number().nullable(), background_cpu_delta_complete: z.boolean().nullable(),
|
|
605
|
+
post_startup_process_count: z.number().nullable().optional() }),
|
|
604
606
|
},
|
|
605
607
|
}, async ({ session_id, cursor }, extra) => {
|
|
606
608
|
try {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// 親配送の記録を、持ち主(登録したMCP process)から数える。
|
|
2
|
+
// parent-delivery.tsはcoreに依存するので、coreのpty_observeが使う分だけをここへ分ける。
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { ensureStateRoot } from "./agent-shared.js";
|
|
7
|
+
const DELIVERY_DIRECTORIES = ["parent-deliveries", "claude-parent-deliveries", "cursor-parent-deliveries"];
|
|
8
|
+
/** 持ち主の保存場所の名前の先頭。pidの使い回しと取り違えないよう、開始時刻も入れる。 */
|
|
9
|
+
export function deliveryOwnerPrefix(owner) {
|
|
10
|
+
return `${owner.pid}-${createHash("sha256").update(owner.started_identity).digest("hex").slice(0, 16)}-`;
|
|
11
|
+
}
|
|
12
|
+
/** 渡したprocessのどれかが持ち主で、まだ親へ届け終えていない配送の数。届け終えた記録はactiveから出ている。 */
|
|
13
|
+
export function unfinishedDeliveriesOwnedBy(processes, stateRoot = ensureStateRoot()) {
|
|
14
|
+
const prefixes = processes.map(deliveryOwnerPrefix);
|
|
15
|
+
let count = 0;
|
|
16
|
+
for (const prefix of ["", "remote-"]) {
|
|
17
|
+
for (const directory of DELIVERY_DIRECTORIES) {
|
|
18
|
+
const active = path.join(stateRoot, prefix + directory, "active");
|
|
19
|
+
let owners;
|
|
20
|
+
try {
|
|
21
|
+
owners = fs.readdirSync(active, { withFileTypes: true });
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code === "ENOENT")
|
|
25
|
+
continue;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
for (const owner of owners) {
|
|
29
|
+
if (!owner.isDirectory() || !prefixes.some(candidate => owner.name.startsWith(candidate)))
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
count += fs.readdirSync(path.join(active, owner.name)).filter(name => name !== "owner.json" && name.endsWith(".json")).length;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (error.code !== "ENOENT")
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return count;
|
|
42
|
+
}
|
package/dist/parent-delivery.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Aitermが所有する、子の完了観測・回答本文の保存・親への配送。
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { randomUUID
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { ensureStateRoot, writeJson0600 } from "./agent-shared.js";
|
|
7
7
|
import { readProcessIdentities } from "./process-runtime.js";
|
|
8
|
+
import { deliveryOwnerPrefix } from "./parent-delivery-owners.js";
|
|
8
9
|
import { CodexDeliveryError, submitCodexParentAnswer, verifyCodexParent } from "./codex-parent-receiver.js";
|
|
9
10
|
import { claudeParentSchema, ClaudeDeliveryError, bindClaudeParentDelivery, submitClaudeParentAnswer, verifyClaudeParent } from "./claude-parent-receiver.js";
|
|
10
11
|
import { cursorParentSchema, CursorDeliveryError, prepareCursorDelivery, submitCursorParentAnswer, verifyCursorParent } from "./cursor-parent-receiver.js";
|
|
@@ -141,8 +142,7 @@ export class ParentDeliveryManager {
|
|
|
141
142
|
if (!ownProcess)
|
|
142
143
|
throw new AitermError("PARENT_DELIVERY_OWNER_UNKNOWN: 配送processを識別できません", 2);
|
|
143
144
|
this.owner = { pid: process.pid, started_identity: ownProcess.started_identity, closed: false };
|
|
144
|
-
|
|
145
|
-
this.ownerDir = path.join(this.active, `${process.pid}-${identity}-${randomUUID()}`);
|
|
145
|
+
this.ownerDir = path.join(this.active, `${deliveryOwnerPrefix(this.owner)}${randomUUID()}`);
|
|
146
146
|
fs.mkdirSync(this.ownerDir, { recursive: true, mode: 0o700 });
|
|
147
147
|
fs.mkdirSync(this.results, { recursive: true, mode: 0o700 });
|
|
148
148
|
fs.mkdirSync(this.claims, { recursive: true, mode: 0o700 });
|
|
@@ -178,11 +178,14 @@ export async function reportRuntimeErrors(options) {
|
|
|
178
178
|
return result("halted");
|
|
179
179
|
}
|
|
180
180
|
// tsとobserved_atは同じ時刻から作る(受け口は両者が10分より離れたreportを断る)。
|
|
181
|
+
// observed_atは秒へ切り捨てず、載せる記録のどの時刻よりも前にしない。受け口は、記録や解決が観測より後のreportを
|
|
182
|
+
// 422で断る。記録の時刻はミリ秒まで持つので、切り捨てると、記録の直後(同じ秒の中)の自動送信が断られていた。
|
|
181
183
|
const ts = String(Math.floor(started.getTime() / 1000));
|
|
184
|
+
const observed = Math.max(started.getTime(), ...snapshot.records.flatMap((record) => [Date.parse(record.last_seen), record.resolved_at === null ? 0 : Date.parse(record.resolved_at)]));
|
|
182
185
|
const reportId = randomUUID();
|
|
183
186
|
const report = buildRuntimeErrorReport(snapshot.records, {
|
|
184
187
|
installedVersion: options.installedVersion ?? pkg.version, reportId,
|
|
185
|
-
observedAt: new Date(
|
|
188
|
+
observedAt: new Date(observed).toISOString(),
|
|
186
189
|
});
|
|
187
190
|
const body = Buffer.from(JSON.stringify(report), "utf8");
|
|
188
191
|
if (body.byteLength > MAX_REPORT_BYTES)
|
package/dist/tmux-runtime.js
CHANGED
|
@@ -118,11 +118,16 @@ export function tmuxSpawnEnv() {
|
|
|
118
118
|
return env;
|
|
119
119
|
}
|
|
120
120
|
export function tmuxCommandWithInput(observe, input, ...args) {
|
|
121
|
+
return runTmux(observe, input, args);
|
|
122
|
+
}
|
|
123
|
+
// session は、新しい session を作る呼び出しの時だけ渡す client の環境(POSIX のみ)。server を起こし得るのは
|
|
124
|
+
// この呼び出しだけなので、空 config の指定と macOS の gui domain での起動もここに限る。
|
|
125
|
+
function runTmux(observe, input, args, session) {
|
|
121
126
|
// maxBuffer は既定 1MiB。capture-pane(大きなスクロールバック)や多セッションの list-sessions で
|
|
122
127
|
// 頭打ちになり stdout が切れる/空になる。Python の subprocess.run は無制限だったので 64MiB へ広げる。
|
|
123
128
|
// Windows は tmux CLI 互換の native psmux を -L namespace 隔離で叩く(WSL 非依存)。
|
|
124
129
|
let r;
|
|
125
|
-
const spawnOpts = { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, input, env: tmuxSpawnEnv() };
|
|
130
|
+
const spawnOpts = { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, input, env: session ?? tmuxSpawnEnv() };
|
|
126
131
|
if (isWin) {
|
|
127
132
|
ensureWinPsmux(observe);
|
|
128
133
|
r = spawnSync(psmuxBin(), ["-L", WIN_NS, ...args], spawnOpts);
|
|
@@ -130,8 +135,10 @@ export function tmuxCommandWithInput(observe, input, ...args) {
|
|
|
130
135
|
else {
|
|
131
136
|
// resolveTmux() は tmux を解決できなければ明確な AitermError を投げる(POSIX 版の事前確認)。
|
|
132
137
|
const bin = resolveTmux(observe);
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
// -f は tmux 本体の引数で、server を起こす時にだけ読まれる(new-session の -f は別物で、config を指さない)。
|
|
139
|
+
const argv = session ? ["-f", TMUX_EMPTY_CONFIG, "-S", SOCK, ...args] : ["-S", SOCK, ...args];
|
|
140
|
+
r = (session && macGuiServerWanted(bin) && startTmuxInMacGui(bin, argv, session))
|
|
141
|
+
|| spawnSync(bin, argv, spawnOpts);
|
|
135
142
|
}
|
|
136
143
|
// ENOBUFS(出力が 64MiB 超)を「code=1 の失敗」へ握り潰すと部分/空 stdout を正常扱いしてしまう。区別して投げる。
|
|
137
144
|
// EXPECTED-FAILURE: 外部システム境界(tmux 出力過大)
|
|
@@ -191,8 +198,8 @@ export function macGuiTmuxPlist(label, dir, argv, env, cwd) {
|
|
|
191
198
|
"",
|
|
192
199
|
].join("\n");
|
|
193
200
|
}
|
|
194
|
-
function startTmuxInMacGui(bin,
|
|
195
|
-
return runInMacGui([bin,
|
|
201
|
+
function startTmuxInMacGui(bin, argv, env) {
|
|
202
|
+
return runInMacGui([bin, ...argv], env, process.cwd(), 30_000, "launchd経由のtmux起動が30秒以内に終わりませんでした");
|
|
196
203
|
}
|
|
197
204
|
// 起動前の認証確認(`claude auth status`・`agent status` 等)も keychain を読むので、tmux server と同じく
|
|
198
205
|
// Aqua の外では gui domain で実行する。gui domain が無ければ null を返し、呼び出し側が直接実行する。
|
|
@@ -306,21 +313,99 @@ export function pasteBufferBaseArgs() {
|
|
|
306
313
|
}
|
|
307
314
|
// new-session -f 用の空 config(端末個人の設定ファイルを読まない)。Windows は NUL デバイス。
|
|
308
315
|
export const TMUX_EMPTY_CONFIG = isWin ? "NUL" : "/dev/null";
|
|
316
|
+
// tmux 3.2 で new-session -e と、update-environment のワイルドカードが入った。
|
|
317
|
+
function tmuxHasSessionEnvironmentFlags(version) {
|
|
318
|
+
const observed = version ?? spawnSync(resolveTmux(), ["-V"], { encoding: "utf8", timeout: 5000 }).stdout;
|
|
319
|
+
const match = /^tmux (\d+)\.(\d+)/.exec(observed ?? "");
|
|
320
|
+
if (!match)
|
|
321
|
+
throw new AitermError("tmuxの環境変数機能を判定できません", 2);
|
|
322
|
+
return Number(match[1]) > 3 || (Number(match[1]) === 3 && Number(match[2]) >= 2);
|
|
323
|
+
}
|
|
309
324
|
export function sessionEnvironmentLaunch(shell, environment, version) {
|
|
310
|
-
|
|
311
|
-
if (!isWin) {
|
|
312
|
-
const observed = version ?? spawnSync(resolveTmux(), ["-V"], { encoding: "utf8", timeout: 5000 }).stdout;
|
|
313
|
-
const match = /^tmux (\d+)\.(\d+)/.exec(observed ?? "");
|
|
314
|
-
if (!match)
|
|
315
|
-
throw new AitermError("tmuxの環境変数機能を判定できません", 2);
|
|
316
|
-
modern = Number(match[1]) > 3 || (Number(match[1]) === 3 && Number(match[2]) >= 2);
|
|
317
|
-
}
|
|
325
|
+
const modern = isWin || tmuxHasSessionEnvironmentFlags(version);
|
|
318
326
|
if (modern)
|
|
319
327
|
return { shell, args: environment.flatMap(entry => ["-e", entry]), register_after_start: false };
|
|
320
328
|
// tmux <3.2はnew-session -eを持たない。子への継承とsession台帳への登録を製品内で完結する。
|
|
321
329
|
const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
322
330
|
return { shell: `exec /usr/bin/env ${environment.map(quote).join(" ")} ${quote(shell)}`, args: [], register_after_start: true };
|
|
323
331
|
}
|
|
332
|
+
// Aiterm が端末ごとに付け直す名札と、呼んだ側の pane を指す値。新しい端末へは継がせない。
|
|
333
|
+
const SESSION_OWN_ENV = /^(?:AITERM_SESSION_ID|AITERM_AGENT_[A-Z0-9_]+|TMUX|TMUX_PANE)$/;
|
|
334
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
335
|
+
export function sessionCallerEnvironment(base = tmuxSpawnEnv() ?? process.env) {
|
|
336
|
+
const env = {};
|
|
337
|
+
for (const [key, value] of Object.entries(base)) {
|
|
338
|
+
if (value !== undefined && !SESSION_OWN_ENV.test(key))
|
|
339
|
+
env[key] = value;
|
|
340
|
+
}
|
|
341
|
+
return env;
|
|
342
|
+
}
|
|
343
|
+
// update-environment へ渡す並び。3.2 以上は "*" で client の環境を丸ごと写す。それより前は名前を並べる。
|
|
344
|
+
// client に無い名前は、その session で「消す」印になる。
|
|
345
|
+
export function updateEnvironmentPatterns(caller, others, wildcard) {
|
|
346
|
+
const names = caller.filter(key => ENV_NAME.test(key));
|
|
347
|
+
const known = new Set(names);
|
|
348
|
+
const rest = [...new Set(others)].filter(key => ENV_NAME.test(key) && !known.has(key));
|
|
349
|
+
return wildcard ? ["*", ...rest] : [...names, ...rest];
|
|
350
|
+
}
|
|
351
|
+
// `NAME=value` と、消す印の `-NAME` から名前を取り出す。server が無ければ null。
|
|
352
|
+
function tmuxEnvironmentNames(...scope) {
|
|
353
|
+
const r = runTmux(false, undefined, ["show-environment", ...scope]);
|
|
354
|
+
if (r.code !== 0)
|
|
355
|
+
return null;
|
|
356
|
+
return r.stdout.split("\n").map(line => line.replace(/^-/, "").split("=", 1)[0]).filter(key => ENV_NAME.test(key));
|
|
357
|
+
}
|
|
358
|
+
// session の表には開いた process の環境が丸ごと入る。名指しで登録した名前は、session の option に控える。
|
|
359
|
+
const REGISTERED_ENV_OPTION = "@aiterm_env_keys";
|
|
360
|
+
// 控えが無ければ null(Windows と、この仕組みより前の版が開いた session)。
|
|
361
|
+
export function registeredSessionEnvironment(name) {
|
|
362
|
+
if (isWin)
|
|
363
|
+
return null;
|
|
364
|
+
const r = runTmux(false, undefined, ["show-options", "-t", `=${name}:`, "-v", REGISTERED_ENV_OPTION]);
|
|
365
|
+
return r.code === 0 && r.stdout.trim() ? r.stdout.trim().split(" ") : null;
|
|
366
|
+
}
|
|
367
|
+
// tmux は、server を起こした client の環境を共通環境として全 session へ配る。何もしないと、最初に端末を
|
|
368
|
+
// 開いた呼び出し元の値が、後から別の呼び出し元が開く端末へ入る。新しい端末の環境を「開いた process の環境」に
|
|
369
|
+
// するため、new-session の間だけ update-environment を差し替えて client の環境を session へ写し、共通環境に
|
|
370
|
+
// しか無い変数は session で消す(psmux は元から端末ごとに呼び出し元の環境を継ぐ)。共通環境そのものは
|
|
371
|
+
// 変えない。同じ server を使う旧版の process が、開く端末の環境をそこから受け取っている。
|
|
372
|
+
export function tmuxNewSession(observe, name, launch, registered = []) {
|
|
373
|
+
if (isWin) {
|
|
374
|
+
return runTmux(observe, undefined, ["new-session", "-d", "-s", name, ...launch.args, "-f", TMUX_EMPTY_CONFIG, launch.shell]);
|
|
375
|
+
}
|
|
376
|
+
const env = sessionCallerEnvironment();
|
|
377
|
+
const wildcard = tmuxHasSessionEnvironmentFlags();
|
|
378
|
+
// tmux は起動時に PWD を共通環境へ足す。
|
|
379
|
+
const others = new Set(["PWD"]);
|
|
380
|
+
for (let attempt = 0;; attempt++) {
|
|
381
|
+
for (const key of tmuxEnvironmentNames("-g") ?? [])
|
|
382
|
+
others.add(key);
|
|
383
|
+
const r = runTmux(observe, undefined, [
|
|
384
|
+
"set-option", "-g", "update-environment", updateEnvironmentPatterns(Object.keys(env), [...others], wildcard).join(" "), ";",
|
|
385
|
+
"new-session", "-d", "-s", name, ...launch.args, launch.shell, ";",
|
|
386
|
+
"set-option", "-t", `=${name}:`, REGISTERED_ENV_OPTION, registered.join(" "), ";",
|
|
387
|
+
"set-option", "-gu", "update-environment",
|
|
388
|
+
], env);
|
|
389
|
+
if (r.code !== 0) {
|
|
390
|
+
// 途中の命令が失敗すると、後続の命令は実行されない。差し替えた設定を戻す。
|
|
391
|
+
runTmux(false, undefined, ["set-option", "-gu", "update-environment"]);
|
|
392
|
+
return r;
|
|
393
|
+
}
|
|
394
|
+
// 共通環境の名前が、どれも session 側で上書きか消去になっていることを確かめる。別の呼び出し元が同時に
|
|
395
|
+
// server を起こした時は、消す対象を取り違えている。
|
|
396
|
+
const shared = tmuxEnvironmentNames("-g");
|
|
397
|
+
const own = new Set(tmuxEnvironmentNames("-t", `=${name}`) ?? []);
|
|
398
|
+
const uncovered = shared?.filter(key => !own.has(key));
|
|
399
|
+
if (uncovered?.length === 0)
|
|
400
|
+
return r;
|
|
401
|
+
runTmux(false, undefined, ["kill-session", "-t", `=${name}`]);
|
|
402
|
+
if (!uncovered || attempt >= 2) {
|
|
403
|
+
return { code: 1, stdout: "", stderr: "共通環境の変数をsessionから外せなかったため、sessionを破棄しました" };
|
|
404
|
+
}
|
|
405
|
+
for (const key of uncovered)
|
|
406
|
+
others.add(key);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
324
409
|
// 人が同じ session を覗く/介入するための attach コマンド。
|
|
325
410
|
// Windows は native psmux(tmux CLI 互換)を -L namespace で叩く。
|
|
326
411
|
export function attachCommand(name) {
|
package/docs/DESIGN.md
CHANGED
|
@@ -41,11 +41,20 @@ sentinelの方言は端末runtimeが実効shellから決める。SSH先の現在
|
|
|
41
41
|
psmuxが出力した余分な環境値を返さない。通常PTYとagentへ`AITERM_SESSION_ID`を注入し、
|
|
42
42
|
`env_vars`の継承とsessionへの登録もAitermが所有する。古いtmuxは子へのenv注入とsession登録を使う。
|
|
43
43
|
|
|
44
|
+
新しい端末の環境は、その端末を開いたprocessの環境にする(ADR 0081)。tmuxはserverを起こしたclientの環境を
|
|
45
|
+
全sessionへ配るので、`new-session`の間だけ`update-environment`を差し替えてclientの環境をsessionへ写し、
|
|
46
|
+
共通環境にしか無い変数はsessionで消す。`AITERM_SESSION_ID`・`AITERM_AGENT_*`・`TMUX`・`TMUX_PANE`は継がない。
|
|
47
|
+
sessionの表には環境が丸ごと入るので、名指しで登録した名前をsessionのoption `@aiterm_env_keys`に控え、
|
|
48
|
+
`env_keys`はその名前だけを返す。psmuxは元から端末ごとに呼び出し元の環境を継ぐ。
|
|
49
|
+
|
|
44
50
|
`pty_observe`は存在、pane/harnessの生存、画面状態と理由、native process identityを分ける。
|
|
45
51
|
PIDは開始識別子・argv digestと組にし、paneとharnessを同一視しない。特定できないidentityはnull。
|
|
46
52
|
同じlaunchに属するnpm shimとnative本体は、中間の非候補processも含めた祖先関係から一つの起動として扱う。
|
|
47
53
|
祖先を共有しない候補は別々に残し、複数候補を一つと推測しない。
|
|
48
54
|
POSIXの停止状態はOSのprocess表から取得し、SIGSTOP中は残画面より優先して`blocked/harness_stopped`を返す。
|
|
55
|
+
sessionを閉じると失うものは数で返す(ADR 0080)。`activity.post_startup_process_count`は起動完了の時点に居なかったprocessの数、
|
|
56
|
+
`pending_child_deliveries`はそのsessionが親として待つ未配送の数。数えられない時はnullで、0と区別する。
|
|
57
|
+
Claude Codeの入力待ちは、起動時の見出しが取得範囲から流れ出た後も、入力欄の形(`❯`行の上下の罫線)で読む。
|
|
49
58
|
画面本文とargv本文は返さず、活動cursorには画面digestとprocess別CPUだけを持たせる。
|
|
50
59
|
初回とpane再作成後の差分はnull。区間中にprocessが消えた時は観測できたCPU増分だけを返し、
|
|
51
60
|
`cpu_delta_complete=false`を付ける。background活動はpane開始から60秒以降に生成された子孫だけを数える。
|
|
@@ -73,6 +82,7 @@ agent sessionへの送信口は`pty_send`だけとする。子の状態は呼び
|
|
|
73
82
|
Claude Codeはtool処理中に画面の実行中表示が消え、Stop hookの実行中には表示が残る。turnの印を実行中判定の正本とする。Stopが発火しないAPIエラー終了では、次の送信時に印の作成後の会話記録にある`isApiErrorMessage`を確認し、終了したturnの印だけを解除する。過去のエラーで新しい印を解除しない。waiterは印を変更せず、読取専用のままエラーを返す。
|
|
74
83
|
それ以外は新しいturnとしてdispatchする。
|
|
75
84
|
CodexとClaude Codeは次のtool境界で同じturnへ取り込む。Cursorは「follow-ups」枠へ入った文を「enter steer」で現在turnへ移し、`turn_ended`は最後に1回書く。
|
|
85
|
+
待ち行列の表示は5秒待ち、子のturnが続いている間だけ30秒まで延ばす(表示が遅い端末がある。turnが終わっていれば、文は新しいturnとして始まっている)。
|
|
76
86
|
Grokは待ち行列へ入れた後に「send now」を押す。旧turnは`cancelled`(`cancellation_context.trigger=send_now`)で閉じ、
|
|
77
87
|
新turnが作業を継ぐので、完了判定はこの継ぎ目を完了と数えない。Grokが待ち行列へ入れない時とCursorの入力欄に本文が残る時は`steered`を返さない。
|
|
78
88
|
Cursorのsubmitはadapterがextended keyboard protocolのEnterへ変換し、呼び出し側は通常のdispatchだけを使う。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
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": [
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
72
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
73
|
-
"aiterm-steer-delivery": "^0.1.
|
|
73
|
+
"aiterm-steer-delivery": "^0.1.10",
|
|
74
74
|
"ws": "8.21.3",
|
|
75
75
|
"zod": "^4.4.3"
|
|
76
76
|
},
|