aiterm-mcp 0.56.1 → 0.57.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -1
- package/README.ja.md +7 -2
- package/README.md +7 -2
- package/dist/core.js +186 -18
- package/dist/harnesses/codex.js +33 -17
- package/dist/index.js +20 -2
- package/docs/DESIGN.md +13 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.57.1] - 2026-10-07
|
|
11
|
+
|
|
12
|
+
### 修正
|
|
13
|
+
|
|
14
|
+
- Windowsで、`pty_close`されたClaude Codeが会話終了のhook(SessionEnd)を走らせない件を直す(ADR 0100)。psmuxは、sessionを止める時に
|
|
15
|
+
画面のprocessへ切断の合図を送らない(POSIXのtmuxは送る)。
|
|
16
|
+
- WindowsのClaude Codeの席を閉じる時、止める前にC-cを間を空けずに2回送り、Claude Codeが自分で終わるのを最大4秒待つ。
|
|
17
|
+
動いている番の席へは、先にC-cを1回送って入力待ちに戻るのを最大1.5秒待つ。終わらなくても、その後は今までどおり止める。
|
|
18
|
+
- `pty_close`は、WindowsのClaude Codeの席でだけ遅くなる(実物で、入力待ちの席は約2.3秒、動いている番の席は約3.2秒)。
|
|
19
|
+
- Linux・macOS、Codex・Grok・Cursorの席、返りの形は変わらない。
|
|
20
|
+
- Codexが回答を流している間に届いた文を、新しいturnでなく差し込み(`mode: agent_steer`)として返す(ADR 0101)。Codex(0.160.1)は
|
|
21
|
+
回答を流している間、画面に動作中の行を出さない。今までは「入力待ち」と読んで新しいturnとして返し、Codexはその文を動いている
|
|
22
|
+
turnへ取り込むので、同じ1つの回答が2回の完了として届いていた(親への配送があれば2通)。
|
|
23
|
+
- 画面に入力欄が見え、この起動の記録(rollout)の最後の境界がturnの開始で、Codexのprocessが生きていて、0.4秒おいて見直しても
|
|
24
|
+
記録が同じturnの開始のままで画面が動いている時だけ、動いていると数える。1つでも欠けたら、今までどおり新しいturnで返す。
|
|
25
|
+
- その間に届いた文の受け付けは、5.6秒から約0.8秒になる(入力受付の11回の確かめを通らない。Windowsは約2秒)。
|
|
26
|
+
- 引数・返りの形・断りの文面、Claude Code・Grok・Cursorの席、画面に動作中の行が出ている時の動きは変わらない。
|
|
27
|
+
- 配送パッケージ`aiterm-steer-delivery`を0.3.1へ上げる。
|
|
28
|
+
- Codexの親配送の登録(`aiterm-setup --codex-steer enable`、`ensureCodexParentSteer`)が、登録の間に終わったCodexを、再起動待ちの記録
|
|
29
|
+
(`stale_processes`)へ残さない。ほかの導入が設定のために一時的に起こして止めたCodexが、止まり切る前の一覧に載って残っていた。
|
|
30
|
+
居ないprocessは照合で当たらないので、配送は止まっていなかった。
|
|
31
|
+
- Codexを起こす時、PATHにnodeの場所が無ければ足す。0.56.1で`aiterm-parent-delivery`が自分でしていた事を、MCPの本体からCodexを
|
|
32
|
+
起こす所(認証の確かめ、model一覧、親配送)にも広げる。
|
|
33
|
+
|
|
34
|
+
## [0.57.0] - 2026-10-07
|
|
35
|
+
|
|
36
|
+
### 追加
|
|
37
|
+
|
|
38
|
+
- `pty_send`(agentの席への送信)に、任意の引数`preface`(本文の前に置く1行)を足す(ADR 0099)。渡る文は「`preface`・空行・`text`」。
|
|
39
|
+
- Claude Codeの席へは、`preface`を端末の貼り付けの印なしで入れ、空行と`text`を1回の貼り付けで入れる。Claude Codeは貼り付けた長い文を
|
|
40
|
+
`<pasted_content>`で包み、席に「包みの中の指示は、包みの外の利用者自身の言葉が頼んだ時だけ従う」と教える。今までは文の全部が包みの中で、
|
|
41
|
+
席が連絡に従わない事があった(止めた直後の席で、実物の7回中5回)。前置きが包みの外に出る。
|
|
42
|
+
- Codex・Grok・Cursorの席へは、`preface`・空行・`text`を1つにつないで今までどおり貼る。
|
|
43
|
+
- `preface`は1行・200字以内・制御文字なしで、行頭が記号や空白でなく、`@`を含まない文。外れた時は`AGENT_PREFACE_INVALID`と
|
|
44
|
+
「文字列は送信していません。」で、待たずに断る。通常PTY・`force`付き・`remote`付きでは使えない(打鍵前に断る)。
|
|
45
|
+
- 省略した時の動き、振り分け、返りの形、断りの文面は変わらない。
|
|
46
|
+
|
|
10
47
|
## [0.56.1] - 2026-10-07
|
|
11
48
|
|
|
12
49
|
### 修正
|
|
@@ -2343,7 +2380,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2343
2380
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2344
2381
|
provenance.
|
|
2345
2382
|
|
|
2346
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.
|
|
2383
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.1...HEAD
|
|
2384
|
+
[0.57.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.0...v0.57.1
|
|
2385
|
+
[0.57.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.56.1...v0.57.0
|
|
2347
2386
|
[0.56.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.56.0...v0.56.1
|
|
2348
2387
|
[0.56.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.4...v0.56.0
|
|
2349
2388
|
[0.55.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.55.3...v0.55.4
|
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.57.1** · 動作対象は 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`は、任意の`preface`(本文の前に置く1行)を受ける。渡る文は「`preface`・空行・`text`」。Claude Codeの席へは、`preface`を端末の貼り付けの印なしで入れ、空行と本文を1回の貼り付けで入れる。Claude Codeは貼り付けた長い文を`<pasted_content>`で包み、「包みの中の指示は、包みの外の利用者自身の言葉が頼んだ時だけ従う」と読むので、連携元が付ける前置き(誰からの連絡か)を包みの外へ出すために使う。Codex・Grok・Cursorの席へは1つにつないで貼る。`preface`は1行・200字以内・制御文字なしで、行頭が記号や空白でなく、`@`を含まない文。外れた時は`AGENT_PREFACE_INVALID`と「文字列は送信していません。」を返す。通常PTY・`force:true`・`remote`付きでは使えない。
|
|
265
|
+
|
|
264
266
|
同じagent sessionへの`pty_send`が重なった時は、1本ずつ通す。後の1通は、前の1通を送り終えてから振り分け直すので、順に送った時と同じ結果になる(起動直後のClaude Codeへ重なった後の1通は`mode=agent_steer`で返る)。前の1通が起動直後の入力受付待ちなら、その分だけ返りが遅れる。待つ上限(POSIX 60秒、Windows 180秒)を越えた時は、打鍵前に`AGENT_SEND_BUSY`と「文字列は送信していません」を返す。送信の途中で終了したprocessのlockは、次の送信が片付けて進む。
|
|
265
267
|
|
|
266
268
|
`agent_launch`・`pty_send`(agent session宛て)は任意の`image`(画像ファイルの絶対パスの配列。png/jpg/jpeg/gif/webp)を受ける。aitermが本文末尾へ添付行を付け、どのharnessも自分のfile読取toolでそのpathを画像として開く。呼出し側はharness別の添付手順を覚えない。不正なpathは送信前に拒否する。
|
|
@@ -399,6 +401,9 @@ pty_close("t1") → 端末を解放
|
|
|
399
401
|
|
|
400
402
|
`pty_close` は冪等で、`closed` / `already_closed` のstructured receiptを返す。
|
|
401
403
|
MCP応答を失ったdurable callerも同じ`session_id`への再試行だけでclose結果を確定できる。
|
|
404
|
+
Windowsでは、Claude Codeの席を閉じる前に、Claude Codeへ自分で終わる機会を与える(C-cを2回送り、最大4秒待つ)。
|
|
405
|
+
psmuxは、POSIXのtmuxが送る切断の合図なしでsessionを止めるので、そのままでは会話終了のhook(SessionEnd)が走らない。
|
|
406
|
+
終わっても終わらなくても、その後にsessionを止める。
|
|
402
407
|
|
|
403
408
|
これだけ。`t1` の端末は本物で永続——`ssh`・`docker exec`・REPL・起動したエージェントのTUIは、そこに住む「もの」に過ぎない。ワーカー起動も1コールで、`agent_launch({ harness: "codex-cli" })`が返す`session_id`を同じ`pty_read`/`pty_send`で操作する。
|
|
404
409
|
|
|
@@ -568,7 +573,7 @@ Claudeの相関済み承認は既存の`claude_approval`を使う。
|
|
|
568
573
|
| ツール | 役割 | 主な引数 |
|
|
569
574
|
| --- | --- | --- |
|
|
570
575
|
| `pty_open` | 端末を1個開き`session_id`を返す | `name?`, `shell?`, `env_vars?` |
|
|
571
|
-
| `pty_send` | テキストを送る。agent sessionでは子のturnが実行中なら現在のturnへ差し込み(`agent_steer`)、それ以外は非ブロックdispatchとして`event_cursor`を返す(`agent_dispatch`)。差し込みでGrokが待ち行列へ入れない時とCursorの入力欄に残った時は失敗する | `session_id`, `text`, `enter=true`, `mark`, `force`, `require_agent=false`, `rtk`, `raw` |
|
|
576
|
+
| `pty_send` | テキストを送る。agent sessionでは子のturnが実行中なら現在のturnへ差し込み(`agent_steer`)、それ以外は非ブロックdispatchとして`event_cursor`を返す(`agent_dispatch`)。差し込みでGrokが待ち行列へ入れない時とCursorの入力欄に残った時は失敗する | `session_id`, `text`, `enter=true`, `mark`, `force`, `require_agent=false`, `preface`, `rtk`, `raw` |
|
|
572
577
|
| `pty_read` | 出力を削減して読む(既定は増分) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk`, `agent_transcript`, `operation_id` |
|
|
573
578
|
| `pty_key` | 制御キーを送る | `session_id`, `key`(`C-c`/`Enter`/`Up`…) |
|
|
574
579
|
| `pty_close` | 冪等に閉じ、`closed` / `already_closed`を返す | `session_id` |
|
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.57.1** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
|
|
227
227
|
|
|
228
228
|
### Update and rollback
|
|
229
229
|
|
|
@@ -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
|
+
`pty_send` to an agent session accepts an optional `preface`: one line placed before the text, so the message reads "`preface`, blank line, `text`". For a Claude Code session Aiterm enters the `preface` without the terminal's paste markers and pastes the blank line and the text in one paste. Claude Code wraps long pasted text in `<pasted_content>` and follows instructions inside it only where the user's own words outside the tags ask it to, so this keeps an integration's header (who the message is from) outside the wrapper. For Codex, Grok and Cursor sessions the three parts are joined and pasted as before. The `preface` must be a single line of at most 200 characters with no control characters, must not start with a symbol or whitespace, and must not contain `@`; otherwise Aiterm returns `AGENT_PREFACE_INVALID` and "文字列は送信していません。" before sending anything. It cannot be used with ordinary PTY sends, `force:true`, or `remote`.
|
|
286
|
+
|
|
285
287
|
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
288
|
|
|
287
289
|
`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.
|
|
@@ -429,6 +431,9 @@ pty_close("t1") → terminal released
|
|
|
429
431
|
|
|
430
432
|
`pty_close` is idempotent and returns a structured `closed` / `already_closed`
|
|
431
433
|
receipt, so durable callers can retry the same `session_id` after losing the MCP response.
|
|
434
|
+
On Windows, closing a Claude Code session first asks Claude Code to exit on its own (Ctrl-C twice, then a wait of up to 4 seconds)
|
|
435
|
+
so that its `SessionEnd` hooks run; psmux stops a session without the hangup signal that tmux sends on POSIX. The session is stopped
|
|
436
|
+
afterwards either way.
|
|
432
437
|
|
|
433
438
|
That's it. The terminal in `t1` is real and persistent — `ssh`, `docker exec`, a REPL, or a launched agent's TUI are just things that live inside it. To launch a worker agent instead, one call does it: `agent_launch({ harness: "codex-cli" })` returns a `session_id` you drive with the same `pty_read` / `pty_send`.
|
|
434
439
|
|
|
@@ -606,7 +611,7 @@ continue to use `claude_approval`.
|
|
|
606
611
|
| Tool | Role | Key args |
|
|
607
612
|
| --- | --- | --- |
|
|
608
613
|
| `pty_open` | Open one terminal and return a `session_id` | `name?`, `shell?`, `env_vars?` |
|
|
609
|
-
| `pty_send` | Send text. On an agent session Aiterm picks the route when it sends: if the child's turn is running, it steers the text into that turn (`agent_steer`); otherwise it is a non-blocking **dispatch** returning an `event_cursor` (`agent_dispatch`). Steering fails when Grok does not queue the text or Cursor leaves it in the composer | `session_id`, `text`, `enter=true`, `mark`, `force`, `require_agent=false`, `rtk`, `raw` |
|
|
614
|
+
| `pty_send` | Send text. On an agent session Aiterm picks the route when it sends: if the child's turn is running, it steers the text into that turn (`agent_steer`); otherwise it is a non-blocking **dispatch** returning an `event_cursor` (`agent_dispatch`). Steering fails when Grok does not queue the text or Cursor leaves it in the composer | `session_id`, `text`, `enter=true`, `mark`, `force`, `require_agent=false`, `preface`, `rtk`, `raw` |
|
|
610
615
|
| `pty_read` | Read output, token-reduced (incremental by default) | `session_id`, `wait`, `until`, `until_regex`, `timeout`, `screen`, `full`, `lines`, `line_range`, `raw`, `rtk`, `agent_transcript`, `operation_id` |
|
|
611
616
|
| `pty_key` | Send a control key | `session_id`, `key` (`C-c`/`Enter`/`Up`…) |
|
|
612
617
|
| `pty_close` | Close idempotently; return `closed` / `already_closed` | `session_id` |
|
package/dist/core.js
CHANGED
|
@@ -22,7 +22,7 @@ import { isWin, SOCKDIR, tmuxCommand, sendPsmuxPayload, loadPtyBufferChunk, past
|
|
|
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";
|
|
23
23
|
import { catalogUnavailable } from "./model-catalog.js";
|
|
24
24
|
import { grokAuthPlan, grokAuthStatus, grokAuthPane, realGrokHome, resolveAndValidateGrokAuth, assertGrokModelAvailable, grokModelChoices, grokEventsTranscript, latestGrokCompletion, grokTurnSettled, observeGrokDone, buildGrokAgentCmd, grokLaunchNote, grokEnvTokens, grokTuiReady, grokTuiBusy, grokPaneObservation, grokRateLimitDialog, grokStartupAction, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE, grokFooterHasConfiguration, grokTranscriptText, createGrokAgentMetadata, } from "./harnesses/grok.js";
|
|
25
|
-
import { codexAuthPlan, codexAuthStatus, codexAuthPane, bindCodexTranscriptSession, latestCodexCompletion, codexTurnSettled, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexTuiBusy, codexHelperProcess, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, } from "./harnesses/codex.js";
|
|
25
|
+
import { codexAuthPlan, codexAuthStatus, codexAuthPane, bindCodexTranscriptSession, latestCodexCompletion, codexTurnSettled, codexRunningTurn, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexTuiBusy, codexHelperProcess, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, } from "./harnesses/codex.js";
|
|
26
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";
|
|
27
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";
|
|
28
28
|
import { readInterimWords, recordInterimBoundary } from "./interim-words.js";
|
|
@@ -762,6 +762,38 @@ export function openSession(name, shell = "bash", envVars = []) {
|
|
|
762
762
|
}
|
|
763
763
|
return [nm, attachHint(nm)];
|
|
764
764
|
}
|
|
765
|
+
export const AGENT_PREFACE_MAX_CHARS = 200;
|
|
766
|
+
// 行頭のASCII記号と空白は、入力欄が命令として読む(Claude Codeの`/`・`!`・`#`・`?`など)。`@`は文中でも候補の枠を開く。
|
|
767
|
+
const AGENT_PREFACE_LEADING_SYMBOL_RE = /^[\s!-\/:-@\[-`{-~]/u;
|
|
768
|
+
const AGENT_PREFACE_CONTROL_RE = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/u;
|
|
769
|
+
/**
|
|
770
|
+
* `pty_send`の`preface`(本文の前に置く1行)を、打鍵の前に確かめる。印なしで入れる文は入力欄がそのまま読むので、
|
|
771
|
+
* 入力欄が命令として読む形を通さない。外れた時は何も送らずに断る。
|
|
772
|
+
*/
|
|
773
|
+
export function assertAgentPreface(preface) {
|
|
774
|
+
const reason = typeof preface !== "string" || preface.length === 0 ? "空です"
|
|
775
|
+
: AGENT_PREFACE_CONTROL_RE.test(preface) ? "改行か制御文字を含んでいます"
|
|
776
|
+
: [...preface].length > AGENT_PREFACE_MAX_CHARS ? `${AGENT_PREFACE_MAX_CHARS}字を越えています`
|
|
777
|
+
: AGENT_PREFACE_LEADING_SYMBOL_RE.test(preface) ? "行頭が記号か空白です"
|
|
778
|
+
: /\s$/u.test(preface) ? "行末が空白です"
|
|
779
|
+
: preface.includes("@") ? "@ を含んでいます"
|
|
780
|
+
: null;
|
|
781
|
+
if (reason) {
|
|
782
|
+
throw new AitermError(`AGENT_PREFACE_INVALID: prefaceが${reason}。1行・${AGENT_PREFACE_MAX_CHARS}字以内・制御文字なしで、` +
|
|
783
|
+
"行頭が記号や空白でなく、@ を含まない文にしてください。文字列は送信していません。", 2);
|
|
784
|
+
}
|
|
785
|
+
return preface;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* 前置きつきの送信文を、harnessごとの形へ分ける。Claude Codeは前置きを印なしで入れ、空行と本文を貼る。
|
|
789
|
+
* Codex・Grok・Cursorは貼り付けを包まないので、前置き・空行・本文を1つにつないで貼る(渡る文は前置きなしの時と同じ並び)。
|
|
790
|
+
*/
|
|
791
|
+
function splitAgentPreface(kind, preface, text) {
|
|
792
|
+
if (preface == null)
|
|
793
|
+
return { body: text };
|
|
794
|
+
assertAgentPreface(preface);
|
|
795
|
+
return kind === "claude" ? { typedPreface: preface, body: `\n\n${text}` } : { body: `${preface}\n\n${text}` };
|
|
796
|
+
}
|
|
765
797
|
function prepareSendText(text, o) {
|
|
766
798
|
if (!o.raw)
|
|
767
799
|
text = text.replace(PASTE_MARKERS_RE, "").replace(ANSI_RE, "").replace(CTRL_RE, "");
|
|
@@ -823,17 +855,18 @@ export function send(name, text, o = {}) {
|
|
|
823
855
|
}
|
|
824
856
|
// POSIX tmuxはUTF-8安全な256byte pasteへ分割する。Windows psmuxは認証済み
|
|
825
857
|
// server commandでpayload全体を単一SendBytesへ変換し、別processとの交差を構造的に防ぐ。
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
858
|
+
const writePty = (payload, bracketedPaste, what) => {
|
|
859
|
+
if (isWin) {
|
|
860
|
+
const sent = sendPsmuxPayload(true, name, payload, bracketedPaste);
|
|
861
|
+
if (sent.code !== 0) {
|
|
862
|
+
throw new AitermError(`psmuxのPTY送信に失敗しました${what}: ${sent.stderr.trim() || `code=${sent.code}`}`, 2);
|
|
863
|
+
}
|
|
864
|
+
return;
|
|
830
865
|
}
|
|
831
|
-
}
|
|
832
|
-
else {
|
|
833
866
|
// tmuxの`paste-buffer -p`はchunkごとに包むため使わない。Claude Codeは貼り付け1回ごとに
|
|
834
867
|
// 画像pathを探すので、chunk境界でpathが切れて画像を落とし、行もずれた(2026-10-01、
|
|
835
868
|
// 画像4枚付きの約500byte)。全体を1回だけ包み、中身はchunkのまま流す。
|
|
836
|
-
const chunks = splitPtyText(
|
|
869
|
+
const chunks = splitPtyText(bracketedPaste ? `\x1b[200~${payload}\x1b[201~` : payload);
|
|
837
870
|
const pasteSupportsNoSanitize = pasteBufferSupportsNoSanitizeFlag();
|
|
838
871
|
const bufferBase = `aiterm-${process.pid}-${randomBytes(8).toString("hex")}`;
|
|
839
872
|
for (let i = 0; i < chunks.length; i += 1) {
|
|
@@ -844,7 +877,7 @@ export function send(name, text, o = {}) {
|
|
|
844
877
|
const loaded = loadPtyBufferChunk(true, bufferName, chunks[i]);
|
|
845
878
|
if (loaded.code !== 0) {
|
|
846
879
|
tmuxCleanup("delete-buffer", "-b", bufferName);
|
|
847
|
-
throw new AitermError(`tmux buffer
|
|
880
|
+
throw new AitermError(`tmux bufferへの送信準備に失敗しました${what}` +
|
|
848
881
|
`(chunk ${i + 1}/${chunks.length}): ${loaded.stderr.trim() || `code=${loaded.code}`}.${partial}`, 2);
|
|
849
882
|
}
|
|
850
883
|
const pasteArgs = pasteBufferBaseArgs();
|
|
@@ -854,14 +887,30 @@ export function send(name, text, o = {}) {
|
|
|
854
887
|
const pasted = tmux(...pasteArgs);
|
|
855
888
|
if (pasted.code !== 0) {
|
|
856
889
|
tmuxCleanup("delete-buffer", "-b", bufferName);
|
|
857
|
-
throw new AitermError(`tmux bufferのPTY
|
|
890
|
+
throw new AitermError(`tmux bufferのPTY送信に失敗しました${what}` +
|
|
858
891
|
`(chunk ${i + 1}/${chunks.length}): ${pasted.stderr.trim() || `code=${pasted.code}`}.${partial}`, 2);
|
|
859
892
|
}
|
|
860
893
|
if (i + 1 < chunks.length) {
|
|
861
894
|
Atomics.wait(PTY_PASTE_PAUSE_BUFFER, 0, 0, PTY_PASTE_CHUNK_PAUSE_MS);
|
|
862
895
|
}
|
|
863
896
|
}
|
|
897
|
+
};
|
|
898
|
+
if (o.typedPreface !== undefined) {
|
|
899
|
+
// 前置きは貼り付けの印なしで入れる。本文は続けて貼る。前置きだけが入って本文が失敗した時は、入力欄に前置きが残る。
|
|
900
|
+
writePty(o.typedPreface, false, "(前置き)");
|
|
901
|
+
Atomics.wait(PTY_PASTE_PAUSE_BUFFER, 0, 0, PTY_PASTE_CHUNK_PAUSE_MS);
|
|
902
|
+
try {
|
|
903
|
+
writePty(text, !!o.bracketedPaste, "");
|
|
904
|
+
}
|
|
905
|
+
catch (error) {
|
|
906
|
+
if (error instanceof AitermError) {
|
|
907
|
+
throw new AitermError(`${error.message} 前置きはPTYに入力済みでEnterは未送信です。再送前に入力を確認・消去してください。`, error.code);
|
|
908
|
+
}
|
|
909
|
+
throw error;
|
|
910
|
+
}
|
|
864
911
|
}
|
|
912
|
+
else
|
|
913
|
+
writePty(text, !!o.bracketedPaste, "");
|
|
865
914
|
if (enter) {
|
|
866
915
|
const entered = tmux("send-keys", "-t", name, "Enter");
|
|
867
916
|
if (entered.code !== 0) {
|
|
@@ -1337,8 +1386,8 @@ export function readOnlyPtyListDiagnostic(runTmux = tmux) {
|
|
|
1337
1386
|
}
|
|
1338
1387
|
return { status: "unverified", session_count: null };
|
|
1339
1388
|
}
|
|
1340
|
-
|
|
1341
|
-
|
|
1389
|
+
/** 別processが待機中・送信中のsessionは閉じない。閉じる前の操作(下のClaude Codeへの終了の鍵)より先に確かめる。 */
|
|
1390
|
+
function assertSessionClosable(name) {
|
|
1342
1391
|
{
|
|
1343
1392
|
// 別プロセスの待機は in-memory Set に映らない。生きた file lock があれば close で state を消さない
|
|
1344
1393
|
const foreign = liveWaitLocks(name);
|
|
@@ -1354,6 +1403,80 @@ function closeSessionInternal(name, observeDependency = true) {
|
|
|
1354
1403
|
throw new AitermError(`session '${name}' は別プロセス${d.pid != null ? `(pid ${d.pid})` : ""}の送信中のため close できません`, 2);
|
|
1355
1404
|
}
|
|
1356
1405
|
}
|
|
1406
|
+
}
|
|
1407
|
+
// Windowsのpsmuxは、sessionを止める時に画面のprocessへ切断の合図を送らない(POSIXのtmuxはSIGHUPを送り、Claude Codeは
|
|
1408
|
+
// 片付けて終わる)。合図なしで止められたClaude Codeは、会話終了のhook(SessionEnd)を走らせない。
|
|
1409
|
+
// 止める前に、Claude Codeへ自分で終わる機会を与える(ADR 0100)。
|
|
1410
|
+
// 動いている番を止めた後、入力待ちに戻るのを待つ上限。
|
|
1411
|
+
const CLAUDE_EXIT_INTERRUPT_WAIT_MS = 1500;
|
|
1412
|
+
// 終了の受付(「Press Ctrl-C again to exit」)は短い。実物では、間が0.3〜0.7秒の2回は通り、2.2秒の2回は切れた。
|
|
1413
|
+
const CLAUDE_EXIT_KEY_GAP_MS = 200;
|
|
1414
|
+
// 2回目の後、会話終了のhookを走らせて終わるのを待つ上限。実物(fox)では、hookが走ったのは2回目の約1秒後で、processはその
|
|
1415
|
+
// 0.2〜0.3秒後に終わった。hookが遅い端末でも走り切れるように、余りを取る。
|
|
1416
|
+
const CLAUDE_EXIT_WAIT_MS = 4000;
|
|
1417
|
+
const CLAUDE_EXIT_POLL_MS = 100;
|
|
1418
|
+
/**
|
|
1419
|
+
* Claude Codeへ終了を頼む鍵と待ちの順番。
|
|
1420
|
+
* 入力待ちでない席(動いている番など)へは、先にC-cを1回送って入力待ちに戻るのを待つ(そのC-cは番を止めるのに使われ、
|
|
1421
|
+
* 終了の受付にならない)。その後、C-cを間を空けずに2回送り、processが終わるのを待つ。
|
|
1422
|
+
* 終わらなかった時も、呼ぶ側は今までどおりsessionを止める。
|
|
1423
|
+
*/
|
|
1424
|
+
export function requestClaudeExit(steps) {
|
|
1425
|
+
if (!steps.idle()) {
|
|
1426
|
+
steps.interrupt();
|
|
1427
|
+
const until = steps.now() + CLAUDE_EXIT_INTERRUPT_WAIT_MS;
|
|
1428
|
+
while (!steps.idle() && steps.now() < until)
|
|
1429
|
+
steps.pause(CLAUDE_EXIT_POLL_MS);
|
|
1430
|
+
}
|
|
1431
|
+
steps.interrupt();
|
|
1432
|
+
steps.pause(CLAUDE_EXIT_KEY_GAP_MS);
|
|
1433
|
+
steps.interrupt();
|
|
1434
|
+
const until = steps.now() + CLAUDE_EXIT_WAIT_MS;
|
|
1435
|
+
for (;;) {
|
|
1436
|
+
if (!steps.alive())
|
|
1437
|
+
return "exited";
|
|
1438
|
+
if (steps.now() >= until)
|
|
1439
|
+
return "still_running";
|
|
1440
|
+
steps.pause(CLAUDE_EXIT_POLL_MS);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
let claudeExitBeforeCloseTestOverride = null;
|
|
1444
|
+
/** 試験用: POSIXでも、閉じる前にClaude Codeへ終了を頼む(nullで既定へ戻す)。 */
|
|
1445
|
+
export function __testSetClaudeExitBeforeClose(value) {
|
|
1446
|
+
claudeExitBeforeCloseTestOverride = value;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* 閉じる席がWindowsのClaude Codeで、processが生きている時だけ、止める前に終了を頼む。
|
|
1450
|
+
* 待ちは同期のまま(最大で数秒)。待つ間にほかの呼び出しを通すと、閉じかけの席へ送信が入る。
|
|
1451
|
+
* 席の観測や鍵の送信に失敗した時は、何もせずに今までどおり止める(閉じる事を妨げない)。
|
|
1452
|
+
*/
|
|
1453
|
+
function letClaudeExitBeforeClose(name) {
|
|
1454
|
+
if (!(claudeExitBeforeCloseTestOverride ?? isWin))
|
|
1455
|
+
return;
|
|
1456
|
+
try {
|
|
1457
|
+
if (tryLoadAgentMetadata(name)?.kind !== "claude" || !sessionExists(name))
|
|
1458
|
+
return;
|
|
1459
|
+
const seen = observeSession(name);
|
|
1460
|
+
if (seen.harness_alive !== true)
|
|
1461
|
+
return;
|
|
1462
|
+
const harness = seen.harness_process;
|
|
1463
|
+
const pause = new Int32Array(new SharedArrayBuffer(4));
|
|
1464
|
+
requestClaudeExit({
|
|
1465
|
+
idle: () => claudePaneObservation(captureScreen(name, AGENT_TUI_READY_LINES)).state === "idle",
|
|
1466
|
+
interrupt: () => { tmux("send-keys", "-t", name, "C-c"); },
|
|
1467
|
+
// processが1つに決まらない席は、席の観測で見る(Windowsではprocessの一覧を引くので遅い)。
|
|
1468
|
+
alive: () => harness ? isPidAlive(harness.pid) : observeSession(name).harness_alive === true,
|
|
1469
|
+
pause: ms => { Atomics.wait(pause, 0, 0, ms); },
|
|
1470
|
+
now: () => performance.now(),
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
catch { /* 終了を頼めなかった席は、今までどおり止める */ }
|
|
1474
|
+
}
|
|
1475
|
+
function closeSessionInternal(name, observeDependency = true, o = {}) {
|
|
1476
|
+
assertSessionName(name);
|
|
1477
|
+
assertSessionClosable(name);
|
|
1478
|
+
if (o.letHarnessExit)
|
|
1479
|
+
letClaudeExitBeforeClose(name);
|
|
1357
1480
|
(observeDependency ? tmux : tmuxCleanup)("kill-session", "-t", name);
|
|
1358
1481
|
for (const p of [logpath(name), offsetpath(name), lastcmdpath(name), markpath(name), sendLockPath(name)]) {
|
|
1359
1482
|
try {
|
|
@@ -1381,7 +1504,7 @@ export function closeSession(name) {
|
|
|
1381
1504
|
export function closeSessionResult(name) {
|
|
1382
1505
|
assertSessionName(name);
|
|
1383
1506
|
const existed = sessionExists(name);
|
|
1384
|
-
closeSessionInternal(name, true);
|
|
1507
|
+
closeSessionInternal(name, true, { letHarnessExit: true });
|
|
1385
1508
|
return {
|
|
1386
1509
|
schema: "aiterm.pty-close-result.v1",
|
|
1387
1510
|
session_id: name,
|
|
@@ -4259,9 +4382,10 @@ async function dispatchAgentTurnInLock(name, text, o) {
|
|
|
4259
4382
|
const startOffset = agentCompletionCursor(meta);
|
|
4260
4383
|
// promptなしで起動したCursorは、最初のdispatch時点ではtranscriptとの相関markerをまだ持たない。
|
|
4261
4384
|
// その1回だけlaunch contextを加え、以後はbind済みconversationへ通常textだけを送る。
|
|
4385
|
+
const prefaced = splitAgentPreface(meta.kind, o.preface, text);
|
|
4262
4386
|
const dispatchText = meta.kind === "cursor" && !meta.vendor_session_id
|
|
4263
|
-
? `${subagentInstruction(meta)}\n\n${
|
|
4264
|
-
:
|
|
4387
|
+
? `${subagentInstruction(meta)}\n\n${prefaced.body}`
|
|
4388
|
+
: prefaced.body;
|
|
4265
4389
|
prepareSendText(dispatchText, { raw: o.raw });
|
|
4266
4390
|
await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
|
|
4267
4391
|
harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: operationId });
|
|
@@ -4284,6 +4408,7 @@ async function dispatchAgentTurnInLock(name, text, o) {
|
|
|
4284
4408
|
rtk: false,
|
|
4285
4409
|
preserveAgentOperation: meta.kind === "claude",
|
|
4286
4410
|
bracketedPaste: true,
|
|
4411
|
+
typedPreface: prefaced.typedPreface,
|
|
4287
4412
|
});
|
|
4288
4413
|
// Cursorの冷間起動ではbracketed pasteの反映が250msを超えることがある。本文がcomposerへ
|
|
4289
4414
|
// 現れた実測をsubmit条件にし、未反映のままEnterだけを失う競合を作らない。
|
|
@@ -4345,6 +4470,39 @@ async function waitSteerScreen(name, predicate, stillRunning) {
|
|
|
4345
4470
|
export function __testSteerQueued(kind, screen) {
|
|
4346
4471
|
return kind === "cursor" ? cursorSteerQueued(screen) : grokSteerQueued(screen);
|
|
4347
4472
|
}
|
|
4473
|
+
// 記録の上でturnが動いている席を、間を置いて見直すまでの待ち。回答を流している画面は、この間に必ず動く(実測で0.15秒ごと)。
|
|
4474
|
+
const CODEX_RECORD_RUNNING_CONFIRM_MS = 400;
|
|
4475
|
+
/**
|
|
4476
|
+
* Codexの席で、画面は入力待ちに見えるが、記録の上ではturnが動いているか(回答を流している間。Codex 0.160.1は、その間
|
|
4477
|
+
* 動作中の行を出さない)。
|
|
4478
|
+
* 止まっている席を動いていると読むと、文は新しいturnになるのに差し込みとして返り、その完了を誰も待たない。
|
|
4479
|
+
* 読み違えない方へ倒し、次を全部満たす時だけtrueにする。1つでも欠けたら、今までどおり新しいturnの道へ回す。
|
|
4480
|
+
* - 画面に入力欄が見えている(承認などの画面は、新しいturnの道が今までどおり打鍵前に断る)。
|
|
4481
|
+
* - この起動の記録の最後の境界がturnの開始で、harnessのprocessが生きている(Codexは、生きている間はturnの終わりを必ず書く)。
|
|
4482
|
+
* - 間を置いて見直しても、記録は同じturnの開始のままで、画面が動いている(止まっている席の画面は動かない)。
|
|
4483
|
+
*/
|
|
4484
|
+
async function codexTurnRunningBehindIdleScreen(name, meta, screen) {
|
|
4485
|
+
try {
|
|
4486
|
+
if (!isAgentTuiIdleReady("codex", screen))
|
|
4487
|
+
return false;
|
|
4488
|
+
const turn = codexRunningTurn(meta);
|
|
4489
|
+
if (turn === null)
|
|
4490
|
+
return false;
|
|
4491
|
+
if (observeSession(name).harness_alive !== true)
|
|
4492
|
+
return false;
|
|
4493
|
+
await sleep(CODEX_RECORD_RUNNING_CONFIRM_MS);
|
|
4494
|
+
const later = captureScreen(name, AGENT_TUI_READY_LINES);
|
|
4495
|
+
if (codexRunningTurn(meta) !== turn)
|
|
4496
|
+
return false;
|
|
4497
|
+
// 見直す間に動作中の行が出た時は、画面の読みでも動いている。
|
|
4498
|
+
if (isAgentTuiBusy("codex", later))
|
|
4499
|
+
return true;
|
|
4500
|
+
return later !== screen && isAgentTuiIdleReady("codex", later);
|
|
4501
|
+
}
|
|
4502
|
+
catch {
|
|
4503
|
+
return false; /* 読めない時は、動いていると数えない */
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4348
4506
|
/**
|
|
4349
4507
|
* agent sessionへの唯一の送信口。呼び出し側は子の状態を知らないまま呼び、Aitermがこの時点の画面で振り分ける。
|
|
4350
4508
|
* 実行中なら現在のturnへ差し込み(steer)、そうでなければ新しいturnとしてdispatchする。
|
|
@@ -4354,13 +4512,17 @@ export function __testSteerQueued(kind, screen) {
|
|
|
4354
4512
|
export async function sendAgentMessage(name, text, o = {}) {
|
|
4355
4513
|
assertSessionName(name);
|
|
4356
4514
|
loadAgentMetadata(name);
|
|
4515
|
+
// 前置きは、順番待ちにも入力受付の待ちにも入る前に確かめる(外れた時に待たせない)。
|
|
4516
|
+
if (o.preface != null)
|
|
4517
|
+
assertAgentPreface(o.preface);
|
|
4357
4518
|
return withAgentSendLock(name, () => sendAgentMessageInLock(name, text, o));
|
|
4358
4519
|
}
|
|
4359
4520
|
async function sendAgentMessageInLock(name, text, o) {
|
|
4360
4521
|
const meta = loadAgentMetadata(name);
|
|
4361
4522
|
// 前面回復は busy 判定より先(bash 前面のままだと画面の実行中マーカーを読んでも打鍵が届かない)。
|
|
4362
4523
|
const paneInputRecovery = await ensureAgentOwnsPaneInput(name, meta.kind);
|
|
4363
|
-
|
|
4524
|
+
const screen = captureScreen(name, AGENT_TUI_READY_LINES);
|
|
4525
|
+
let running = isAgentTuiBusy(meta.kind, screen);
|
|
4364
4526
|
// Claudeのtool処理中は画面のbusy表示が消えることがある。Stopまで保持するturnの印を正とし、
|
|
4365
4527
|
// APIエラーでStopが来なかった印は、会話記録の終了時刻を確認して消す。
|
|
4366
4528
|
// 有効な印がある間は差し込み、終了後は画面にbusy表示が残っても新しいturnとして送る。
|
|
@@ -4373,10 +4535,13 @@ async function sendAgentMessageInLock(name, text, o) {
|
|
|
4373
4535
|
// turnは終わっているので新しいturnとして送る。dispatchは入力を受け付けるまで待ってから送る。
|
|
4374
4536
|
if (running && meta.kind === "cursor" && latestCursorCompletion(meta, readTranscriptLines) !== null)
|
|
4375
4537
|
running = false;
|
|
4538
|
+
// Codexは回答を流している間、画面に動作中の行を出さない。その間に届いた文を、Codexは動いているturnへ取り込む(ADR 0101)。
|
|
4539
|
+
if (!running && meta.kind === "codex" && await codexTurnRunningBehindIdleScreen(name, meta, screen))
|
|
4540
|
+
running = true;
|
|
4376
4541
|
if (running) {
|
|
4377
|
-
return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery });
|
|
4542
|
+
return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery, preface: o.preface });
|
|
4378
4543
|
}
|
|
4379
|
-
return dispatchAgentTurnInLock(name, text, { raw: o.raw, before_send: o.before_send, pane_input_recovery: paneInputRecovery });
|
|
4544
|
+
return dispatchAgentTurnInLock(name, text, { raw: o.raw, before_send: o.before_send, pane_input_recovery: paneInputRecovery, preface: o.preface });
|
|
4380
4545
|
}
|
|
4381
4546
|
/**
|
|
4382
4547
|
* 実行中turnへ追加の指示を渡す。各harnessの標準操作だけを使い、完了境界は1つに保つ。
|
|
@@ -4395,6 +4560,8 @@ async function steerRunningTurn(name, meta, text, o) {
|
|
|
4395
4560
|
harness: agentHarness(meta.kind),
|
|
4396
4561
|
pane_input_recovery: o.pane_input_recovery,
|
|
4397
4562
|
};
|
|
4563
|
+
const prefaced = splitAgentPreface(meta.kind, o.preface, text);
|
|
4564
|
+
text = prefaced.body;
|
|
4398
4565
|
prepareSendText(text, { raw: o.raw });
|
|
4399
4566
|
// Claudeのoperation相関は変えない。差し込みは実行中turnの一部であり、新しいturnを予約しない。
|
|
4400
4567
|
const preserveAgentOperation = meta.kind === "claude";
|
|
@@ -4406,6 +4573,7 @@ async function steerRunningTurn(name, meta, text, o) {
|
|
|
4406
4573
|
rtk: false,
|
|
4407
4574
|
preserveAgentOperation,
|
|
4408
4575
|
bracketedPaste: true,
|
|
4576
|
+
typedPreface: prefaced.typedPreface,
|
|
4409
4577
|
});
|
|
4410
4578
|
if (meta.kind === "cursor") {
|
|
4411
4579
|
const visible = await waitCursorPromptVisible(name, text);
|
package/dist/harnesses/codex.js
CHANGED
|
@@ -262,24 +262,23 @@ export function codexUsageLimit(record) {
|
|
|
262
262
|
return error?.info === "usage_limit_exceeded" ? error.message : null;
|
|
263
263
|
}
|
|
264
264
|
/**
|
|
265
|
-
* この起動のroot rollout
|
|
266
|
-
* Codexはturnの開始でtask_started、終わりでtask_complete
|
|
267
|
-
*
|
|
268
|
-
* rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はfalse(分からない時は「終わった」と数えない)。
|
|
265
|
+
* この起動のroot rolloutの、末尾から見て最初のturnの境界。
|
|
266
|
+
* Codexはturnの開始でtask_started、終わりでtask_complete、止めた時はturn_aborted(reasonはinterrupted)を書く(0.160.1の実記録)。
|
|
267
|
+
* rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はnull。
|
|
269
268
|
*/
|
|
270
|
-
|
|
269
|
+
function codexLatestTurnBoundary(meta) {
|
|
271
270
|
let transcript;
|
|
272
271
|
try {
|
|
273
272
|
transcript = codexRootTranscript(meta);
|
|
274
273
|
}
|
|
275
274
|
catch {
|
|
276
|
-
return
|
|
275
|
+
return null;
|
|
277
276
|
}
|
|
278
277
|
if (!transcript)
|
|
279
|
-
return
|
|
278
|
+
return null;
|
|
280
279
|
const lines = readTailLinesNewestFirst(transcript, AGENT_TURN_BOUNDARY_TAIL_BYTES);
|
|
281
280
|
if (!lines)
|
|
282
|
-
return
|
|
281
|
+
return null;
|
|
283
282
|
for (const line of lines) {
|
|
284
283
|
if (!line.includes("task_started") && !line.includes("task_complete") && !line.includes("turn_aborted"))
|
|
285
284
|
continue;
|
|
@@ -288,19 +287,36 @@ export function codexTurnSettled(meta) {
|
|
|
288
287
|
record = JSON.parse(line);
|
|
289
288
|
}
|
|
290
289
|
catch {
|
|
291
|
-
return
|
|
290
|
+
return null;
|
|
292
291
|
}
|
|
293
292
|
if (record?.type !== "event_msg")
|
|
294
293
|
continue;
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return typeof record.payload.turn_id === "string" && record.payload.turn_id !== "";
|
|
299
|
-
// 見たのはEscで止めた時の形だけ。他の理由(別のturnへの置き換え等)は、次のturnが続くかも知れないので数えない。
|
|
300
|
-
if (record?.payload?.type === "turn_aborted")
|
|
301
|
-
return record.payload.reason === "interrupted";
|
|
294
|
+
const type = record?.payload?.type;
|
|
295
|
+
if (type === "task_started" || type === "task_complete" || type === "turn_aborted")
|
|
296
|
+
return { type, turn_id: record.payload.turn_id, reason: record.payload.reason };
|
|
302
297
|
}
|
|
303
|
-
return
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* この起動のroot rolloutで、最後のturnが終わっていて、次のturnが始まっていないか。
|
|
302
|
+
* 末尾から見て最初の境界が、task_completeか、止めた時のturn_abortedの時だけtrue。
|
|
303
|
+
* rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はfalse(分からない時は「終わった」と数えない)。
|
|
304
|
+
*/
|
|
305
|
+
export function codexTurnSettled(meta) {
|
|
306
|
+
const boundary = codexLatestTurnBoundary(meta);
|
|
307
|
+
if (boundary?.type === "task_complete")
|
|
308
|
+
return typeof boundary.turn_id === "string" && boundary.turn_id !== "";
|
|
309
|
+
// 見たのはEscで止めた時の形だけ。他の理由(別のturnへの置き換え等)は、次のturnが続くかも知れないので数えない。
|
|
310
|
+
return boundary?.type === "turn_aborted" && boundary.reason === "interrupted";
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* この起動のroot rolloutの上で動いているturnのID。末尾から見て最初の境界がtask_startedの時だけ返す。
|
|
314
|
+
* rolloutが無い、この起動に結び付かない、書いている途中、境界が読む範囲に無い時はnull(分からない時は「動いている」と数えない)。
|
|
315
|
+
* Codexは、processが生きている間、turnの終わりを必ず書く。書かずに終わるのは落ちた時なので、呼ぶ側はprocessの生死も見る。
|
|
316
|
+
*/
|
|
317
|
+
export function codexRunningTurn(meta) {
|
|
318
|
+
const boundary = codexLatestTurnBoundary(meta);
|
|
319
|
+
return boundary?.type === "task_started" && typeof boundary.turn_id === "string" && boundary.turn_id !== "" ? boundary.turn_id : null;
|
|
304
320
|
}
|
|
305
321
|
export function latestCodexCompletion(meta, readTranscriptLines) {
|
|
306
322
|
const transcript = codexRootTranscript(meta);
|
package/dist/index.js
CHANGED
|
@@ -209,6 +209,9 @@ function remoteToolHandler(name) {
|
|
|
209
209
|
async function sendRemote(target, args, extra) {
|
|
210
210
|
if (args.image && args.image.length > 0)
|
|
211
211
|
throw new core.AitermError("REMOTE_IMAGE_UNSUPPORTED: 別端末への画像添付には未対応です", 2);
|
|
212
|
+
// 現地が古い版だと、知らない引数として前置きだけが黙って落ちる。届く形を確かめられるまで通さない。
|
|
213
|
+
if (args.preface !== undefined)
|
|
214
|
+
throw new core.AitermError("REMOTE_PREFACE_UNSUPPORTED: 別端末へのprefaceには未対応です。文字列は送信していません。", 2);
|
|
212
215
|
const key = deliveryKey(args.session_id, target);
|
|
213
216
|
const delivery = args.force ? null : await deliveryForRequest(extra, true);
|
|
214
217
|
// 終わった依頼の回答は送る前に保存する。実行中の依頼は残る。現地が差し込みを選べばその依頼が回答を受け取る。
|
|
@@ -289,6 +292,8 @@ registerRemoteAwareTool("pty_send", {
|
|
|
289
292
|
NON_BLOCKING_RULE +
|
|
290
293
|
"自動配送以外の結果回収は pty_read(agent_transcript:true)、Claude の durable turn は claude_turn を使う。" +
|
|
291
294
|
"require_agent:true はagent登録が無いsessionへ打鍵せず、AGENT_SESSION_REQUIREDで未送信を返す。" +
|
|
295
|
+
"preface はagent sessionへの送信で本文の前に置く1行。Claude Codeの席では貼り付けの印なしで入れ、本文だけを貼る" +
|
|
296
|
+
"(前置きが貼り付けの包みの外へ出る)。他のharnessでは前置き・空行・本文を1つで貼る。" +
|
|
292
297
|
"force:true は非Claude agent sessionへの手動介入用の素送信。aiterm相関付きClaudeの承認UIはclaude_approvalを使う。",
|
|
293
298
|
inputSchema: {
|
|
294
299
|
session_id: z.string(),
|
|
@@ -308,6 +313,13 @@ registerRemoteAwareTool("pty_send", {
|
|
|
308
313
|
.default(false)
|
|
309
314
|
.describe("非Claude agent sessionでは自動dispatchせず素送信する。aiterm相関付きClaudeのactive turnには使えない"),
|
|
310
315
|
require_agent: z.boolean().default(false).describe("agent登録が無いsessionは打鍵前に拒否する。force:trueとは併用できない"),
|
|
316
|
+
preface: z
|
|
317
|
+
.string()
|
|
318
|
+
.optional()
|
|
319
|
+
.describe("agent session への送信(forceなし)で、本文の前に置く1行。送る文は「preface・空行・text」の並びになる。" +
|
|
320
|
+
"Claude Codeの席ではprefaceを貼り付けの印なしで入れ、textだけを貼る。Codex/Grok/Cursorの席では1つで貼る。" +
|
|
321
|
+
"1行・200字以内・制御文字なしで、行頭が記号や空白でなく、@ を含まない文。外れた時と通常PTY送信・force送信・remote付きでは、" +
|
|
322
|
+
"打鍵前に拒否する"),
|
|
311
323
|
rtk: z.boolean().default(false).describe("既知コマンドを rtk 形へ委譲して送る(rtk 不在なら素通し)"),
|
|
312
324
|
raw: z.boolean().default(false).describe("送信前サニタイズを無効化"),
|
|
313
325
|
image: z
|
|
@@ -332,7 +344,7 @@ registerRemoteAwareTool("pty_send", {
|
|
|
332
344
|
// dispatch前に行った pane 入力の回復("fg" / "fg_stopped" / "stty_raw")。通常送信では省略(additive)。
|
|
333
345
|
pane_input_recovery: z.array(z.string()).optional(),
|
|
334
346
|
},
|
|
335
|
-
}, async ({ session_id, text, enter, mark, force, require_agent, rtk, raw, image }, extra) => {
|
|
347
|
+
}, async ({ session_id, text, enter, mark, force, require_agent, rtk, raw, image, preface }, extra) => {
|
|
336
348
|
let delivery = null;
|
|
337
349
|
try {
|
|
338
350
|
if (require_agent && force) {
|
|
@@ -342,6 +354,12 @@ registerRemoteAwareTool("pty_send", {
|
|
|
342
354
|
if (require_agent && !agentSession) {
|
|
343
355
|
throw new Error(`AGENT_SESSION_REQUIRED: session '${session_id}' のagent登録がありません。文字列は送信していません。`);
|
|
344
356
|
}
|
|
357
|
+
if (preface !== undefined) {
|
|
358
|
+
if (!agentSession) {
|
|
359
|
+
throw new Error("preface は agent session への送信(forceなし)だけで使えます。文字列は送信していません。");
|
|
360
|
+
}
|
|
361
|
+
core.assertAgentPreface(preface);
|
|
362
|
+
}
|
|
345
363
|
if (agentSession) {
|
|
346
364
|
if (enter === false)
|
|
347
365
|
throw new Error("agent session への dispatch は enter:false と併用できません(手動介入は force:true)");
|
|
@@ -350,7 +368,7 @@ registerRemoteAwareTool("pty_send", {
|
|
|
350
368
|
if (rtk)
|
|
351
369
|
throw new Error("agent session への dispatch は rtk:true と併用できません");
|
|
352
370
|
delivery = await deliveryForRequest(extra);
|
|
353
|
-
const receipt = await core.sendAgentMessage(session_id, core.attachImages(text, image), { raw, before_send: delivery?.before_send });
|
|
371
|
+
const receipt = await core.sendAgentMessage(session_id, core.attachImages(text, image), { raw, before_send: delivery?.before_send, preface });
|
|
354
372
|
if (receipt.schema === "aiterm.agent-steer.v1") {
|
|
355
373
|
return {
|
|
356
374
|
content: [
|
package/docs/DESIGN.md
CHANGED
|
@@ -57,6 +57,19 @@ agent配送だけを期待する連携元は`pty_send(require_agent:true)`を指
|
|
|
57
57
|
`AGENT_SESSION_REQUIRED`と「文字列は送信していません」を返す。未送信を確定してから対象sessionを復旧できる。
|
|
58
58
|
省略時の通常PTY送信は維持し、`force:true`との併用は打鍵前に拒否する。
|
|
59
59
|
|
|
60
|
+
agentの席への`pty_send`は、任意の`preface`(本文の前に置く1行)を受ける(ADR 0099)。Claude Codeの席へは、`preface`を貼り付けの印なしで入れ、
|
|
61
|
+
空行と本文を1回の貼り付けで入れる。貼り付けた長い文をClaude Codeが`<pasted_content>`で包んでも、前置きは包みの外に出る。
|
|
62
|
+
ほかのharnessの席へは1つにつないで貼る。`preface`は入力欄がそのまま読むので、1行・200字以内・制御文字なし・行頭が記号や空白でない・
|
|
63
|
+
`@`を含まない文だけを通し、外れた時は打鍵前に断る。
|
|
64
|
+
|
|
65
|
+
Codexは回答を流している間、画面に動作中の行を出さない。`pty_send`の振り分けは、Codexの席に限り、画面が入力待ちでも、この起動の記録の
|
|
66
|
+
最後の境界がturnの開始で、processが生きていて、0.4秒おいて見直しても記録が同じturnのままで画面が動いている時を、動いていると数える
|
|
67
|
+
(ADR 0101)。止まっている席を動いていると読まないよう、1つでも欠けたら新しいturnの道へ回す。
|
|
68
|
+
|
|
69
|
+
Windowsの`pty_close`は、Claude Codeの席を止める前に、C-cを間を空けずに2回送って、Claude Codeが自分で終わるのを待つ(ADR 0100)。
|
|
70
|
+
psmuxは切断の合図なしでsessionを止めるので、そのままでは会話終了のhook(SessionEnd)が走らない。動いている番の席へは、先にC-cを
|
|
71
|
+
1回送って入力待ちに戻るのを待つ。待ちは同期で、上限つき。終わらなくても、その後にsessionを止める。POSIXとほかのharnessの席は変えない。
|
|
72
|
+
|
|
60
73
|
`pty_observe`は存在、pane/harnessの生存、画面状態と理由、native process identityを分ける。
|
|
61
74
|
PIDは開始識別子・argv digestと組にし、paneとharnessを同一視しない。特定できないidentityはnull。
|
|
62
75
|
同じlaunchに属するnpm shimとnative本体は、中間の非候補processも含めた祖先関係から一つの起動として扱う。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.57.1",
|
|
4
4
|
"mcpName": "io.github.kitepon/aiterm-mcp",
|
|
5
5
|
"description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
|
|
6
6
|
"keywords": [
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
75
|
-
"aiterm-steer-delivery": "^0.3.
|
|
75
|
+
"aiterm-steer-delivery": "^0.3.1",
|
|
76
76
|
"ws": "8.21.3",
|
|
77
77
|
"zod": "^4.4.3"
|
|
78
78
|
},
|