aiterm-mcp 0.59.1 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,58 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.60.0] - 2026-10-13
11
+
12
+ ### 追加
13
+
14
+ - BellTeamが、差し込んだ文を取った番の終わりと、自分で始めていない番を追える口(ADR 0122)。要求の`_meta["aiterm/caller"]`が`BellTeam`の時だけ足す。
15
+ ツールの説明・引数・`structuredContent`の形と、印の無い呼び手への返りは変えない。答えの本文は運ばず、位置だけを返す。
16
+ - `pty_send`の差し込み(`mode: "agent_steer"`)の返りに、`event_cursor`(打つ直前の完了の位置)と`wait_process`
17
+ (`aiterm-wait --session <席> --cursor <N> --steer <steer_id>`)。結果の`_meta["aiterm/steer"]`に`{ schema: "aiterm.steer-ref.v1", steer_id, interim_from }`。
18
+ - `aiterm-wait --steer <steer_id>`は、その文を取った番の終わりで返る。結果に`steer: { placement, taken_at }`を足す。
19
+ `placement`は`same_turn`(動いていた番が取り込んだ)・`next_turn`(番の後で、その文が新しい番を始めた)・`unknown`(記録の中で見分けられず、打った後の
20
+ 最初の番の終わりで返した)。文が会話の記録に入っていない時は`outcome: "not_taken"`で、終了コードは8。
21
+ - `pty_observe`の要求の`_meta["aiterm/interim_from"]`に、上か下が返した値を渡すと、その位置より後の合間の言葉を返す(`seq`は1から)。
22
+ - `pty_list`の結果の`_meta["aiterm/seat_turns"]`に、席の今の番。`{ schema: "aiterm.seat-turns.v1", seats: [{ session_id, launch_id, harness, turn }] }`で、
23
+ `turn`は`{ state: "running", turn_id, started_at, event_cursor, wait_process, interim_from }`・`{ state: "idle" }`・`null`(分からない)。
24
+ Codexの席だけで、会話の記録から読む(画面は読まない)。動いている番の位置は番の頭で、そこから待てばその番の終わりで返る。
25
+ - 文の見分けは、打つ直前に控えた文の頭と、harnessの会話の記録の突き合わせ。CodexとClaude Codeで見分ける。GrokとCursorの差し込みは必ず今の番へ入るので、
26
+ 打った後の最初の完了を`same_turn`で返す。別端末の席(`remote`)への送信は、まだこの手がかりを返さない。
27
+
28
+ ### 変更
29
+
30
+ - 試験: `test/core-agent.test.mjs`を、話題の切れ目で5つのfileへ分けた(共通の準備は`test/core-agent-support.mjs`)。試験の中身と本数は同じ。
31
+ fileの中の試験は1本ずつ順に走るので、この1 fileがWindowsで687秒かかり、全体の試験(720秒)の長さを決めていた。分けたfileは並んで走る。
32
+ 同じ訳で、`test/runtime-error-store.test.mjs`(Windowsで236秒)も2つに分けた(共通の準備は`test/runtime-error-store-support.mjs`)。
33
+ - 試験: 試験は、動かした利用者・席・runnerの置き場と鍵に触らない(ADR 0121、ADR 0087の追記)。製品の動きは変わらない。
34
+ - 試験の前処理(`test/seat-env.mjs`)が、試験へ渡す環境を要る物だけにし、homeとharness・gh・git・npmの置き場を、試験ごとの空の置き場の中に向ける。
35
+ 試験fileは最初のimportで前処理を読むので、入口(`npm test`)を通らずに流されても同じに効く。
36
+ - 一般の試験は、端末に入っている本物のClaude Code・Codex・Grok・Cursorを起こさない。実物で確かめる試験は、`AITERM_TEST_`で始まる変数で
37
+ 実行ファイルを渡した時だけ動く。
38
+ - CIの入口と`npm test`は、この確かめ(`test/seat-env.test.mjs`)を先に流し、落ちたらほかの試験を起こさない。CIの入口は、
39
+ 端末の本体(tmux・psmux・pwsh)が見えない時と、選んだ試験の名前が空や不正の時に、組む前に落ちる。
40
+ - 混んだ端末で上限に当たった待ち(席が起きるのを待つ所)を延ばした。
41
+ - 試験: Codexのhookの試験の偽の受け口(`test/fixtures/codex-hook-queue.mjs`)が、キューのfileをその場で書いていた。同時に動くhookの数だけ
42
+ 偽物が起きて同じfileを見るので、空にしてから書き終えるまでの間に読んだ偽物が異常終了し、「同時に二つのhookが動いても…」が
43
+ `CODEX_RECEIVER_TRANSPORT_FAILED`で落ちる事があった(macOSのCI)。一時fileからの置き換えにした。製品の動きは変わらない。
44
+
45
+ ### 修正
46
+
47
+ - Claude CodeのStopのhookが、席の印を継いだ別の会話(席が起こした子や、裏の会話)の番の終わりで、席の答えのfileを上書きし、席の番の印を
48
+ 消していた件を直す(ADR 0119)。席の会話と違う会話のStopでは、席の記録に触らない。席の会話IDは、席の記録を先に、無ければ環境の値で見る。
49
+ - **設定を替えた直後に同じ席へ届いた文が、替える前の設定で動く事があった**(ADR 0120)。`agent_configure`は、命令を打つ前の確かめに
50
+ 数秒かかる間、席ごとの送信の行列(ADR 0088)に並んでいなかった。その間に届いた`pty_send`が先に席へ入り、設定替えは番の後になった
51
+ (実例: ファストを入れた3秒後の1通が通常の速度で動いた)。設定替えを同じ行列に並べ、替えている間に届いた文は替え終えてから席へ入れる。
52
+ - `pty_send`の返りは、同じ席の設定替えが終わるまで遅れる。
53
+ - 番が動いている席への設定替えは、行列を持たずに番の終わりを待つ。その間の差し込みは今までどおり通る。
54
+ - 順番が来なかった時(POSIXは60秒、Windowsは180秒)、設定替えは`AGENT_SEND_BUSY`と「設定は変えていません。」で断る。
55
+ 送信の断りの文は変えていない。
56
+ - **Codexの完了待ちが、長く続いた番で「Codex transcript のturn増分が大きすぎます」と誤りで止まっていた件を直す。**
57
+ 待ち始めの位置から16MiBを超えて書いた番で起きた(2026-10-13の夜の番は、開始の後ろが約8.4MBまで行った)。会話の記録を4MiBずつ読み、
58
+ 番の大きさには上限を置かない。改行が来ないまま16MiBを超えた1行だけ、持たずに読み飛ばす。
59
+ - 片付け済み(archived)などで断られたCodexへの配送を、会話を戻した後に同じ配送IDで渡し直せなかった件を直す
60
+ (`aiterm-parent-delivery codex submit`が`PARENT_DELIVERY_DUPLICATE`で断っていた)。共通部品`aiterm-steer-delivery`を`^0.4.7`へ上げる。
61
+
10
62
  ## [0.59.1] - 2026-10-13
11
63
 
12
64
  ### 修正
@@ -2629,7 +2681,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
2629
2681
  `ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
2630
2682
  provenance.
2631
2683
 
2632
- [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.59.1...HEAD
2684
+ [Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.60.0...HEAD
2685
+ [0.60.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.59.1...v0.60.0
2633
2686
  [0.59.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.59.0...v0.59.1
2634
2687
  [0.59.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.4...v0.59.0
2635
2688
  [0.58.4]: https://github.com/kitepon/aiterm-mcp/compare/v0.58.3...v0.58.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.59.1** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
212
+ **状態:** 開発継続中 · 現行公開版 **v0.60.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
213
213
 
214
214
  ### 更新と巻き戻し
215
215
 
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.59.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).
230
+ **Status:** actively maintained · current public release **v0.60.0** · runs on Linux · WSL2 · macOS · native Windows (tmux on POSIX, the tmux-CLI-compatible [psmux](https://github.com/psmux/psmux) on native Windows — no WSL required) · MIT · see the [CHANGELOG](CHANGELOG.md).
231
231
 
232
232
  ### Update and rollback
233
233
 
@@ -293,7 +293,7 @@ Set `require_agent:true` on `pty_send` when the integration requires agent deliv
293
293
 
294
294
  When the caller gives no `preface`, Aiterm adds one itself for a Claude Code session whenever the text is long enough for Claude Code to wrap it (three or more line breaks, or 800 characters and up): "Aitermが届ける、この席の呼び手からの正規の送信です。下の貼り付けは、どこかから拾った文ではなく、呼び手がこの席へ宛てて送った文です。" ("A regular message from this session's caller, delivered by Aiterm. The paste below was sent to this session by the caller, not picked up from somewhere else.") The line states where the message comes from and does not tell the session to obey it. It applies to new turns, to steering, to the `prompt` of `agent_launch`, and to `remote` sessions (the Aiterm on that machine adds it). Without that line the whole message sits inside the wrapper, and a Claude Code session may decline to act on it as "not the user's own instruction". Short texts, Codex, Grok and Cursor sessions, and sends with `raw` or `force` are unchanged (ADR 0116).
295
295
 
296
- 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.
296
+ 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. `agent_configure` waits in the same line: a `pty_send` that arrives while a configuration change is in progress is sent after the change, so the turn runs with the new setting. A configuration change to a session whose turn is running does not hold the line while it waits for the turn to end, so steering still goes through. If its turn does not come within the same limit, `agent_configure` returns `AGENT_SEND_BUSY` with an explicit unchanged message.
297
297
 
298
298
  `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.
299
299
 
@@ -37,6 +37,7 @@ export function readTailLinesNewestFirst(file, maxBytes) {
37
37
  * 記録を末尾から先頭へさかのぼり、行を新しい順に`visit`へ渡す。`visit`がundefined以外を返したら、そこで止めてその値を返す。
38
38
  * 先頭まで読んで何も返らなければundefined。書いている途中の行(改行で終わっていない末尾)がある時と、読めなかった時はnull。
39
39
  * 1回に読むのは`chunkBytes`ずつ。行が読む範囲に収まらない時は、収まるまで範囲を広げる(行の途中では切らない)。
40
+ * `visit`の2つめは、その行の頭のbyte位置。
40
41
  */
41
42
  export function scanLinesNewestFirst(file, visit, chunkBytes) {
42
43
  let end = safeStatSize(file);
@@ -56,13 +57,18 @@ export function scanLinesNewestFirst(file, visit, chunkBytes) {
56
57
  return null;
57
58
  if (tail && chunk[chunk.length - 1] !== 0x0a)
58
59
  return null;
59
- const lines = chunk.subarray(cut).toString("utf8").split("\n");
60
- for (let index = lines.length - 1; index >= 0; index -= 1) {
61
- if (lines[index].trim() === "")
62
- continue;
63
- const found = visit(lines[index]);
64
- if (found !== undefined)
65
- return found;
60
+ const body = chunk.subarray(cut);
61
+ for (let stop = body.length; stop > 0;) {
62
+ if (body[stop - 1] === 0x0a)
63
+ stop -= 1;
64
+ const begin = stop === 0 ? 0 : body.lastIndexOf(0x0a, stop - 1) + 1;
65
+ const line = body.subarray(begin, stop).toString("utf8");
66
+ if (line.trim() !== "") {
67
+ const found = visit(line, start + cut + begin);
68
+ if (found !== undefined)
69
+ return found;
70
+ }
71
+ stop = begin;
66
72
  }
67
73
  end = start + cut;
68
74
  }
@@ -2,7 +2,8 @@
2
2
  // aiterm-wait — agent turn 完了eventの純リーダー観測CLI。
3
3
  // 完了/timeout/close を1行のJSON receiptで返してexitする。lock・PTY・dispatch状態には一切触れない。
4
4
  // 親AIホストのバックグラウンドタスクとして起動し、exitを「観測終了の通知」として使う。
5
- // exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 6=rate_limited=harness利用上限 / 7=error=turnがエラー終了 / 1=waiter自身のエラー)。
5
+ // exit≠完了: exit code は outcome を映す(0=done / 5=running=未完了 / 3=timeout=未完了 / 4=closed / 6=rate_limited=harness利用上限 / 7=error=turnがエラー終了 / 8=not_taken=差し込んだ文が会話に入っていない / 1=waiter自身のエラー)。
6
+ // --steer は、差し込みの返り(BellTeamだけに返す。ADR 0122)のwait_processが付ける。その差し込みを取った番の終わりを待つ。
6
7
  // --timeout 0 は待たずに一度だけ観測する照会で、未完了は running(timeout と混同させない)。
7
8
  // receipt の outcome が正で、done 以外は未完了。timeout の既定は core の DEFAULT_AGENT_DONE_TIMEOUT(600秒)。
8
9
  import { fileURLToPath } from "node:url";
@@ -10,6 +11,7 @@ import * as fs from "node:fs";
10
11
  import { AitermError, detectAgentRateLimit, observeAgentDone } from "./core.js";
11
12
  const SESSION_RE = /^[A-Za-z0-9_-]{1,64}$/;
12
13
  const OPERATION_RE = /^sha256:[0-9a-f]{64}$/;
14
+ const STEER_RE = /^[0-9a-f]{16}$/;
13
15
  const USAGE = "usage: aiterm-wait --session <name> [--cursor <event_cursor>] [--operation sha256:<64hex>] [--timeout <sec>]" +
14
16
  "(--timeout 0 は待たずに一度だけ観測する照会で、未完了は outcome=running / exit 5)";
15
17
  export function parseArgs(argv) {
@@ -17,9 +19,10 @@ export function parseArgs(argv) {
17
19
  let operationId = null;
18
20
  let timeout = null;
19
21
  let cursor = null;
22
+ let steer = null;
20
23
  for (let i = 0; i < argv.length; i++) {
21
24
  const a = argv[i];
22
- if (a === "--session" || a === "--operation" || a === "--timeout" || a === "--cursor") {
25
+ if (a === "--session" || a === "--operation" || a === "--timeout" || a === "--cursor" || a === "--steer") {
23
26
  const v = argv[i + 1];
24
27
  if (v === undefined)
25
28
  throw new Error(`${a} に値がありません。${USAGE}`);
@@ -45,6 +48,13 @@ export function parseArgs(argv) {
45
48
  throw new Error(`--cursor は0以上の整数完了境界だけを受理します。${USAGE}`);
46
49
  cursor = Number(v);
47
50
  }
51
+ else if (a === "--steer") {
52
+ if (steer !== null)
53
+ throw new Error(`--steer が重複しています。${USAGE}`);
54
+ if (!STEER_RE.test(v))
55
+ throw new Error(`--steer が不正です。${USAGE}`);
56
+ steer = v;
57
+ }
48
58
  else {
49
59
  if (timeout !== null)
50
60
  throw new Error(`--timeout が重複しています。${USAGE}`);
@@ -61,7 +71,9 @@ export function parseArgs(argv) {
61
71
  }
62
72
  if (session === null)
63
73
  throw new Error(`--session は必須です。${USAGE}`);
64
- return { session, operationId, timeout: timeout ?? 600, cursor };
74
+ if (steer !== null && operationId !== null)
75
+ throw new Error(`--steer は --operation と併用できません。${USAGE}`);
76
+ return { session, operationId, timeout: timeout ?? 600, cursor, steer };
65
77
  }
66
78
  function emit(value) {
67
79
  process.stdout.write(JSON.stringify(value) + "\n");
@@ -76,6 +88,7 @@ const OUTCOME_EXIT_CODES = {
76
88
  closed: 4,
77
89
  rate_limited: 6,
78
90
  error: 7,
91
+ not_taken: 8,
79
92
  };
80
93
  export async function main(argv) {
81
94
  const cmd = parseArgs(argv);
@@ -85,6 +98,7 @@ export async function main(argv) {
85
98
  operation_id: cmd.operationId,
86
99
  timeout: cmd.timeout,
87
100
  cursor: cmd.cursor,
101
+ steer: cmd.steer,
88
102
  });
89
103
  }
90
104
  catch (e) {
@@ -138,6 +138,22 @@ function consumeOperationMarker(file, marker) {
138
138
  }
139
139
  fs.unlinkSync(file);
140
140
  }
141
+ /**
142
+ * 席の会話ID。席の記録(Aitermが書く `…agent.json`)を先に読み、無い・読めない時だけ、起動時に環境へ入れた値を見る。
143
+ * 記録を先にするのは、席の会話を後から結び直した時に、環境の値は起動時のまま残るため。
144
+ * どちらも無い時は null(今までどおり、印だけで席の物と数える)。
145
+ */
146
+ function seatVendorSessionId(agents, session, launchId) {
147
+ try {
148
+ const meta = JSON.parse(fs.readFileSync(path.join(agents, `${session}.${launchId}.agent.json`), "utf8"));
149
+ if (typeof meta.vendor_session_id === "string" && meta.vendor_session_id)
150
+ return meta.vendor_session_id;
151
+ }
152
+ catch {
153
+ // 席の記録が無い・読めない。
154
+ }
155
+ return process.env.AITERM_AGENT_VENDOR_SESSION_ID || null;
156
+ }
141
157
  async function main() {
142
158
  if (!hasAitermEnv())
143
159
  noop();
@@ -163,12 +179,17 @@ async function main() {
163
179
  fail("Claude session_id がありません");
164
180
  if (typeof payload.last_assistant_message !== "string")
165
181
  fail("last_assistant_message がありません");
182
+ // 席の印(環境変数)は、席のprocessが起こした子や、Claude Codeの裏の常駐を通じて、席と関係の無い会話にも継がれる。
183
+ // 席の会話と違う会話のStopは、席の記録(答え・event・番の印)に触らない(ADR 0119)。
184
+ const agents = agentsDir();
185
+ const seatConversation = seatVendorSessionId(agents, session, launchId);
186
+ if (seatConversation !== null && seatConversation !== payload.session_id)
187
+ noop();
166
188
  const text = payload.last_assistant_message;
167
189
  const resultBytes = Buffer.byteLength(text, "utf8");
168
190
  if (resultBytes > MAX_RESULT_BYTES)
169
191
  fail(`assistant result が${MAX_RESULT_BYTES} bytesを超えています`);
170
192
  const resultDigest = createHash("sha256").update(text, "utf8").digest("hex");
171
- const agents = agentsDir();
172
193
  const resultFile = path.join(agents, `${session}.${launchId}.claude-result.json`);
173
194
  const eventFile = path.join(agents, `${session}.${launchId}.events.jsonl`);
174
195
  const operationFile = path.join(agents, `${session}.${launchId}.claude-operation.json`);
package/dist/core.js CHANGED
@@ -29,7 +29,8 @@ import { grokAuthPlan, grokAuthStatus, grokAuthPane, GROK_MODEL_DEFAULTS, realGr
29
29
  import { codexAuthPlan, codexAuthStatus, codexAuthPane, realCodexHome, readCodexConfigPins, bindCodexTranscriptSession, latestCodexCompletion, codexTurnSettled, codexRunningTurn, observeCodexDone, buildCodexAgentCmd, codexLaunchNote, codexTuiReady, codexPaneObservation, codexTuiBusy, codexHelperProcess, codexRateLimitModelSwitchDialog, codexApprovalDialog, codexStartupAction, CODEX_COMPOSER_MARKER_RE, codexModelChoice, codexEffortChoice, codexMoreReasoningChoice, codexTranscriptText, createCodexAgentMetadata, codexModelChoices, codexFastCommandOffered, codexFooterShowsFast, codexServiceTierNotices, restoreCodexDefaults, restoreCodexDefaultInFile, } from "./harnesses/codex.js";
30
30
  import { claudeAuthPlan, claudeAuthStatus, claudeAuthPane, OPERATION_ID_RE, CLAUDE_RESULT_MAX_BYTES, CLAUDE_EFFORTS, agentManagedClaudeSettingsPath, agentClaudeResultPath, agentClaudeOperationPath, agentClaudeApprovalReceiptPath, agentClaudeDispatchReceiptPath, validateOperationId, readClaudeResultText, assertClaudeAuthenticationReady, claudeModelChoices, claudeConfigDir, claudeFastIndicatorShown, claudeFastResult, buildClaudeAgentCmd, claudeLaunchNote, claudeTuiReady, claudePaneObservation, claudeUsageLimit, claudeLiveSelectMenu, claudeModelPicker, claudeModelPickerRow, claudeModelSwitchConfirm, claudeModelSwitchResult, claudeStartupAction, claudeLoginMethodMenu, CLAUDE_COMPOSER_MARKER_RE, createClaudeAgentMetadata, claudeSessionTranscriptPath, claudeApiErrorFromLine, claudeApiErrorAfter, } from "./harnesses/claude.js";
31
31
  import { cursorAuthPlan, cursorAuthStatus, cursorAuthPane, bindCursorTranscriptSession, cursorTurnBoundary, latestCursorCompletion, observeCursorDone, cursorTranscriptText, assertCursorAuthenticationReady, assertCursorModelAvailable, cursorModelChoices, cursorModelIsFastVariant, cursorFastParameter, cursorHeaderShowsFast, cursorPickerView, cursorEffortSelected, cursorParameterRows, cursorUserConfigPath, CURSOR_DEFAULT_MODEL_KEYS, realCursorHome, buildCursorAgentCmd, cursorAgentArgv, cursorPwshLaunchLine, cursorPromptWithLineage, createCursorAgentMetadata, cursorLaunchNote, cursorEffortNavigation, cursorTuiReady, cursorPaneObservation, cursorPromptHooksRunning, cursorUsageLimit, CURSOR_SUBMIT_SEQUENCE, CURSOR_COMPOSER_CONTENT_MARKER_RE, validateCursorModelEffort, } from "./harnesses/cursor.js";
32
- import { readInterimWords, recordInterimBoundary } from "./interim-words.js";
32
+ import { interimFromNow, readInterimWords, recordInterimBoundary } from "./interim-words.js";
33
+ import { codexSeatTurn, observeSteerDone, readSteerEntry, recordSteer } from "./steer-follow.js";
33
34
  import { resolveAgentBin, resolveThroughlineBin, runThroughlineHandoffContext, isWindowsNativeExecutable, agentBinForPaneShell, resolveWinPaneShell } from "./agent-resolver.js";
34
35
  export { AitermError } from "./errors.js";
35
36
  export { tmuxSpawnEnv } from "./tmux-runtime.js";
@@ -330,6 +331,7 @@ function cleanupAgentState(name) {
330
331
  f.endsWith(".claude-approval.json") ||
331
332
  f.endsWith(".claude-dispatch") ||
332
333
  f.endsWith(".interim.json") ||
334
+ f.endsWith(".steers.jsonl") ||
333
335
  f.endsWith(".auth.json"))
334
336
  fs.unlinkSync(p);
335
337
  else if (f.endsWith(".codex-home") || f.endsWith(".grok-home") || f.endsWith(".cursor-plugin") || f.endsWith(".home")) {
@@ -2235,13 +2237,16 @@ function reapDeadAgentSendLock(name, replacedToken = null) {
2235
2237
  }
2236
2238
  return "done";
2237
2239
  }
2240
+ // 順番が来なかった時の断りの末尾。同じ行列に並ぶ設定替え(ADR 0120)は、何も変えていない事を書く。
2241
+ const AGENT_SEND_UNSENT = "文字列は送信していません。少し後で再度 pty_send してください。";
2242
+ const AGENT_CONFIGURE_UNCHANGED = "設定は変えていません。少し後で再度 agent_configure してください。";
2238
2243
  // BellTeamのため: 先頭の「AGENT_SEND_BUSY: agent session '<名前>' は」と、文中の「文字列は送信していません。」で
2239
2244
  // 未送信の断りを見分けている(2026-10-05)。この2か所を変える時は、変える前に知らせる。
2240
- function agentSendBusyError(name, pid, waitMs) {
2245
+ function agentSendBusyError(name, pid, waitMs, untouched = AGENT_SEND_UNSENT) {
2241
2246
  return new AitermError(`AGENT_SEND_BUSY: agent session '${name}' は${pid != null ? `別プロセス(pid ${pid})` : "先に受け付けた送信"}の処理中です。` +
2242
- `${waitMs}ms待っても順番が来ませんでした。文字列は送信していません。少し後で再度 pty_send してください。`, 2);
2247
+ `${waitMs}ms待っても順番が来ませんでした。${untouched}`, 2);
2243
2248
  }
2244
- async function acquireAgentSendFileLock(name, deadline, waitMs) {
2249
+ async function acquireAgentSendFileLock(name, deadline, waitMs, untouched = AGENT_SEND_UNSENT) {
2245
2250
  const p = agentSendLockPath(name);
2246
2251
  let checkedOwner = null;
2247
2252
  let checkedOwnerReplaced = false;
@@ -2279,11 +2284,11 @@ async function acquireAgentSendFileLock(name, deadline, waitMs) {
2279
2284
  `pty_listで対象を確認し、pty_closeでsessionを閉じてから同じIDで再作成してください`, 2);
2280
2285
  }
2281
2286
  if (performance.now() >= deadline)
2282
- throw agentSendBusyError(name, dead ? null : probe.pid, waitMs);
2287
+ throw agentSendBusyError(name, dead ? null : probe.pid, waitMs, untouched);
2283
2288
  await sleep(AGENT_SEND_LOCK_POLL_MS);
2284
2289
  }
2285
2290
  }
2286
- async function withAgentSendLock(name, run) {
2291
+ async function withAgentSendLock(name, run, untouched = AGENT_SEND_UNSENT) {
2287
2292
  const waitMs = agentSendLockWaitMs;
2288
2293
  const deadline = performance.now() + waitMs;
2289
2294
  const previous = agentSendQueues.get(name) ?? Promise.resolve();
@@ -2308,8 +2313,8 @@ async function withAgentSendLock(name, run) {
2308
2313
  ]);
2309
2314
  clearTimeout(timer);
2310
2315
  if (!myTurn)
2311
- throw agentSendBusyError(name, null, waitMs);
2312
- const release = await acquireAgentSendFileLock(name, deadline, waitMs);
2316
+ throw agentSendBusyError(name, null, waitMs, untouched);
2317
+ const release = await acquireAgentSendFileLock(name, deadline, waitMs, untouched);
2313
2318
  agentSendHeld.add(name);
2314
2319
  try {
2315
2320
  return await run();
@@ -2881,13 +2886,37 @@ async function settlePublishedClaudeCompletionMarker(meta, marker) {
2881
2886
  }
2882
2887
  return active;
2883
2888
  }
2884
- /** BellTeamのため: agent sessionの現在(または直前)のturnの合間の言葉。agent sessionでなければnull。 */
2885
- export function readSessionInterimWords(name, after) {
2889
+ /**
2890
+ * BellTeamのため: agent sessionの現在(または直前)のturnの合間の言葉。agent sessionでなければnull。
2891
+ * fromを渡した時は、その位置(差し込みの返りか、席の今の番が渡した位置)より後の言葉(ADR 0122)。
2892
+ */
2893
+ export function readSessionInterimWords(name, after, from = null) {
2886
2894
  assertSessionName(name);
2887
2895
  const dir = existingAgentsDir();
2888
2896
  if (!dir || !fs.readdirSync(dir).some((f) => f.startsWith(`${name}.`) && f.endsWith(".agent.json")))
2889
2897
  return null;
2890
- return readInterimWords(loadAgentMetadata(name), after);
2898
+ return readInterimWords(loadAgentMetadata(name), after, from);
2899
+ }
2900
+ /**
2901
+ * BellTeamのため(ADR 0122): agent sessionの、記録の上の今の番。画面は読まない。
2902
+ * BellTeamは、自分の送信で始めていない番(Aitermの子の結果が始めた番など)を、ここで見つけて追う。
2903
+ * 動いている番のwait_processは、その番の終わりで返る。
2904
+ */
2905
+ export function readSeatTurns(sessionIds) {
2906
+ const seats = [];
2907
+ for (const name of sessionIds) {
2908
+ const meta = tryLoadAgentMetadata(name);
2909
+ if (!meta)
2910
+ continue;
2911
+ let turn = null;
2912
+ try {
2913
+ turn = codexSeatTurn(meta);
2914
+ }
2915
+ catch { /* 読めない席は、分からないまま返す */ }
2916
+ seats.push({ session_id: name, launch_id: meta.launch_id, harness: agentHarness(meta.kind),
2917
+ turn: turn?.state === "running" ? { ...turn, wait_process: agentWaitProcess(name, turn.event_cursor) } : turn });
2918
+ }
2919
+ return { schema: "aiterm.seat-turns.v1", seats };
2891
2920
  }
2892
2921
  /** agent harness の構造化 transcript から直近完了ターンの最終回答と相関情報を読む。 */
2893
2922
  export async function readAgentTranscriptResult(name, o = {}) {
@@ -3053,7 +3082,7 @@ export function windowsStartProcessArgumentList(args) {
3053
3082
  }
3054
3083
  // npmのplatform別bin shimをcallerに解釈させず、現在稼働中のNodeと同梱CLIを直接起動する。
3055
3084
  // Windowsでもbackendはpsmux、対話shellはPowerShell 7のまま。これはwaiter processの入口だけを所有する。
3056
- export function agentWaitProcess(session, cursor, runtime = {}) {
3085
+ export function agentWaitProcess(session, cursor, runtime = {}, steer = null) {
3057
3086
  const executable = runtime.executable ?? liveNodeExecutable();
3058
3087
  const args = [
3059
3088
  runtime.cliPath ?? fileURLToPath(new URL("./aiterm-wait-cli.js", import.meta.url)),
@@ -3061,6 +3090,8 @@ export function agentWaitProcess(session, cursor, runtime = {}) {
3061
3090
  session,
3062
3091
  "--cursor",
3063
3092
  String(cursor),
3093
+ // 差し込みを取った番の終わりを待つ(ADR 0122)。
3094
+ ...(steer ? ["--steer", steer] : []),
3064
3095
  ];
3065
3096
  return {
3066
3097
  executable,
@@ -3139,6 +3170,7 @@ export function detectAgentRateLimit(kind, aitermSession) {
3139
3170
  // 外部waiterプロセス用の純リーダー観測。lock・PTY・metadata書込・dispatch状態には一切触れない。
3140
3171
  // Codex/Cursor/Grokは各harnessのtranscript、Claudeはevent fileを増分走査し、vendor_session_idのbind永続化を
3141
3172
  // 行わない(waiterは観測者であって所有者でない)。
3173
+ // steerを渡した時は、その差し込み(pty_sendの返りのsteer_id)を取った番の終わりを待つ(ADR 0122。steer-follow.ts)。
3142
3174
  export async function observeAgentDone(name, o = {}) {
3143
3175
  const meta = loadAgentMetadata(name);
3144
3176
  const operationId = o.operation_id == null ? null : validateOperationId(o.operation_id);
@@ -3149,6 +3181,23 @@ export async function observeAgentDone(name, o = {}) {
3149
3181
  throw new AitermError("cursor は0以上の整数完了境界で指定してください", 2);
3150
3182
  }
3151
3183
  const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
3184
+ if (o.steer != null) {
3185
+ if (operationId)
3186
+ throw new AitermError("steer は operation_id と併用できません", 2);
3187
+ const plain = (cursor, timeoutSeconds) => observeAgentDone(name, { timeout: timeoutSeconds, cursor, signal: o.signal });
3188
+ const entry = readSteerEntry(meta, o.steer);
3189
+ if (!entry) {
3190
+ // 控えが無い(古くて消えた)。位置が渡されていれば、今までの待ち(その位置より後の最初の完了)で返す。
3191
+ if (o.cursor == null)
3192
+ throw new AitermError("STEER_NOT_FOUND: この席に、その差し込みの控えがありません。", 2);
3193
+ return { ...await plain(o.cursor, timeout), steer: { placement: "unknown", taken_at: null } };
3194
+ }
3195
+ return observeSteerDone(meta, entry, timeout, {
3196
+ signal: o.signal, plain,
3197
+ parseDone: line => parseAgentDoneEvent(line, meta).event,
3198
+ rateLimit: () => detectAgentRateLimit(meta.kind, name),
3199
+ });
3200
+ }
3152
3201
  if (meta.kind === "codex" && meta.completion_route === "codex_transcript") {
3153
3202
  return observeCodexDone(meta, timeout, o.cursor, o.signal);
3154
3203
  }
@@ -4851,11 +4900,28 @@ export async function configureAgent(name, opts) {
4851
4900
  if ((model && /[\r\n]/.test(model)) || (effort && /[\r\n]/.test(effort))) {
4852
4901
  throw new AitermError("model/reasoning_effort に改行を含められません", 2);
4853
4902
  }
4854
- const meta = loadAgentMetadata(name);
4855
- if (fast === false && modelIsFastVariant(meta.kind, model)) {
4903
+ if (fast === false && modelIsFastVariant(loadAgentMetadata(name).kind, model)) {
4856
4904
  throw new FastModeError("FAST_MODE_MODEL_CONFLICT", `model=${model} はfast版のIDですが、fast: false が渡されました。元のmodel IDを渡すか、fastを省いてください。設定は変えていません。`);
4857
4905
  }
4906
+ // 設定替えは席の入力欄へ命令を打つので、送信と同じ行列(ADR 0088)に並ぶ。替えている間に届いた文は、替え終えてから席へ入る(ADR 0120)。
4907
+ // 番が動いている席では、行列を持ったまま番の終わりを待たない(待つ間も、その席への差し込みを通す)。
4908
+ const deadline = performance.now() + AGENT_TUI_READY_TIMEOUT_MS;
4909
+ for (;;) {
4910
+ const result = await withAgentSendLock(name, () => configureAgentInLock(name, model, effort, fast), AGENT_CONFIGURE_UNCHANGED);
4911
+ if (result)
4912
+ return result;
4913
+ if (performance.now() >= deadline)
4914
+ throw new AitermError(`agent session '${name}' は入力待ちではありません`, 2);
4915
+ await sleep(AGENT_TUI_READY_POLL_MS);
4916
+ }
4917
+ }
4918
+ /** 返りの null は、席の画面が番の最中を示していて、何も打っていない時。 */
4919
+ async function configureAgentInLock(name, model, effort, fast) {
4920
+ // 席の控えは、行列の順番が来てから読む(前の送信が書いた分を、古い控えで上書きしない)。
4921
+ const meta = loadAgentMetadata(name);
4858
4922
  bindCompletedInitialPrompt(meta);
4923
+ if (isAgentTuiBusy(meta.kind, captureScreen(name, AGENT_TUI_READY_LINES)))
4924
+ return null;
4859
4925
  const { ready, codexRateLimitModelSwitch } = await waitAgentTuiReadyAfterCodexRateLimitRecovery(name, meta, AGENT_TUI_READY_TIMEOUT_MS);
4860
4926
  if (!ready.ready) {
4861
4927
  if (codexRateLimitModelSwitch) {
@@ -5433,7 +5499,7 @@ async function sendAgentMessageInLock(name, text, o) {
5433
5499
  "前の文の完了待ちが終わってから送り直してください。Grokは120秒受け取れないと、前の文を自分から取り消します。", 2);
5434
5500
  }
5435
5501
  if (running) {
5436
- return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery, preface: o.preface });
5502
+ return steerRunningTurn(name, meta, text, { raw: o.raw, pane_input_recovery: paneInputRecovery, preface: o.preface, follow: o.follow });
5437
5503
  }
5438
5504
  return dispatchAgentTurnInLock(name, text, { raw: o.raw, before_send: o.before_send, pane_input_recovery: paneInputRecovery, preface: o.preface });
5439
5505
  }
@@ -5445,7 +5511,26 @@ async function sendAgentMessageInLock(name, text, o) {
5445
5511
  * - Grok: 待ち行列へ入れた後に「send now」を押す。旧turnは`cancelled`(trigger=send_now)で閉じ、
5446
5512
  * 新turnが作業を継ぐ。完了判定はこの継ぎ目を完了と数えない(grokCompletionEvent)。
5447
5513
  */
5514
+ /**
5515
+ * 差し込みを打つ直前の位置と、文の頭を控える(ADR 0122)。控えられなくても差し込みは止めない。
5516
+ * 完了の位置は、そこから今までの待ちを始めると「打った後の最初の完了」で返る位置。Cursorの位置はuser発話の数で、
5517
+ * 差し込みが新しいuser発話として書かれない時も今の番の終わりで返るよう、今の番の手前を指す。
5518
+ */
5519
+ function recordSteerPosition(meta, body) {
5520
+ try {
5521
+ const completion = agentCompletionCursor(meta);
5522
+ const cursor = meta.kind === "cursor" ? Math.max(0, completion - 1) : completion;
5523
+ const entry = recordSteer(meta, interimFromNow(meta, cursor), body);
5524
+ return entry ? { steer_id: entry.steer_id, event_cursor: entry.event_cursor, interim_from: entry.interim_from } : null;
5525
+ }
5526
+ catch {
5527
+ return null;
5528
+ }
5529
+ }
5448
5530
  async function steerRunningTurn(name, meta, text, o) {
5531
+ const prefaced = splitAgentPreface(meta.kind, o.preface, text, { defaultForWrapped: !o.raw });
5532
+ text = prefaced.body;
5533
+ prepareSendText(text, { raw: o.raw });
5449
5534
  const receipt = {
5450
5535
  schema: "aiterm.agent-steer.v1",
5451
5536
  session_id: meta.aiterm_session,
@@ -5453,10 +5538,8 @@ async function steerRunningTurn(name, meta, text, o) {
5453
5538
  vendor: meta.kind,
5454
5539
  harness: agentHarness(meta.kind),
5455
5540
  pane_input_recovery: o.pane_input_recovery,
5541
+ ...(o.follow ? { follow: recordSteerPosition(meta, text) } : {}),
5456
5542
  };
5457
- const prefaced = splitAgentPreface(meta.kind, o.preface, text, { defaultForWrapped: !o.raw });
5458
- text = prefaced.body;
5459
- prepareSendText(text, { raw: o.raw });
5460
5543
  // Claudeのoperation相関は変えない。差し込みは実行中turnの一部であり、新しいturnを予約しない。
5461
5544
  const preserveAgentOperation = meta.kind === "claude";
5462
5545
  send(name, text, {
@@ -225,6 +225,18 @@ function codexLaunchVerdictMemoFor(launchId) {
225
225
  }
226
226
  return memo;
227
227
  }
228
+ // 起動ごとの、「この起動の会話ではない」と決まったrollout。古いrollout、Codex CLIのroot会話でない物、最初のturnまでにこの起動の印が無い物。
229
+ const codexLaunchOthers = new Map();
230
+ function codexLaunchOthersFor(launchId) {
231
+ let others = codexLaunchOthers.get(launchId);
232
+ if (!others) {
233
+ if (codexLaunchOthers.size >= CODEX_LAUNCH_VERDICT_MEMO_MAX)
234
+ codexLaunchOthers.delete(codexLaunchOthers.keys().next().value);
235
+ others = new Set();
236
+ codexLaunchOthers.set(launchId, others);
237
+ }
238
+ return others;
239
+ }
228
240
  /** 結び付けた会話(vendor_session_id)のroot rollout。会話の切り替えは追わない。完了の読み位置(cursor)はこのfileの位置。 */
229
241
  export function codexBoundTranscript(meta) {
230
242
  if (meta.kind !== "codex" || !meta.codex_home || !meta.vendor_session_id)
@@ -297,7 +309,17 @@ export function codexRootTranscript(meta) {
297
309
  return bound ? codexSwitchedTranscript(meta, bound) ?? bound : null;
298
310
  }
299
311
  if (meta.hook_route === "shared_codex_home") {
300
- const matches = listCodexTranscripts(meta.codex_home).filter((file) => codexTranscriptMatchesLaunch(file, meta));
312
+ // まだ会話を結び付けていない席(起こしてから、2つめの送信までの間)。この起動の印を持つrolloutを探す。
313
+ // 「この起動の物でない」と決まったrolloutは、後から変わらないので覚えて読み直さない(席の今の番は、結び付く前の席も見回りで聞かれる。ADR 0122)。
314
+ const others = codexLaunchOthersFor(meta.launch_id);
315
+ const matches = listCodexTranscripts(meta.codex_home).filter((file) => {
316
+ if (others.has(file))
317
+ return false;
318
+ const { verdict } = codexLaunchVerdict(file, meta);
319
+ if (verdict === "other")
320
+ others.add(file);
321
+ return verdict === "match";
322
+ });
301
323
  if (matches.length > 1) {
302
324
  throw new AitermError("共有CODEX_HOMEに同じlaunch markerのroot rolloutが複数あります。sessionを閉じて起動し直してください。", 2);
303
325
  }
@@ -306,7 +328,7 @@ export function codexRootTranscript(meta) {
306
328
  return listCodexTranscripts(meta.codex_home)[0] ?? null;
307
329
  }
308
330
  /** rolloutの会話ID。結び付けた会話のrolloutならそのID、会話が切り替わった後のrolloutなら、その頭に書かれたID。 */
309
- function codexRolloutSessionId(meta, transcript) {
331
+ export function codexRolloutSessionId(meta, transcript) {
310
332
  if (meta.vendor_session_id && path.basename(transcript).includes(meta.vendor_session_id))
311
333
  return meta.vendor_session_id;
312
334
  return codexTranscriptSessionId(transcript) ?? codexRolloutCreation(transcript)?.id ?? meta.vendor_session_id;
@@ -383,6 +405,8 @@ export function codexUsageLimit(record) {
383
405
  }
384
406
  // 完了待ちが、会話の切り替えを確かめる間隔。送った文はすぐ記録へ書かれるので、普通のturnはここへ来ない。
385
407
  const CODEX_SWITCH_CHECK_MS = 3_000;
408
+ // 完了待ちが、記録を1回に読む大きさ。
409
+ const CODEX_TRANSCRIPT_READ_CHUNK_BYTES = 4 * 1024 * 1024;
386
410
  /** 1行がturnの境界ならその境界、境界でなければundefined、境界の語を含むのに読めない行ならnull。 */
387
411
  function codexTurnBoundaryLine(line) {
388
412
  if (!line.includes("task_started") && !line.includes("task_complete") && !line.includes("turn_aborted"))
@@ -477,7 +501,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
477
501
  let transcript = anchored();
478
502
  let startOffset = requestedCursor ?? (transcript ? safeStatSize(transcript) : 0);
479
503
  let cursor = startOffset;
480
- let carry = "";
504
+ let carry = Buffer.alloc(0);
481
505
  let malformedEvents = 0;
482
506
  let discardLeadingFragment = false;
483
507
  let initializedBoundary = false;
@@ -506,6 +530,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
506
530
  signal?.throwIfAborted();
507
531
  if (!fs.existsSync(metadataFile))
508
532
  return observation("closed");
533
+ let behind = false;
509
534
  transcript ??= anchored();
510
535
  if (transcript && meta.vendor_session_id && cursor === startOffset && performance.now() >= switchCheckAt) {
511
536
  switchCheckAt = performance.now() + CODEX_SWITCH_CHECK_MS;
@@ -532,7 +557,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
532
557
  }
533
558
  transcript = current;
534
559
  startOffset = cursor;
535
- carry = "";
560
+ carry = Buffer.alloc(0);
536
561
  initializedBoundary = false;
537
562
  discardLeadingFragment = false;
538
563
  }
@@ -549,18 +574,26 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
549
574
  if (size < cursor) {
550
575
  throw new AitermError("Codex transcript が完了待機中に短くなりました。該当セッションを閉じて起動し直してください。", 2);
551
576
  }
552
- if (size - startOffset > CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES) {
553
- throw new AitermError("Codex transcript のturn増分が大きすぎます。該当セッションを閉じて起動し直してください。", 2);
554
- }
555
577
  if (size > cursor) {
556
- carry += readFileRange(transcript, cursor, size).toString("utf8");
557
- cursor = size;
558
- const parts = carry.split("\n");
559
- carry = parts.pop() ?? "";
578
+ // 1回に読むのは決めた大きさまで。番の大きさには上限を置かない。前は、読み始めから16MiBを超えて書いた番で、待ちが誤りで止まった。
579
+ // 席の今の番(ADR 0122)から待つ呼び手は、長く続いた番を頭から読む。
580
+ const chunk = readFileRange(transcript, cursor, Math.min(size, cursor + CODEX_TRANSCRIPT_READ_CHUNK_BYTES));
581
+ cursor += chunk.length;
582
+ behind = chunk.length > 0 && cursor < size;
583
+ const joined = carry.length ? Buffer.concat([carry, chunk]) : chunk;
584
+ const last = joined.lastIndexOf(0x0a);
585
+ const parts = last < 0 ? [] : joined.subarray(0, last).toString("utf8").split("\n");
586
+ carry = last < 0 ? joined : joined.subarray(last + 1);
560
587
  if (discardLeadingFragment && parts.length > 0) {
561
588
  parts.shift();
562
589
  discardLeadingFragment = false;
563
590
  }
591
+ // 改行が来ないまま上限を超えた行は、番の境界の行ではない。持たずに、その行の終わりまで読み飛ばす。
592
+ if (carry.length > CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES) {
593
+ carry = Buffer.alloc(0);
594
+ discardLeadingFragment = true;
595
+ malformedEvents++;
596
+ }
564
597
  const harnessSessionId = meta.vendor_session_id ? codexRolloutSessionId(meta, transcript) : codexTranscriptSessionId(transcript);
565
598
  for (const line of parts) {
566
599
  if (!line.trim())
@@ -595,6 +628,9 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
595
628
  }
596
629
  }
597
630
  }
631
+ // まだ読んでいない分が在る間は、待たずに読み進める(一度だけ見る照会も、今在る分は全部読んでから答える)。
632
+ if (behind)
633
+ continue;
598
634
  if (performance.now() >= deadline)
599
635
  return observation(timeout === 0 ? "running" : "timeout");
600
636
  await sleep(AGENT_DONE_POLL_MS);