aiterm-mcp 0.61.8 → 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 CHANGED
@@ -7,6 +7,29 @@ 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
+
22
+ ## [0.61.9] - 2026-10-23
23
+
24
+ ### Fixed
25
+
26
+ - Asynchronous agent launches wait for graph admission before their initial checks and their synchronous create/metadata/CLI input segment. CLI validation and readiness waits stay outside that mutex. A short competing writer cannot strand a managed shell between generation binding and CLI input; native effects are never retried.
27
+ - Explicit native `pty_close` can recover a returned unknown launch only when the original native/server and managed generation still match, the raw initial prompt is absent, send remains unissued and the harness is confirmed exited. The close keeps the failed record and releases its slot only after native close and confirmed absence. Issued RPCs, live harnesses and uncertain input or identity remain blocked.
28
+
29
+ ### Added
30
+
31
+ - The BellTeam native execution entry accepts explicit `agent_execution/close_launch` with the original request and exact target generation. It checks that expectation during admission and before close, uses the serving SDK management identity, and returns a versioned `_meta["aiterm/closed_launch"]` receipt only after close, confirmed absence and metadata commit. The original unknown record stays unchanged. Failed or uncertain closes return no closed receipt; later generations and historical receipts cannot authorize another close. The proof supports empty exited Claude Code and Codex launches.
32
+
10
33
  ## [0.61.8] - 2026-10-23
11
34
 
12
35
  ### Fixed
@@ -2759,7 +2782,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2759
2782
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2760
2783
  provenance.
2761
2784
 
2762
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.8...HEAD
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
2787
+ [0.61.9]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.8...v0.61.9
2763
2788
  [0.61.8]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.7...v0.61.8
2764
2789
  [0.61.7]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.6...v0.61.7
2765
2790
  [0.61.6]: https://github.com/kitepon/aiterm-mcp/compare/v0.61.5...v0.61.6
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.8** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
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`を返す。
@@ -740,6 +741,12 @@ POSIXの明示native入口でも、`agent_auth`の名前なし開始、認証済
740
741
 
741
742
  この明示入口では `agent_execution` の `reconcile_launch` に元の `request` と正確な `target: {session_id, launch_id}` を渡し、結果不明の空起動を既存世代へ結合できる。保存したnative/serverと現在のmanaged世代、raw状態で初期promptなし、生存して入力待ちのharnessをmetadata受付内で照合し、該当するunknown launch枠だけを解放する。受付済みでも本文未発行なら、照合を繰り返すたびに現在世代・入力待ち・競合操作なしを再確認する。保存した受付結果だけで後の世代を確認済みとしない。起動・打鍵・close・本文再送は行わない。issuedのRPC、入力状態不明、世代交代や世代未確認は拒否する。後続本文は元参照で通常のsend受付を通して一度だけ発行する。照合成功は失われた元RPC receiptの回収を意味しない。
742
743
 
744
+ CLIが終了した結果不明の空起動には、BellTeam native入口の `agent_execution({action: "close_launch", request, target: {session_id, launch_id}})` を明示的に使う。元requestと期待世代を受付内とnative close直前で照合し、原native/server・managed世代一致、raw初期prompt `none`・初期配送なし・send未発行・現在CLI終了の正規probeがすべて必要になる。Claude CodeとCodexで同じ証明条件を使う。生存・issued・送信済み・欠測・世代交代は拒否し、sessionだけのcloseへの代用や自動再起動・再送はしない。旧版は新actionを拒否する。
745
+
746
+ 成功時の `structuredContent` は応答するSDKの別管理requestのExecutionRecordで、元unknown recordは変えない。`_meta["aiterm/closed_launch"]` に厳密なreceiptを返す。固定項目は `schema: "aiterm.closed-launch.v1"`、`version: 1`、`action: "close_launch"`、`outcome: "closed"`、元 `request`、正確な `target`、`management_ref`(応答SDKのMCP request、`tool: "pty_close"`)。`evidence` は原 `native` binding、`initial_prompt: "none"`、`initial_prompt_delivery: null`、`harness_alive: false`、`absence_confirmed: true`、`metadata_committed: true` を持つ。実close・不在確認・slot解放commitが揃った場合だけ返し、失敗や結果不明には返さない。本体が不明を解除する際は元pending操作と自身のstop/versionも照合する。過去receiptで後発の不明を解除しない。
747
+
748
+ 同じ明示入口で、返却済みunknown launchのCLIが終了している場合は、元native/server・managed世代とraw初期promptなし・本文未発行・harness終了を同期検証できた時だけ通常の`pty_close`で対象席を閉じられる。成功と不在を確認して該当枠だけを外し、元unknown requestを保持する。発行中RPC、生存harness、入力や世代の欠測は拒否する。旧requestを再実行せず、必要な後続は新しい通常要求として行う。
749
+
743
750
 
744
751
  ## 人が覗く
745
752
 
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.8** · 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).
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`
@@ -798,6 +799,12 @@ In the opt-in POSIX native execution entry, an ordinary message arriving at a ve
798
799
 
799
800
  An explicit `agent_execution` action `reconcile_launch` can bind an existing empty launch after a lost launch result in this opt-in entry. Supply the original `request` and exact `target: {session_id, launch_id}`. The server checks the original native/server identity, current managed generation, raw no-prompt state and a live ready harness under metadata admission, then releases only that unknown launch slot. Repeating an accepted empty launch before its send also rechecks the current generation, readiness and absence of competing session operations; a saved acceptance alone cannot confirm a later generation. It never launches, types, closes or resends. `issued` RPCs, unknown input and changed or unverified generations remain blocked. Any subsequent body uses the original reference through normal send admission once; a successful reconciliation does not prove the lost RPC receipt.
800
801
 
802
+ For an empty unknown launch whose harness has exited, the BellTeam native entry accepts `agent_execution({action: "close_launch", request, target: {session_id, launch_id}})`. The original request and generation are atomic expectations, checked within admission and immediately before the native close. Raw initial prompt must be `none`, initial delivery absent, send unissued, and the original native/server and managed identity must match. The private runtime proof covers Claude Code and Codex; live, issued, sent, missing or replaced targets remain refused. No session-only close fallback or automatic relaunch/send occurs. Older servers reject this action.
803
+
804
+ Success retains the original unknown record and returns the serving SDK's distinct management execution record in `structuredContent`. `_meta["aiterm/closed_launch"]` is a strict receipt with `schema: "aiterm.closed-launch.v1"`, `version: 1`, `action: "close_launch"`, `outcome: "closed"`, the original `request`, exact `target`, and `management_ref` (the serving MCP request, `tool: "pty_close"`). Its `evidence` contains original `native` binding, `initial_prompt: "none"`, `initial_prompt_delivery: null`, `harness_alive: false`, `absence_confirmed: true`, and `metadata_committed: true`. Only confirmed close, absence and committed slot release produce this receipt. Errors or lost/uncertain results do not. Hosts must match the original pending operation and local stop/version state before clearing its uncertainty; historical receipts cannot clear later uncertainty.
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.
807
+
801
808
 
802
809
  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.
803
810
  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.
@@ -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
+ }