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 +54 -1
- package/README.ja.md +1 -1
- package/README.md +2 -2
- package/dist/agent-shared.js +13 -7
- package/dist/aiterm-wait-cli.js +17 -3
- package/dist/claude-stop-hook.js +22 -1
- package/dist/core.js +101 -18
- package/dist/harnesses/codex.js +47 -11
- package/dist/index.js +27 -7
- package/dist/interim-words.js +80 -8
- package/dist/parent-delivery.js +2 -1
- package/dist/steer-follow.js +610 -0
- package/package.json +3 -3
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.
|
|
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.
|
|
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.
|
|
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
|
|
package/dist/agent-shared.js
CHANGED
|
@@ -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
|
|
60
|
-
for (let
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
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
|
}
|
package/dist/aiterm-wait-cli.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/dist/claude-stop-hook.js
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
2885
|
-
|
|
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
|
-
|
|
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, {
|
package/dist/harnesses/codex.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
|
|
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);
|