aiterm-mcp 0.61.9 → 0.61.11
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 +5 -1
- package/README.md +5 -1
- package/dist/agent-resolver.js +25 -1
- package/dist/agent-screen-observation.js +287 -0
- package/dist/core.js +154 -456
- package/dist/execution-invocation.js +41 -0
- package/dist/execution-tools.js +3 -36
- package/dist/harnesses/claude.js +13 -7
- package/dist/harnesses/codex.js +21 -7
- package/dist/harnesses/cursor.js +10 -28
- package/dist/harnesses/grok.js +19 -10
- package/dist/psmux-send-worker.js +3 -2
- package/dist/tmux-runtime.js +42 -6
- package/docs/DESIGN.md +45 -1
- package/package.json +1 -1
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.61.11] - 2026-10-24
|
|
11
|
+
|
|
12
|
+
### Validation scope
|
|
13
|
+
|
|
14
|
+
- Windows native full validation passed (908 passed, 709 skipped, 0 failed) on GitHub-hosted Windows 2022, Node 22 and psmux 3.3.8. Linux full validation passed (1574 passed, 46 skipped, 0 failed); macOS acceptance combines its previously passing full suite with the subsequent launch and shell-runtime focused gates. Validation uses isolated CLI fixtures; commercial image and real CLI acceptance remain separate.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Shell dialect selection, current prompt reconciliation and encoding share one runtime entry for ordinary and marked sends. A stale psmux PowerShell report no longer sends PowerShell syntax into a current Git Bash prompt; raw input and TUI pastes remain literal.
|
|
19
|
+
|
|
20
|
+
- Windows agent launches use the existing native PowerShell 7 launch adaptation for all harnesses. One shared argv construction feeds POSIX quoting and Windows native invocation; accepted shebang wrappers run through noninteractive Git Bash in the same pane. Startup commands no longer depend on the interactive MSYS bash console input loop. The original text limit is checked before the existing ASCII Base64 adaptation; TUI bodies remain literal.
|
|
21
|
+
- Native Windows panes retain runtime additions to the caller PATH instead of replacing them with registry PATH. Explicit PATH entries keep precedence.
|
|
22
|
+
- Non-raw PowerShell shell submissions establish UTF-8 input, output and native pipeline encoding in the submitted script, including single-line commands. Profiles, psmux interactive initialization, deferred Enter and raw PTY input retain their existing behavior.
|
|
23
|
+
- Agent composer pastes retain their sanitized literal text even when the native foreground query still reports a startup shell. Shell encoding is limited to shell submissions.
|
|
24
|
+
- Agent launch commands for Aiterm-owned bash panes are submitted as one physical line through the existing POSIX multiline adaptation, instead of one interactive line per embedded newline joined by continuation prompts. Argument bytes, lineage, metadata, native admission and receipts are unchanged, and the agent composer body remains literal.
|
|
25
|
+
|
|
26
|
+
### Tests
|
|
27
|
+
|
|
28
|
+
- Real Windows Bash panes retain literal multiline file bytes and correct completion for ordinary and marked sends even when the backend foreground report is deliberately replaced with `pwsh`. Native argv fixtures also verify the received read-only sandbox pair without relying on the startup command echo.
|
|
29
|
+
|
|
30
|
+
- POSIX-only native test teardown also stays skipped on Windows. A file-level hook previously reached the psmux `tmux.exe` alias and stopped other fixture namespaces even when every test case was skipped. A native canary regression covers this boundary.
|
|
31
|
+
- Argument fixtures wait for their final output marker within the existing deadline. Nested lineage assertions inspect child output instead of shell echo.
|
|
32
|
+
- The portable-context fake TUI counts a bracketed multiline paste and its submit Enter as one input.
|
|
33
|
+
- A bash round-trip check and a native launch regression compare the argv bytes a fake CLI received, through the CLI's own file, for quotes, backslashes, history characters, percent signs, Japanese text and newlines.
|
|
34
|
+
|
|
35
|
+
## [0.61.10] - 2026-10-23
|
|
36
|
+
|
|
37
|
+
### Validation scope
|
|
38
|
+
|
|
39
|
+
- This release is accepted for the owner-requested Linux image integration with passing Linux/macOS product suites and the SDK/MCP consumer contract. Hosted Windows validation remains unresolved: the full run reported 887 passed, 709 skipped and 8 failed; focused comparisons against the preceding public source also failed. Do not treat this release as a completed three-OS acceptance. No Windows support contract or native effect retry policy is changed.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Managed native session listings re-observe only an exact `server exited unexpectedly` failure for up to 500 ms. They still require scoped absence evidence or a valid native listing, retain unknown on ambiguous results, and preserve the existing issued slot and generation checks without repeating native effects.
|
|
44
|
+
- `agent_configure` reads a Claude Code seat's busy state with the same harness judgment as `pty_observe` and the ready gate. A stale "esc to interrupt" left in the conversation area no longer keeps a stopped seat waiting as if a turn were running.
|
|
45
|
+
- `pty_list` and `pty_observe` re-read the tmux session listing for up to 500 ms when the server reports `server exited unexpectedly`, which macOS returns right after the last session closes and before the server finishes exiting. If the listing still cannot be read, the error is returned as before.
|
|
46
|
+
|
|
10
47
|
## [0.61.9] - 2026-10-23
|
|
11
48
|
|
|
12
49
|
### Fixed
|
|
@@ -2770,7 +2807,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2770
2807
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2771
2808
|
provenance.
|
|
2772
2809
|
|
|
2773
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.
|
|
2810
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.11...HEAD
|
|
2811
|
+
[0.61.11]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.10...v0.61.11
|
|
2812
|
+
[0.61.10]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.9...v0.61.10
|
|
2774
2813
|
[0.61.9]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.8...v0.61.9
|
|
2775
2814
|
[0.61.8]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.7...v0.61.8
|
|
2776
2815
|
[0.61.7]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.6...v0.61.7
|
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.61.
|
|
212
|
+
**状態:** 開発継続中 · 現行公開版 **v0.61.11** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -434,6 +434,9 @@ MCP クライアントが aiterm を stdio 越しにプログラムから駆動
|
|
|
434
434
|
|
|
435
435
|
## 仕組み
|
|
436
436
|
|
|
437
|
+
Windows nativeのPowerShell shellへの非raw送信は、Console入出力とnative pipelineをUTF-8へ揃えます。実行時に追加した呼出元PATHは新しいpaneへ渡します。raw PTY入力は従来どおりです。
|
|
438
|
+
agentの起動はPowerShell 7のpaneでargvを渡して実行し、受入済みshebang wrapperだけは同じpane内でGit Bashを非対話実行します。Aitermが作る起動scriptはASCIIの1行にし、agent TUIへ渡す本文は文字どおりに保ちます。
|
|
439
|
+
|
|
437
440
|
```mermaid
|
|
438
441
|
flowchart LR
|
|
439
442
|
AI["AI / MCP client<br/>(the orchestrator)"] -->|"pty_send · pty_observe · agent_launch · agent_models · agent_configure · agent_auth · agent_approval · claude_turn · claude_approval<br/>旧launcher alias · diagnostics"| S["aiterm-mcp<br/>stdio MCP · 18 tools"]
|
|
@@ -516,6 +519,7 @@ sessionへ登録する名前の配列で、`pty_list({ env_keys: ["JOB_OWNER"] }
|
|
|
516
519
|
登録していない値と未設定値はnull。
|
|
517
520
|
一覧の`aiterm.pty-list-result.v1`は`observed_at`と`sessions`を持ち、各行に`session_id`、`current_command`、
|
|
518
521
|
`attached`、`width`、`height`、`harness`、`environment`を返す。従来のtextも維持する。
|
|
522
|
+
最後のsessionを閉じた直後は、終了中のserverの一覧を最長500msで再観測する場合がある。管理されたnative操作は、正常一覧または同socketの厳密な不在を確認できなければunknownを保つ。closeや送信は再実行しない。
|
|
519
523
|
|
|
520
524
|
`pty_observe({ session_id, cursor? })`は`aiterm.pty-observe-result.v1`で`exists`、`observed_at`、
|
|
521
525
|
`state`(busy/idle/blocked/dead/missing/unknown)、`reason`、`pane_alive`、`harness_alive`を返す。
|
package/README.md
CHANGED
|
@@ -227,7 +227,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
227
227
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
228
228
|
Release re-registers the Official MCP Registry entry.
|
|
229
229
|
|
|
230
|
-
**Status:** actively maintained · current public release **v0.61.
|
|
230
|
+
**Status:** actively maintained · current public release **v0.61.11** · 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).
|
|
231
231
|
|
|
232
232
|
### Update and rollback
|
|
233
233
|
|
|
@@ -551,6 +551,7 @@ On top of that sits a productized layer a raw tmux bridge doesn't have: **token-
|
|
|
551
551
|
`pty_list({ env_keys: ["JOB_OWNER"] })` returns only registered non-secret values in `environment`; unregistered and missing values are null.
|
|
552
552
|
Its `aiterm.pty-list-result.v1` receipt contains `observed_at` and `sessions`, whose entries include `session_id`,
|
|
553
553
|
`current_command`, `attached`, `width`, `height`, `harness`, and `environment`. Existing text remains available.
|
|
554
|
+
After the last session closes, session listing may briefly re-observe an exiting server for up to 500 ms. Managed native operations retain unknown unless the listing or exact scoped absence can be confirmed; this never repeats a close or send.
|
|
554
555
|
|
|
555
556
|
`pty_observe({ session_id, cursor? })` returns `aiterm.pty-observe-result.v1` with `exists`, `observed_at`, `state`
|
|
556
557
|
(busy/idle/blocked/dead/missing/unknown), `reason`, `pane_alive`, and `harness_alive`. `pane_process` and `harness_process`
|
|
@@ -805,6 +806,9 @@ Success retains the original unknown record and returns the serving SDK's distin
|
|
|
805
806
|
For a returned unknown launch whose harness has exited, the same native entry admits explicit `pty_close` only after synchronous proof of the original native/server and managed generation, raw no-initial-prompt state, an unissued body send and an exited harness. It releases the failed launch slot after successful close and confirmed absence, preserving the original unknown record. Issued RPCs, live harnesses and missing input or generation evidence remain blocked. The old request is never replayed; a later ordinary request may create a fresh generation.
|
|
806
807
|
|
|
807
808
|
|
|
809
|
+
On native Windows, non-raw commands sent to a PowerShell shell use UTF-8 for Console input/output and native pipelines. Runtime additions to the caller PATH are passed to new panes; raw PTY input is unchanged.
|
|
810
|
+
Agent launches use a PowerShell 7 pane and pass the harness argv directly; accepted shebang wrappers use noninteractive Git Bash in that same pane. The owned startup script is sent as one ASCII line, while agent TUI bodies retain their literal text.
|
|
811
|
+
|
|
808
812
|
Sessions live on a shared tmux socket on POSIX or a shared psmux namespace on native Windows. The attach line printed by `pty_open` and `agent_launch` lets a human attach to the same terminal and intervene, including a Claude/Codex/Grok/Cursor harness session: `tmux -S … attach -t <id>` on POSIX, or `psmux -L <namespace> attach -t <id>` on native Windows.
|
|
809
813
|
On POSIX, the first `agent_launch` can create the tmux server when its socket does not yet exist. Aiterm verifies the exact created session and server before accepting that launch; an uncertain result is reported without retrying the input.
|
|
810
814
|
|
package/dist/agent-resolver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// エージェント CLI(claude / codex / grok / cursor-agent)・Throughline・pane shell(Windows は Git Bash)の
|
|
1
|
+
// エージェント CLI(claude / codex / grok / cursor-agent)・Throughline・pane shell(Windows は PowerShell / Git Bash)の
|
|
2
2
|
// 実行ファイルをどう見つけ、どう起動するかの所有者(OS 分岐の所有者。tmux とは独立)。
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import * as fs from "node:fs";
|
|
@@ -257,3 +257,27 @@ export function runThroughlineHandoffContext(bin, sessionId, supplementFile = nu
|
|
|
257
257
|
encoding: "utf8",
|
|
258
258
|
});
|
|
259
259
|
}
|
|
260
|
+
/** Owned Windows agent launch: PowerShell uses argv, without interactive MSYS input. */
|
|
261
|
+
export function windowsAgentLaunchLine(cwd, env, argv) {
|
|
262
|
+
const psq = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
263
|
+
const parts = [];
|
|
264
|
+
if (cwd)
|
|
265
|
+
parts.push(`Set-Location -LiteralPath ${psq(cwd)}`);
|
|
266
|
+
for (const [name, value] of env) {
|
|
267
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name))
|
|
268
|
+
throw new AitermError("環境変数名が不正です", 2);
|
|
269
|
+
parts.push(`$env:${name}=${psq(value)}`);
|
|
270
|
+
}
|
|
271
|
+
parts.push(`& ${argv.map(psq).join(" ")}`);
|
|
272
|
+
return parts.join("; ");
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Keep accepted shebang wrappers in the same pane, as for control commands.
|
|
276
|
+
* Invoking .sh directly in PowerShell uses its file association and can open a
|
|
277
|
+
* separate Git Bash window whose process outlives the pane (fox, 2026-10-14).
|
|
278
|
+
*/
|
|
279
|
+
export function windowsAgentArgv(argv, scriptShell) {
|
|
280
|
+
if (/\.(?:exe|com|cmd|bat|ps1)$/i.test(argv[0]))
|
|
281
|
+
return argv;
|
|
282
|
+
return [scriptShell(), argv[0].replace(/\\/g, "/"), ...argv.slice(1)];
|
|
283
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { AitermError } from "./errors.js";
|
|
2
|
+
import { grokTuiReady, grokTuiBusy, grokPaneObservation, grokLaunchBlockingDialog, assertGrokSandboxNotRejected, GROK_COMPOSER_MARKER_RE } from "./harnesses/grok.js";
|
|
3
|
+
import { codexTuiReady, codexTuiBusy, codexPaneObservation, codexRateLimitModelSwitchDialog, CODEX_COMPOSER_MARKER_RE } from "./harnesses/codex.js";
|
|
4
|
+
import { claudeTuiReady, claudeTuiBusy, claudePaneObservation, claudeLoginMethodMenu, claudeStartupAction, CLAUDE_COMPOSER_MARKER_RE } from "./harnesses/claude.js";
|
|
5
|
+
import { cursorTuiReady, cursorTuiBusy, cursorPaneObservation, CURSOR_COMPOSER_CONTENT_MARKER_RE } from "./harnesses/cursor.js";
|
|
6
|
+
const AGENT_DONE_SETTLE_MIN_MS = 250;
|
|
7
|
+
const AGENT_DONE_SCREEN_SETTLE_POLL_MS = 100;
|
|
8
|
+
const AGENT_DONE_SCREEN_SETTLE_MAX_POLLS = 5;
|
|
9
|
+
const AGENT_DONE_SCREEN_SETTLE_MIN_SAMPLES = 3;
|
|
10
|
+
export const AGENT_TUI_READY_TIMEOUT_MS = 30_000;
|
|
11
|
+
// 画面が起動コマンドの表示のまま(agentがまだ何も描いていない)で上の待ちが切れた時に、起動から待つ上限。
|
|
12
|
+
// Codex親の既定のtool timeout(60秒)の内側に収める。越えると親には時間切れに見えたまま子へ初手が送られる。
|
|
13
|
+
export const AGENT_TUI_FIRST_DRAW_TIMEOUT_MS = 50_000;
|
|
14
|
+
const LAUNCH_ECHO_TAIL_CHARS = 24;
|
|
15
|
+
const LAUNCH_ECHO_MIN_CHARS = 8;
|
|
16
|
+
export const AGENT_TUI_READY_POLL_MS = 500;
|
|
17
|
+
const AGENT_TUI_READY_STABLE_SAMPLES = 11;
|
|
18
|
+
// 上の11回は、起動の途中に一瞬だけ出る入力欄を採らないための確かめ(ADR 0014)。席のharnessが自分の記録で
|
|
19
|
+
// 「この起動でturnを1つ終えていて、次のturnを始めていない」と示している時は起動の途中ではないので、短い間隔で
|
|
20
|
+
// 続けて確かめるだけで送る(ADR 0097)。記録で示せない時は、今までどおり11回。
|
|
21
|
+
const AGENT_TUI_SETTLED_STABLE_SAMPLES = 2;
|
|
22
|
+
const AGENT_TUI_SETTLED_POLL_MS = 100;
|
|
23
|
+
// submit座礁観測(dispatch後にcomposerへ送信textが残存していないかの有界チェック)
|
|
24
|
+
const AGENT_SUBMIT_RESIDUE_DELAY_MS = 250;
|
|
25
|
+
const AGENT_SUBMIT_RESIDUE_POLL_MS = 300;
|
|
26
|
+
const AGENT_SUBMIT_RESIDUE_MAX_SAMPLES = 5;
|
|
27
|
+
const AGENT_SUBMIT_RESIDUE_TAIL_CHARS = 32;
|
|
28
|
+
const AGENT_SUBMIT_RESIDUE_MIN_TAIL_CHARS = 8;
|
|
29
|
+
const CURSOR_PROMPT_VISIBLE_POLL_MS = 100;
|
|
30
|
+
const CURSOR_PROMPT_VISIBLE_MAX_SAMPLES = 50;
|
|
31
|
+
let agentTuiReadyStableSamplesTestOverride = null;
|
|
32
|
+
/**
|
|
33
|
+
* harness別の画面の読みは、この振り分けだけが持つ。coreの観測・起動準備・送信口は`kind`を渡して呼び、
|
|
34
|
+
* どのharnessの関数を呼ぶかを自分で選ばない。読みの中身(入力欄・動作中・承認待ちの印)は各harness moduleが所有する。
|
|
35
|
+
*/
|
|
36
|
+
export function agentPaneObservation(kind, screen) {
|
|
37
|
+
if (kind === "claude")
|
|
38
|
+
return claudePaneObservation(screen);
|
|
39
|
+
if (kind === "codex")
|
|
40
|
+
return codexPaneObservation(screen);
|
|
41
|
+
if (kind === "cursor")
|
|
42
|
+
return cursorPaneObservation(screen);
|
|
43
|
+
return grokPaneObservation(screen);
|
|
44
|
+
}
|
|
45
|
+
export function isAgentTuiReady(kind, screen) {
|
|
46
|
+
if (kind === "claude")
|
|
47
|
+
return claudeTuiReady(screen);
|
|
48
|
+
if (kind === "codex")
|
|
49
|
+
return codexTuiReady(screen);
|
|
50
|
+
if (kind === "cursor")
|
|
51
|
+
return cursorTuiReady(screen);
|
|
52
|
+
return grokTuiReady(screen);
|
|
53
|
+
}
|
|
54
|
+
// 動作中の印は各harnessの判定を使う(agentPaneObservationがbusyを返す読みと同じ)。以前はClaudeとCursorだけ、ここに
|
|
55
|
+
// 別の正規表現があった。Claudeは会話欄に残った「esc to interrupt」を動作中と読み、止まった席の設定替え(agent_configure)
|
|
56
|
+
// が入力待ちを待ち続けていた。同じ画面を2つの読みで判断しない。
|
|
57
|
+
export function isAgentTuiBusy(kind, screen) {
|
|
58
|
+
if (kind === "claude")
|
|
59
|
+
return claudeTuiBusy(screen);
|
|
60
|
+
if (kind === "codex")
|
|
61
|
+
return codexTuiBusy(screen);
|
|
62
|
+
if (kind === "cursor")
|
|
63
|
+
return cursorTuiBusy(screen);
|
|
64
|
+
return grokTuiBusy(screen);
|
|
65
|
+
}
|
|
66
|
+
// ready gate 用: 入力欄マーカーがあっても busy 表示中は ready と数えない。
|
|
67
|
+
// frontend 推定(inferAgentFrontend)は「agent TUI が前面か」を見るだけなので isAgentTuiReady のまま。
|
|
68
|
+
export function isAgentTuiIdleReady(kind, screen) {
|
|
69
|
+
return agentPaneObservation(kind, screen).state === "idle";
|
|
70
|
+
}
|
|
71
|
+
// 起動側が明示応答すべき既知UI。ここで自動承認せず、ready timeoutを待たずに
|
|
72
|
+
// `initial_prompt=not_sent`を返してsessionを生かしたままcallerへ制御を戻す。
|
|
73
|
+
function isAgentTuiActionRequired(kind, screen) {
|
|
74
|
+
if (kind === "grok")
|
|
75
|
+
return grokLaunchBlockingDialog(screen) !== null;
|
|
76
|
+
if (kind === "codex") {
|
|
77
|
+
return codexPaneObservation(screen).state === "blocked";
|
|
78
|
+
}
|
|
79
|
+
if (kind === "claude") {
|
|
80
|
+
return /new MCP servers? found in this project/iu.test(screen)
|
|
81
|
+
|| claudeLoginMethodMenu(screen)
|
|
82
|
+
|| claudeStartupAction(screen, false)?.kind === "initial_theme_selected"
|
|
83
|
+
|| screen.includes("Is this a project you created or one you trust")
|
|
84
|
+
|| screen.includes("trust this folder")
|
|
85
|
+
|| (screen.includes("Claude Code running in Bypass Permissions mode") && screen.includes("Yes, I accept"));
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 画面の末尾が、Aitermが打った起動コマンドの表示のままかを見る(純粋関数)。
|
|
91
|
+
* 混んだ端末ではagentのTUIが描かれる前にready待ちが切れる。画面の最後の文字列が起動コマンドの一部なら、
|
|
92
|
+
* agentはまだ何も描いていない。空白と行頭の継続prompt(`> `/`>> `)は端末の折返しとshellの表示なので比べない。
|
|
93
|
+
*/
|
|
94
|
+
export function launchEchoIsLastOnScreen(screen, launchLine) {
|
|
95
|
+
const command = launchLine.replace(/\s+/g, "");
|
|
96
|
+
const lines = screen.split("\n");
|
|
97
|
+
let tail = "";
|
|
98
|
+
for (let i = lines.length - 1; i >= 0 && tail.length < LAUNCH_ECHO_TAIL_CHARS; i--) {
|
|
99
|
+
tail = lines[i].replace(/^\s*>{1,2}(?:\s|$)/, "").replace(/\s+/g, "") + tail;
|
|
100
|
+
}
|
|
101
|
+
tail = tail.slice(-LAUNCH_ECHO_TAIL_CHARS);
|
|
102
|
+
return tail.length >= LAUNCH_ECHO_MIN_CHARS && command.includes(tail);
|
|
103
|
+
}
|
|
104
|
+
export async function waitAgentTuiReadyImpl(kind, sample, sleepFn, opts = {}) {
|
|
105
|
+
const timeoutMs = opts.timeoutMs ?? AGENT_TUI_READY_TIMEOUT_MS;
|
|
106
|
+
const pollMs = opts.pollMs ?? AGENT_TUI_READY_POLL_MS;
|
|
107
|
+
const stableSamples = opts.stableSamples ?? agentTuiReadyStableSamplesTestOverride ?? AGENT_TUI_READY_STABLE_SAMPLES;
|
|
108
|
+
const settledSamples = opts.settledSamples ?? AGENT_TUI_SETTLED_STABLE_SAMPLES;
|
|
109
|
+
const settledPollMs = opts.settledPollMs ?? AGENT_TUI_SETTLED_POLL_MS;
|
|
110
|
+
const now = opts.now ?? (() => performance.now());
|
|
111
|
+
const start = now();
|
|
112
|
+
let deadline = start + timeoutMs;
|
|
113
|
+
// 呼ぶ側が既定より短い待ちを指定した時は、その待ちを守る(延ばさない)。
|
|
114
|
+
const firstDrawDeadline = timeoutMs >= AGENT_TUI_READY_TIMEOUT_MS ? start + AGENT_TUI_FIRST_DRAW_TIMEOUT_MS : deadline;
|
|
115
|
+
let samples = 0;
|
|
116
|
+
let readyStreak = 0;
|
|
117
|
+
let settledStreak = 0;
|
|
118
|
+
let lastScreen = "";
|
|
119
|
+
for (;;) {
|
|
120
|
+
lastScreen = sample();
|
|
121
|
+
samples++;
|
|
122
|
+
if (kind === "grok")
|
|
123
|
+
assertGrokSandboxNotRejected(lastScreen);
|
|
124
|
+
if (isAgentTuiIdleReady(kind, lastScreen)) {
|
|
125
|
+
readyStreak++;
|
|
126
|
+
if (readyStreak >= stableSamples)
|
|
127
|
+
return { ready: true, samples, lastScreen };
|
|
128
|
+
if (opts.turnSettled?.()) {
|
|
129
|
+
settledStreak++;
|
|
130
|
+
if (settledStreak >= settledSamples)
|
|
131
|
+
return { ready: true, samples, lastScreen };
|
|
132
|
+
}
|
|
133
|
+
else if (settledStreak > 0) {
|
|
134
|
+
// 記録の上でturnが始まった。短い間隔で数えた分を、11回の確かめへ持ち越さない。
|
|
135
|
+
settledStreak = 0;
|
|
136
|
+
readyStreak = 1;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
readyStreak = 0;
|
|
141
|
+
settledStreak = 0;
|
|
142
|
+
// The one keep-current key can be accepted before the TUI redraws.
|
|
143
|
+
// Wait for that exact handled modal to disappear; never resend its key.
|
|
144
|
+
const recoveringModal = opts.codexRateLimitRecovery && kind === "codex"
|
|
145
|
+
&& codexRateLimitModelSwitchDialog(lastScreen) !== null;
|
|
146
|
+
if (isAgentTuiActionRequired(kind, lastScreen) && !recoveringModal)
|
|
147
|
+
return { ready: false, samples, lastScreen };
|
|
148
|
+
}
|
|
149
|
+
if (now() >= deadline) {
|
|
150
|
+
// TUIが一度も描かれていない間に切れた時だけ、決めた上限まで待ちを延ばす。描かれた後の画面は今までどおり扱う。
|
|
151
|
+
if (deadline >= firstDrawDeadline || opts.launchLine === undefined
|
|
152
|
+
|| !launchEchoIsLastOnScreen(lastScreen, opts.launchLine))
|
|
153
|
+
return { ready: false, samples, lastScreen };
|
|
154
|
+
deadline = firstDrawDeadline;
|
|
155
|
+
}
|
|
156
|
+
await sleepFn(settledStreak > 0 ? settledPollMs : pollMs);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function normalizeResidueText(s) {
|
|
160
|
+
return s.replace(/\s+/g, "");
|
|
161
|
+
}
|
|
162
|
+
function agentSubmitResidueTail(text) {
|
|
163
|
+
// 行単位でなく text 全体の正規化末尾から取る: 最終行が短い prompt(「以上」等の締め行)でも
|
|
164
|
+
// 直前行の内容を含む末尾 32 codepoint で観測できる。composer は末尾(カーソル位置)を表示し、
|
|
165
|
+
// submit 済みの transcript echo は長文では先頭側を表示するため、末尾一致は座礁側に偏る。
|
|
166
|
+
const cps = [...normalizeResidueText(text)];
|
|
167
|
+
if (cps.length < AGENT_SUBMIT_RESIDUE_MIN_TAIL_CHARS)
|
|
168
|
+
return null;
|
|
169
|
+
return cps.slice(-AGENT_SUBMIT_RESIDUE_TAIL_CHARS).join("");
|
|
170
|
+
}
|
|
171
|
+
function agentSubmitResidueOnScreen(kind, screen, tail) {
|
|
172
|
+
// Cursorは送信成立後のactive turnで明示的な停止UIを出す。scrollbackに残った旧「>」を
|
|
173
|
+
// composerと誤認すると、実際には走っているpromptをsubmit座礁として報告してしまう。
|
|
174
|
+
if (kind === "cursor" && isAgentTuiBusy(kind, screen))
|
|
175
|
+
return false;
|
|
176
|
+
const lines = screen.split("\n");
|
|
177
|
+
// 入力欄マーカーは ready 判定と同じ記号を行頭基準で探す。submit 済みの transcript echo は
|
|
178
|
+
// マーカー行より上に出るため、最後のマーカー行以降だけを composer 領域として見る。
|
|
179
|
+
// grok は Windows native 描画(`>`・実測 1.0.4)も ready 判定と同様に受ける。
|
|
180
|
+
const markerRe = kind === "codex"
|
|
181
|
+
? CODEX_COMPOSER_MARKER_RE
|
|
182
|
+
: kind === "claude"
|
|
183
|
+
? CLAUDE_COMPOSER_MARKER_RE
|
|
184
|
+
: kind === "cursor"
|
|
185
|
+
? CURSOR_COMPOSER_CONTENT_MARKER_RE
|
|
186
|
+
: GROK_COMPOSER_MARKER_RE;
|
|
187
|
+
let markerIdx = -1;
|
|
188
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
189
|
+
if (markerRe.test(lines[i])) {
|
|
190
|
+
markerIdx = i;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (markerIdx < 0)
|
|
195
|
+
return null;
|
|
196
|
+
if (kind === "cursor") {
|
|
197
|
+
// Cursorは長いpasteを入力欄の先頭側だけ表示し、矢印の次行へ本文を折り返すことがある。
|
|
198
|
+
// 最新composerから下部statusを除いた領域にplaceholder以外の本文が残っていれば、
|
|
199
|
+
// 送信文の末尾が画面外でも未submitの陽性証拠とする。
|
|
200
|
+
const composerLines = lines.slice(markerIdx);
|
|
201
|
+
composerLines[0] = composerLines[0].replace(/^\s*(?:->|>|→)\s*/u, "");
|
|
202
|
+
while (composerLines.at(-1)?.trim() === "")
|
|
203
|
+
composerLines.pop();
|
|
204
|
+
if (/^\s*\/\S.*\s·\s\S/u.test(composerLines.at(-1) ?? ""))
|
|
205
|
+
composerLines.pop();
|
|
206
|
+
while (composerLines.at(-1)?.trim() === "")
|
|
207
|
+
composerLines.pop();
|
|
208
|
+
if (/^\s*(?:Auto|Ask)(?:\s|·|$)/iu.test(composerLines.at(-1) ?? ""))
|
|
209
|
+
composerLines.pop();
|
|
210
|
+
while (composerLines.at(-1)?.trim() === "")
|
|
211
|
+
composerLines.pop();
|
|
212
|
+
const composerText = composerLines.join("\n").trim();
|
|
213
|
+
if (composerText.length > 0
|
|
214
|
+
&& !/^(?:Add a follow-up|Plan,\s*search,\s*build anything)\b/iu.test(composerText))
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return normalizeResidueText(lines.slice(markerIdx).join("")).includes(tail);
|
|
218
|
+
}
|
|
219
|
+
export async function detectAgentSubmitResidueImpl(kind, text, sample, sleepFn, opts = {}) {
|
|
220
|
+
const tail = agentSubmitResidueTail(text);
|
|
221
|
+
if (!tail)
|
|
222
|
+
return { residue: null, samples: 0 };
|
|
223
|
+
const delayMs = opts.delayMs ?? AGENT_SUBMIT_RESIDUE_DELAY_MS;
|
|
224
|
+
const pollMs = opts.pollMs ?? AGENT_SUBMIT_RESIDUE_POLL_MS;
|
|
225
|
+
const maxSamples = opts.maxSamples ?? AGENT_SUBMIT_RESIDUE_MAX_SAMPLES;
|
|
226
|
+
if (delayMs > 0)
|
|
227
|
+
await sleepFn(delayMs);
|
|
228
|
+
let samples = 0;
|
|
229
|
+
let last = null;
|
|
230
|
+
for (let i = 0; i < maxSamples; i++) {
|
|
231
|
+
last = agentSubmitResidueOnScreen(kind, sample(), tail);
|
|
232
|
+
samples++;
|
|
233
|
+
// 残存が消えた(または判定不能になった)時点で確定。true だけは描画遅延と区別するため
|
|
234
|
+
// 全サンプル持続した場合にのみ報告する。
|
|
235
|
+
if (last !== true)
|
|
236
|
+
return { residue: last, samples };
|
|
237
|
+
if (i < maxSamples - 1)
|
|
238
|
+
await sleepFn(pollMs);
|
|
239
|
+
}
|
|
240
|
+
return { residue: true, samples };
|
|
241
|
+
}
|
|
242
|
+
export async function waitCursorPromptVisibleImpl(text, sample, sleepFn, opts = {}) {
|
|
243
|
+
const tail = agentSubmitResidueTail(text) ?? normalizeResidueText(text);
|
|
244
|
+
const pollMs = opts.pollMs ?? CURSOR_PROMPT_VISIBLE_POLL_MS;
|
|
245
|
+
const maxSamples = opts.maxSamples ?? CURSOR_PROMPT_VISIBLE_MAX_SAMPLES;
|
|
246
|
+
for (let i = 0; i < maxSamples; i++) {
|
|
247
|
+
if (agentSubmitResidueOnScreen("cursor", sample(), tail) === true) {
|
|
248
|
+
return { visible: true, samples: i + 1 };
|
|
249
|
+
}
|
|
250
|
+
if (i < maxSamples - 1)
|
|
251
|
+
await sleepFn(pollMs);
|
|
252
|
+
}
|
|
253
|
+
return { visible: false, samples: maxSamples };
|
|
254
|
+
}
|
|
255
|
+
export async function settleAgentDoneScreenImpl(sample, sleepFn, opts = {}) {
|
|
256
|
+
const minDelayMs = opts.minDelayMs ?? AGENT_DONE_SETTLE_MIN_MS;
|
|
257
|
+
const pollMs = opts.pollMs ?? AGENT_DONE_SCREEN_SETTLE_POLL_MS;
|
|
258
|
+
const maxPolls = opts.maxPolls ?? AGENT_DONE_SCREEN_SETTLE_MAX_POLLS;
|
|
259
|
+
const minSamples = opts.minSamples ?? AGENT_DONE_SCREEN_SETTLE_MIN_SAMPLES;
|
|
260
|
+
if (minDelayMs > 0)
|
|
261
|
+
await sleepFn(minDelayMs);
|
|
262
|
+
let prev = sample();
|
|
263
|
+
let samples = 1;
|
|
264
|
+
let stableStreak = 1;
|
|
265
|
+
for (let i = 0; i < maxPolls; i++) {
|
|
266
|
+
if (pollMs > 0)
|
|
267
|
+
await sleepFn(pollMs);
|
|
268
|
+
const current = sample();
|
|
269
|
+
samples++;
|
|
270
|
+
if (current.screen === prev.screen && current.logSize === prev.logSize) {
|
|
271
|
+
stableStreak++;
|
|
272
|
+
if (samples >= minSamples && stableStreak >= 2)
|
|
273
|
+
return { unstable: false, samples };
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
stableStreak = 1;
|
|
277
|
+
}
|
|
278
|
+
prev = current;
|
|
279
|
+
}
|
|
280
|
+
return { unstable: true, samples };
|
|
281
|
+
}
|
|
282
|
+
export function __testSetAgentTuiReadyStableSamples(value) {
|
|
283
|
+
if (value !== null && (!Number.isInteger(value) || value < 1 || value > 1000)) {
|
|
284
|
+
throw new AitermError("test ready stable samplesが不正です", 2);
|
|
285
|
+
}
|
|
286
|
+
agentTuiReadyStableSamplesTestOverride = value;
|
|
287
|
+
}
|