aiterm-mcp 0.61.9 → 0.61.10
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 +14 -1
- package/README.ja.md +2 -1
- package/README.md +2 -1
- package/dist/agent-screen-observation.js +287 -0
- package/dist/core.js +119 -438
- package/dist/execution-invocation.js +41 -0
- package/dist/execution-tools.js +3 -36
- package/dist/harnesses/cursor.js +9 -4
- package/docs/DESIGN.md +25 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.61.10] - 2026-10-23
|
|
11
|
+
|
|
12
|
+
### Validation scope
|
|
13
|
+
|
|
14
|
+
- 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.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- 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.
|
|
19
|
+
- `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.
|
|
20
|
+
- `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.
|
|
21
|
+
|
|
10
22
|
## [0.61.9] - 2026-10-23
|
|
11
23
|
|
|
12
24
|
### Fixed
|
|
@@ -2770,7 +2782,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2770
2782
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2771
2783
|
provenance.
|
|
2772
2784
|
|
|
2773
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.
|
|
2785
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.10...HEAD
|
|
2786
|
+
[0.61.10]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.9...v0.61.10
|
|
2774
2787
|
[0.61.9]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.8...v0.61.9
|
|
2775
2788
|
[0.61.8]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.7...v0.61.8
|
|
2776
2789
|
[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.10** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -516,6 +516,7 @@ sessionへ登録する名前の配列で、`pty_list({ env_keys: ["JOB_OWNER"] }
|
|
|
516
516
|
登録していない値と未設定値はnull。
|
|
517
517
|
一覧の`aiterm.pty-list-result.v1`は`observed_at`と`sessions`を持ち、各行に`session_id`、`current_command`、
|
|
518
518
|
`attached`、`width`、`height`、`harness`、`environment`を返す。従来のtextも維持する。
|
|
519
|
+
最後のsessionを閉じた直後は、終了中のserverの一覧を最長500msで再観測する場合がある。管理されたnative操作は、正常一覧または同socketの厳密な不在を確認できなければunknownを保つ。closeや送信は再実行しない。
|
|
519
520
|
|
|
520
521
|
`pty_observe({ session_id, cursor? })`は`aiterm.pty-observe-result.v1`で`exists`、`observed_at`、
|
|
521
522
|
`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.10** · 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`
|
|
@@ -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
|
+
}
|