aiterm-mcp 0.57.1 → 0.57.3
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 +53 -1
- package/README.ja.md +4 -1
- package/README.md +4 -1
- package/dist/core.js +1 -0
- package/dist/harnesses/codex.js +190 -20
- package/dist/harnesses/cursor.js +36 -5
- package/dist/harnesses/grok.js +1 -0
- package/dist/interim-words.js +6 -3
- package/docs/DESIGN.md +13 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.57.3] - 2026-10-08
|
|
11
|
+
|
|
12
|
+
### 修正
|
|
13
|
+
|
|
14
|
+
- Codexの席で、入力欄の`/new`・`/clear`で会話が切り替わった後、完了待ちが完了を拾えずに時間切れになっていた件を直す(ADR 0105)。
|
|
15
|
+
Codexは、processを生かしたまま、新しい会話を別の記録(rollout)へ書く。Aitermは最初の会話の記録を見続けていたので、
|
|
16
|
+
切り替えた後のturnは完了が届かず、回答の読み出しは前の会話の回答を返していた。席を閉じるまで直らなかった。
|
|
17
|
+
- 今の会話の記録を、結び付けた会話より後に書かれた、同じ起動の印を持つ派生でないroot会話(`codex-tui`・`cli`)から決める。
|
|
18
|
+
- 新しいturnを送る時に、席の`vendor_session_id`を今の会話へ結び付け直す。完了待ちは、読み始めた記録が3秒増えない時に今の会話を見直す。
|
|
19
|
+
- 引数、返りの形、断りの文面、会話が切り替わっていない席の読みは変えていない。BellTeamは席へ命令を打たないので、動きは変わらない。
|
|
20
|
+
- 切り替えた後の最初のturnでは、合間の言葉は出ず、完了は最長で3秒遅れる。派生させた会話(`/fork`)は追わない。
|
|
21
|
+
- 手で打った`/compact`の途中に届いた文が、差し込みで返るのにCodexでは後の別のturnになる件は、直していない(理由はADR 0105)。
|
|
22
|
+
自動の圧縮の途中に届いた文は、同じturnへ入る事を実物で確かめた。
|
|
23
|
+
|
|
24
|
+
- Cursorのturnが誤りで終わった時、完了待ちが`done`を返していた件を直す(ADR 0106)。
|
|
25
|
+
Cursorは、記録の`turn_ended`へ`status: "error"`と文を書く。Aitermは`status`を見ていなかったので、呼んだ側に、
|
|
26
|
+
失敗が、空の(または途中までの)回答の完了として届いていた。Codexで0.57.2に直したのと同じ形。
|
|
27
|
+
- サービスの誤り(`[unavailable] Error`など): `outcome: "error"`。`aiterm-wait`はexit 7。`error`は1行の文、
|
|
28
|
+
文の頭の角括弧の名前を`error_kind`で返す。
|
|
29
|
+
- 利用上限の文: `outcome: "rate_limited"`。画面から読めた時と同じ返りに揃える。
|
|
30
|
+
- 席で止めたturn(`User aborted request`)と、`status: "success"`は、今までどおり`done`。
|
|
31
|
+
- 引数、断りの文面、送信の振り分け、ほかの席は変えていない。
|
|
32
|
+
|
|
33
|
+
### Tests
|
|
34
|
+
|
|
35
|
+
- 試験の前処理(`test/seat-env.mjs`)が作る置き場(`/tmp/at-*`)を、試験processの終わりに消す(ADR 0104)。
|
|
36
|
+
今までは誰も消しておらず、全部の試験を1回流すと約100個残っていた。つながるtmuxのserverが残っている置き場は消さず、場所を標準エラーへ書く。
|
|
37
|
+
製品の動きは変えない。
|
|
38
|
+
- `test/codex-relay-official.test.mjs`を、対象のOS(macOSとWindows)でだけ流す。Linuxで公式のCodexの実行ファイルを指定すると、
|
|
39
|
+
対象でない確かめ(`ps`の`comm=`、socketの確かめ)で2件落ちていた。
|
|
40
|
+
- `test/claude-exit-before-close.test.mjs`の2件を、席を閉じた0.3秒後に記録を読む形から、切断の合図の記録が出るまで待つ形へ直す。
|
|
41
|
+
混んだmacOSのCIで、記録が0.3秒より遅れて1件落ちた。
|
|
42
|
+
|
|
43
|
+
## [0.57.2] - 2026-10-07
|
|
44
|
+
|
|
45
|
+
### 修正
|
|
46
|
+
|
|
47
|
+
- Codexのturnがサービスの誤りや通信の失敗(HTTP 500・401・途中で切れる応答など)で終わった時、完了待ちが`done`・空の回答を返していた件を直す(ADR 0103)。
|
|
48
|
+
Codexは、この時も記録へ`task_complete`を書く(回答は無く、`error`が付く)。Aitermは利用上限だけを`rate_limited`に分けていたので、
|
|
49
|
+
ほかの失敗は、呼んだ側に、空の回答の完了として届いていた。
|
|
50
|
+
- 完了待ちは`outcome: "error"`を返す。`aiterm-wait`はexit 7、親への配送は`outcome=error`。
|
|
51
|
+
- `error`は、応答の本文とURLを落とした1行の文にする(例「unexpected status 401 Unauthorized」)。
|
|
52
|
+
- Codexが記録した種類(`internal_server_error`・`http_connection_failed`など)を、新しい項目`error_kind`で返す。ほかの席と、`error`でない返りでは`null`。
|
|
53
|
+
- 合間の言葉(`kind: "error"`)も、利用上限のほかは同じ1行にする。
|
|
54
|
+
- 引数、`done`と`rate_limited`の返り、断りの文面、Claude Code・Grok・Cursorの席は変えていない。
|
|
55
|
+
|
|
56
|
+
### 文書
|
|
57
|
+
|
|
58
|
+
- 通信の失敗・取消・待ちの時間切れを実行時エラーとして記録しない事と、重さの決め方を、決まりとして書く(ADR 0102)。動きは変えない。
|
|
59
|
+
|
|
10
60
|
## [0.57.1] - 2026-10-07
|
|
11
61
|
|
|
12
62
|
### 修正
|
|
@@ -2380,7 +2430,9 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
2380
2430
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
2381
2431
|
provenance.
|
|
2382
2432
|
|
|
2383
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.
|
|
2433
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.3...HEAD
|
|
2434
|
+
[0.57.3]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.2...v0.57.3
|
|
2435
|
+
[0.57.2]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.1...v0.57.2
|
|
2384
2436
|
[0.57.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.57.0...v0.57.1
|
|
2385
2437
|
[0.57.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.56.1...v0.57.0
|
|
2386
2438
|
[0.56.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.56.0...v0.56.1
|
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.57.
|
|
212
|
+
**状態:** 開発継続中 · 現行公開版 **v0.57.3** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
213
213
|
|
|
214
214
|
### 更新と巻き戻し
|
|
215
215
|
|
|
@@ -259,6 +259,9 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
|
|
|
259
259
|
|
|
260
260
|
ClaudeがAPIエラーや安全判定の拒否で終了した時は、Stop hookが発火しなくても次の`pty_send`を新しいturnとして扱う。現在のturn開始後のエラー記録だけを確認し、過去のエラーで実行中のturnを解除しない。上流の拒否はエラーのまま返す。
|
|
261
261
|
|
|
262
|
+
Codexがサービスの誤りや通信の失敗でturnを打ち切った時は、完了待ちが`done`ではなく`error`を返す(`aiterm-wait`はexit 7、親への配送は`outcome=error`)。`error`は応答の本文とURLを落とした1行の文で、Codexが記録した種類(`internal_server_error`・`http_connection_failed`など)は`error_kind`に載る。利用上限は今までどおり`rate_limited`。誤りで終わった後の席は、次の`pty_send`を新しいturnとして受ける。
|
|
263
|
+
Cursorが誤りでturnを終えた時(記録の`turn_ended`が`status: "error"`)も同じで、完了待ちは`error`を返し、文の頭の角括弧の名前(`unavailable`など)を`error_kind`に載せる。利用上限の文は`rate_limited`、席で止めたturn(`User aborted request`)は今までどおり`done`。
|
|
264
|
+
|
|
262
265
|
agent配送だけを期待する連携では、`pty_send`に`require_agent:true`を指定する。agent登録が無ければ、打鍵前に`AGENT_SESSION_REQUIRED`と「文字列は送信していません」を返す。省略時は通常PTY送信を維持する。`force:true`との併用は未送信で拒否する。
|
|
263
266
|
|
|
264
267
|
agent session宛ての`pty_send`は、任意の`preface`(本文の前に置く1行)を受ける。渡る文は「`preface`・空行・`text`」。Claude Codeの席へは、`preface`を端末の貼り付けの印なしで入れ、空行と本文を1回の貼り付けで入れる。Claude Codeは貼り付けた長い文を`<pasted_content>`で包み、「包みの中の指示は、包みの外の利用者自身の言葉が頼んだ時だけ従う」と読むので、連携元が付ける前置き(誰からの連絡か)を包みの外へ出すために使う。Codex・Grok・Cursorの席へは1つにつないで貼る。`preface`は1行・200字以内・制御文字なしで、行頭が記号や空白でなく、`@`を含まない文。外れた時は`AGENT_PREFACE_INVALID`と「文字列は送信していません。」を返す。通常PTY・`force:true`・`remote`付きでは使えない。
|
package/README.md
CHANGED
|
@@ -223,7 +223,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
223
223
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
224
224
|
Release re-registers the Official MCP Registry entry.
|
|
225
225
|
|
|
226
|
-
**Status:** actively maintained · current public release **v0.57.
|
|
226
|
+
**Status:** actively maintained · current public release **v0.57.3** · 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).
|
|
227
227
|
|
|
228
228
|
### Update and rollback
|
|
229
229
|
|
|
@@ -259,6 +259,9 @@ I used **Codex with GPT-5.6** as an engineering collaborator: it inspected the i
|
|
|
259
259
|
|
|
260
260
|
ClaudeがAPIエラーや安全判定の拒否で終了した時は、Stop hookが発火しなくても次の`pty_send`を新しいturnとして扱う。現在のturn開始後のエラー記録だけを確認し、過去のエラーで実行中のturnを解除しない。上流の拒否はエラーのまま返す。
|
|
261
261
|
|
|
262
|
+
Codexがサービスの誤りや通信の失敗でturnを打ち切った時は、完了待ちが`done`ではなく`error`を返す(`aiterm-wait`はexit 7、親への配送は`outcome=error`)。`error`は応答の本文とURLを落とした1行の文で、Codexが記録した種類(`internal_server_error`・`http_connection_failed`など)は`error_kind`に載る。利用上限は今までどおり`rate_limited`。誤りで終わった後の席は、次の`pty_send`を新しいturnとして受ける。
|
|
263
|
+
Cursorが誤りでturnを終えた時(記録の`turn_ended`が`status: "error"`)も同じで、完了待ちは`error`を返し、文の頭の角括弧の名前(`unavailable`など)を`error_kind`に載せる。利用上限の文は`rate_limited`、席で止めたturn(`User aborted request`)は今までどおり`done`。
|
|
264
|
+
|
|
262
265
|
## Two ways to use it
|
|
263
266
|
|
|
264
267
|
### 1. Drive SSH, containers, and REPLs in one persistent terminal — the primitive
|
package/dist/core.js
CHANGED
package/dist/harnesses/codex.js
CHANGED
|
@@ -127,22 +127,31 @@ export function listCodexTranscripts(codexHome) {
|
|
|
127
127
|
visit(sessionsDir);
|
|
128
128
|
return files.sort();
|
|
129
129
|
}
|
|
130
|
-
|
|
130
|
+
/**
|
|
131
|
+
* rolloutがこの起動のroot会話かを、頭の1MBから読む。
|
|
132
|
+
* - match: Codex CLIのroot会話で、developerの文にこの起動の印がある。
|
|
133
|
+
* - other: root会話でない。または、最初のturnの文脈(turn_context)まで書かれているのに印が無い。後から変わらない。
|
|
134
|
+
* - undecided: 書いている途中などで、まだ決められない。
|
|
135
|
+
* forkedは、ほかの会話から派生した会話(session_metaのforked_from_id)か。派生は元の会話の文を写すので、印だけでは持ち主を決められない。
|
|
136
|
+
*/
|
|
137
|
+
function codexLaunchVerdict(file, meta) {
|
|
131
138
|
const createdAt = Date.parse(meta.created_at);
|
|
132
139
|
try {
|
|
133
140
|
const st = fs.statSync(file);
|
|
134
141
|
if (Number.isFinite(createdAt) && st.mtimeMs + 5_000 < createdAt)
|
|
135
|
-
return false;
|
|
142
|
+
return { verdict: "other", forked: false };
|
|
136
143
|
}
|
|
137
144
|
catch {
|
|
138
|
-
return false;
|
|
145
|
+
return { verdict: "undecided", forked: false };
|
|
139
146
|
}
|
|
140
147
|
const size = Math.min(safeStatSize(file), 1024 * 1024);
|
|
141
148
|
if (size === 0)
|
|
142
|
-
return false;
|
|
149
|
+
return { verdict: "undecided", forked: false };
|
|
143
150
|
const marker = `AITERM_AGENT_LAUNCH_ID=${meta.launch_id}`;
|
|
144
151
|
let rootCli = false;
|
|
152
|
+
let forked = false;
|
|
145
153
|
let launchMarker = false;
|
|
154
|
+
let firstTurnContext = false;
|
|
146
155
|
for (const line of readFileRange(file, 0, size).toString("utf8").split("\n")) {
|
|
147
156
|
if (!line.trim())
|
|
148
157
|
continue;
|
|
@@ -156,8 +165,11 @@ export function codexTranscriptMatchesLaunch(file, meta) {
|
|
|
156
165
|
if (record?.type === "session_meta") {
|
|
157
166
|
rootCli = record?.payload?.originator === "codex-tui" && record?.payload?.source === "cli";
|
|
158
167
|
if (!rootCli)
|
|
159
|
-
return false;
|
|
168
|
+
return { verdict: "other", forked: false };
|
|
169
|
+
forked = typeof record?.payload?.forked_from_id === "string" && record.payload.forked_from_id !== "";
|
|
160
170
|
}
|
|
171
|
+
if (record?.type === "turn_context")
|
|
172
|
+
firstTurnContext = true;
|
|
161
173
|
if (record?.type === "response_item" &&
|
|
162
174
|
record?.payload?.type === "message" &&
|
|
163
175
|
record?.payload?.role === "developer" &&
|
|
@@ -167,9 +179,12 @@ export function codexTranscriptMatchesLaunch(file, meta) {
|
|
|
167
179
|
item.text.includes(marker));
|
|
168
180
|
}
|
|
169
181
|
if (rootCli && launchMarker)
|
|
170
|
-
return
|
|
182
|
+
return { verdict: "match", forked };
|
|
171
183
|
}
|
|
172
|
-
return
|
|
184
|
+
return { verdict: rootCli && firstTurnContext ? "other" : "undecided", forked };
|
|
185
|
+
}
|
|
186
|
+
export function codexTranscriptMatchesLaunch(file, meta) {
|
|
187
|
+
return codexLaunchVerdict(file, meta).verdict === "match";
|
|
173
188
|
}
|
|
174
189
|
export function codexTranscriptSessionId(file) {
|
|
175
190
|
const size = Math.min(safeStatSize(file), AGENT_EVENT_TAIL_BYTES);
|
|
@@ -191,11 +206,96 @@ export function codexTranscriptSessionId(file) {
|
|
|
191
206
|
}
|
|
192
207
|
return null;
|
|
193
208
|
}
|
|
209
|
+
/** rolloutのfile名にある会話ID(UUIDv7)と、そこに入っている作成時刻(ms)。v7でなければnull。 */
|
|
210
|
+
function codexRolloutCreation(file) {
|
|
211
|
+
const id = /-([0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.jsonl$/.exec(path.basename(file))?.[1];
|
|
212
|
+
return id ? { id, ms: Number.parseInt(id.slice(0, 8) + id.slice(9, 13), 16) } : null;
|
|
213
|
+
}
|
|
214
|
+
// 起動ごとの、rolloutの判定の控え(file → この起動の会話か)。頭の読み直しを避ける。決められなかったfileは入れない。
|
|
215
|
+
const CODEX_LAUNCH_VERDICT_MEMO_MAX = 64;
|
|
216
|
+
const codexLaunchVerdictMemo = new Map();
|
|
217
|
+
function codexLaunchVerdictMemoFor(launchId) {
|
|
218
|
+
let memo = codexLaunchVerdictMemo.get(launchId);
|
|
219
|
+
if (!memo) {
|
|
220
|
+
if (codexLaunchVerdictMemo.size >= CODEX_LAUNCH_VERDICT_MEMO_MAX) {
|
|
221
|
+
codexLaunchVerdictMemo.delete(codexLaunchVerdictMemo.keys().next().value);
|
|
222
|
+
}
|
|
223
|
+
memo = new Map();
|
|
224
|
+
codexLaunchVerdictMemo.set(launchId, memo);
|
|
225
|
+
}
|
|
226
|
+
return memo;
|
|
227
|
+
}
|
|
228
|
+
/** 結び付けた会話(vendor_session_id)のroot rollout。会話の切り替えは追わない。完了の読み位置(cursor)はこのfileの位置。 */
|
|
229
|
+
export function codexBoundTranscript(meta) {
|
|
230
|
+
if (meta.kind !== "codex" || !meta.codex_home || !meta.vendor_session_id)
|
|
231
|
+
return null;
|
|
232
|
+
return findLatestCodexTranscript(meta.codex_home, meta.vendor_session_id);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* 結び付けた会話より後に書かれた、この起動の別の会話のroot rollout。無ければnull(ADR 0105)。
|
|
236
|
+
* Codexは、入力欄の /new・/clear で、processを生かしたまま新しい会話(別のrollout)へ移る。古いrolloutには何も書かない。
|
|
237
|
+
* 新しい会話の候補は、次を全部満たすrolloutだけ。
|
|
238
|
+
* - 会話IDがUUIDv7で、その作成時刻がこの席の起動より後。
|
|
239
|
+
* - 結び付けた会話のrolloutより後に書かれている(mtime)。
|
|
240
|
+
* - Codex CLIのroot会話で、派生(forked_from_id)でなく、developerの文にこの起動の印がある。
|
|
241
|
+
* 印は起動時に渡したdeveloper_instructionsから来るので、派生でない会話が持つなら、この席のprocessが始めた会話。
|
|
242
|
+
* 派生は元の会話の文を写すので、別の端末で派生させた会話を拾わないよう、追わない。
|
|
243
|
+
* 候補が複数なら、最後に書かれた物。
|
|
244
|
+
*/
|
|
245
|
+
function codexSwitchedTranscript(meta, bound) {
|
|
246
|
+
if (!meta.codex_home || meta.hook_route !== "shared_codex_home")
|
|
247
|
+
return null;
|
|
248
|
+
const launchedAt = Date.parse(meta.created_at);
|
|
249
|
+
if (!Number.isFinite(launchedAt))
|
|
250
|
+
return null;
|
|
251
|
+
let boundMtime;
|
|
252
|
+
try {
|
|
253
|
+
boundMtime = fs.statSync(bound).mtimeMs;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const memo = codexLaunchVerdictMemoFor(meta.launch_id);
|
|
259
|
+
let latest = null;
|
|
260
|
+
let latestMtime = boundMtime;
|
|
261
|
+
for (const file of listCodexTranscripts(meta.codex_home)) {
|
|
262
|
+
if (file === bound || memo.get(file) === false)
|
|
263
|
+
continue;
|
|
264
|
+
const creation = codexRolloutCreation(file);
|
|
265
|
+
if (!creation || creation.ms + 5_000 < launchedAt)
|
|
266
|
+
continue;
|
|
267
|
+
let mtime;
|
|
268
|
+
try {
|
|
269
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (mtime <= latestMtime)
|
|
275
|
+
continue;
|
|
276
|
+
let own = memo.get(file);
|
|
277
|
+
if (own === undefined) {
|
|
278
|
+
const { verdict, forked } = codexLaunchVerdict(file, meta);
|
|
279
|
+
if (verdict === "undecided")
|
|
280
|
+
continue;
|
|
281
|
+
own = verdict === "match" && !forked;
|
|
282
|
+
memo.set(file, own);
|
|
283
|
+
}
|
|
284
|
+
if (!own)
|
|
285
|
+
continue;
|
|
286
|
+
latest = file;
|
|
287
|
+
latestMtime = mtime;
|
|
288
|
+
}
|
|
289
|
+
return latest;
|
|
290
|
+
}
|
|
291
|
+
/** この起動の、今の会話のroot rollout。結び付けた会話から、同じprocessの新しい会話へ移っていれば、新しい方。 */
|
|
194
292
|
export function codexRootTranscript(meta) {
|
|
195
293
|
if (meta.kind !== "codex" || !meta.codex_home)
|
|
196
294
|
return null;
|
|
197
|
-
if (meta.vendor_session_id)
|
|
198
|
-
|
|
295
|
+
if (meta.vendor_session_id) {
|
|
296
|
+
const bound = findLatestCodexTranscript(meta.codex_home, meta.vendor_session_id);
|
|
297
|
+
return bound ? codexSwitchedTranscript(meta, bound) ?? bound : null;
|
|
298
|
+
}
|
|
199
299
|
if (meta.hook_route === "shared_codex_home") {
|
|
200
300
|
const matches = listCodexTranscripts(meta.codex_home).filter((file) => codexTranscriptMatchesLaunch(file, meta));
|
|
201
301
|
if (matches.length > 1) {
|
|
@@ -205,12 +305,22 @@ export function codexRootTranscript(meta) {
|
|
|
205
305
|
}
|
|
206
306
|
return listCodexTranscripts(meta.codex_home)[0] ?? null;
|
|
207
307
|
}
|
|
308
|
+
/** rolloutの会話ID。結び付けた会話のrolloutならそのID、会話が切り替わった後のrolloutなら、その頭に書かれたID。 */
|
|
309
|
+
function codexRolloutSessionId(meta, transcript) {
|
|
310
|
+
if (meta.vendor_session_id && path.basename(transcript).includes(meta.vendor_session_id))
|
|
311
|
+
return meta.vendor_session_id;
|
|
312
|
+
return codexTranscriptSessionId(transcript) ?? codexRolloutCreation(transcript)?.id ?? meta.vendor_session_id;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* 今の会話のroot rolloutを返し、席の登録の会話ID(vendor_session_id)をその会話へ合わせる。
|
|
316
|
+
* 会話が切り替わっていた時は、ここで結び付け直す。呼ぶのは席の持ち主(送信の道)だけ。完了待ちは書かない。
|
|
317
|
+
*/
|
|
208
318
|
export function bindCodexTranscriptSession(meta) {
|
|
209
319
|
const transcript = codexRootTranscript(meta);
|
|
210
320
|
if (!transcript)
|
|
211
321
|
return null;
|
|
212
|
-
const harnessSessionId = codexTranscriptSessionId(transcript);
|
|
213
|
-
if (harnessSessionId &&
|
|
322
|
+
const harnessSessionId = meta.vendor_session_id ? codexRolloutSessionId(meta, transcript) : codexTranscriptSessionId(transcript);
|
|
323
|
+
if (harnessSessionId && meta.vendor_session_id !== harnessSessionId) {
|
|
214
324
|
meta.vendor_session_id = harnessSessionId;
|
|
215
325
|
writeAgentMetadata(meta);
|
|
216
326
|
}
|
|
@@ -254,6 +364,16 @@ export function codexTurnError(record) {
|
|
|
254
364
|
return null;
|
|
255
365
|
return { message: message || info, info };
|
|
256
366
|
}
|
|
367
|
+
// 呼んだ側の画面にそのまま出る1行の文。Codexの文は、応答の本文・URLを含む事がある
|
|
368
|
+
// (実物 2026-10-07、codex 0.160.1: "unexpected status 401 Unauthorized: <応答の文>, url: http://…/v1/responses")。
|
|
369
|
+
// 「unexpected status」はHTTPの状態までで切り、ほかの文はURLを伏せる。改行は畳み、長さを切る。
|
|
370
|
+
const CODEX_ERROR_LINE_MAX = 200;
|
|
371
|
+
export function codexTurnErrorLine(error) {
|
|
372
|
+
const flat = error.message.replace(/\s+/g, " ").trim();
|
|
373
|
+
const status = /^unexpected status (\d{3}(?: [A-Za-z][A-Za-z' -]*)?)/.exec(flat);
|
|
374
|
+
const text = status ? `unexpected status ${status[1].trim()}` : flat.replace(/https?:\/\/[^\s)\]>"']+/gi, "…");
|
|
375
|
+
return text.length > CODEX_ERROR_LINE_MAX ? `${text.slice(0, CODEX_ERROR_LINE_MAX - 1)}…` : text;
|
|
376
|
+
}
|
|
257
377
|
// 利用上限はtask_completeのcodex_error_infoで見分ける。token_countのused_percentは100%のまま
|
|
258
378
|
// 返事が続くことがある(macbookの実記録で7千件以上)ので上限の印にしない。
|
|
259
379
|
// rate_limit_exceededは短い間の混雑で、Codexが自分で再試行するので上限に数えない。
|
|
@@ -261,6 +381,8 @@ export function codexUsageLimit(record) {
|
|
|
261
381
|
const error = codexTurnError(record);
|
|
262
382
|
return error?.info === "usage_limit_exceeded" ? error.message : null;
|
|
263
383
|
}
|
|
384
|
+
// 完了待ちが、会話の切り替えを確かめる間隔。送った文はすぐ記録へ書かれるので、普通のturnはここへ来ない。
|
|
385
|
+
const CODEX_SWITCH_CHECK_MS = 3_000;
|
|
264
386
|
/**
|
|
265
387
|
* この起動のroot rolloutの、末尾から見て最初のturnの境界。
|
|
266
388
|
* Codexはturnの開始でtask_started、終わりでtask_complete、止めた時はturn_aborted(reasonはinterrupted)を書く(0.160.1の実記録)。
|
|
@@ -322,7 +444,7 @@ export function latestCodexCompletion(meta, readTranscriptLines) {
|
|
|
322
444
|
const transcript = codexRootTranscript(meta);
|
|
323
445
|
if (!transcript)
|
|
324
446
|
return null;
|
|
325
|
-
const harnessSessionId = meta
|
|
447
|
+
const harnessSessionId = codexRolloutSessionId(meta, transcript);
|
|
326
448
|
let latest = null;
|
|
327
449
|
for (const line of readTranscriptLines(transcript)) {
|
|
328
450
|
if (!line.trim())
|
|
@@ -338,15 +460,21 @@ export function latestCodexCompletion(meta, readTranscriptLines) {
|
|
|
338
460
|
}
|
|
339
461
|
export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
|
|
340
462
|
const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
|
|
341
|
-
|
|
342
|
-
const
|
|
463
|
+
// 読み位置(cursor)は、送った時に結び付いていた会話のrolloutの位置。結び付けが済んだ席は、そのrolloutから読み始める。
|
|
464
|
+
const anchored = () => meta.vendor_session_id ? codexBoundTranscript(meta) : codexRootTranscript(meta);
|
|
465
|
+
let transcript = anchored();
|
|
466
|
+
let startOffset = requestedCursor ?? (transcript ? safeStatSize(transcript) : 0);
|
|
343
467
|
let cursor = startOffset;
|
|
344
468
|
let carry = "";
|
|
345
469
|
let malformedEvents = 0;
|
|
346
470
|
let discardLeadingFragment = false;
|
|
347
471
|
let initializedBoundary = false;
|
|
472
|
+
// 待ち始めてからrolloutが1行も増えない時、会話が切り替わっていないかを見る時刻(ADR 0105)。
|
|
473
|
+
let switchCheckAt = performance.now() + CODEX_SWITCH_CHECK_MS;
|
|
474
|
+
// 切り替わった後の会話では、前の会話の最後の書き込みより後の完了だけを数える。
|
|
475
|
+
let completedAfter = null;
|
|
348
476
|
const deadline = performance.now() + timeout * 1000;
|
|
349
|
-
const observation = (outcome, ev = null, rateLimit = null) => ({
|
|
477
|
+
const observation = (outcome, ev = null, rateLimit = null, failed = null) => ({
|
|
350
478
|
schema: "aiterm.agent-wait-result.v1",
|
|
351
479
|
session_id: meta.aiterm_session,
|
|
352
480
|
launch_id: meta.launch_id,
|
|
@@ -359,13 +487,44 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
|
|
|
359
487
|
malformed_events: malformedEvents,
|
|
360
488
|
at: ev?.at ?? null,
|
|
361
489
|
rate_limit: rateLimit,
|
|
362
|
-
error: null,
|
|
490
|
+
error: failed ? codexTurnErrorLine(failed) : null,
|
|
491
|
+
error_kind: failed?.info ?? null,
|
|
363
492
|
});
|
|
364
493
|
for (;;) {
|
|
365
494
|
signal?.throwIfAborted();
|
|
366
495
|
if (!fs.existsSync(metadataFile))
|
|
367
496
|
return observation("closed");
|
|
368
|
-
transcript ??=
|
|
497
|
+
transcript ??= anchored();
|
|
498
|
+
if (transcript && meta.vendor_session_id && cursor === startOffset && performance.now() >= switchCheckAt) {
|
|
499
|
+
switchCheckAt = performance.now() + CODEX_SWITCH_CHECK_MS;
|
|
500
|
+
let current = null;
|
|
501
|
+
try {
|
|
502
|
+
current = codexRootTranscript(meta);
|
|
503
|
+
}
|
|
504
|
+
catch { /* 読めない時は、今のrolloutを見続ける */ }
|
|
505
|
+
if (current && current !== transcript && safeStatSize(transcript) === cursor) {
|
|
506
|
+
// 送った後に、同じprocessの新しい会話へ移っていた。送る時に無かった会話なので、前の会話の最後の書き込みより後の記録が今回の分。
|
|
507
|
+
// 読み位置を指定せずに待ち始めた時は、今までどおり、今より後の完了を待つ。
|
|
508
|
+
const size = safeStatSize(current);
|
|
509
|
+
if (requestedCursor == null) {
|
|
510
|
+
cursor = size;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
try {
|
|
514
|
+
completedAfter = fs.statSync(transcript).mtimeMs;
|
|
515
|
+
}
|
|
516
|
+
catch {
|
|
517
|
+
completedAfter = null;
|
|
518
|
+
}
|
|
519
|
+
cursor = Math.max(0, size - CODEX_TRANSCRIPT_INCREMENT_MAX_BYTES);
|
|
520
|
+
}
|
|
521
|
+
transcript = current;
|
|
522
|
+
startOffset = cursor;
|
|
523
|
+
carry = "";
|
|
524
|
+
initializedBoundary = false;
|
|
525
|
+
discardLeadingFragment = false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
369
528
|
if (transcript) {
|
|
370
529
|
if (!initializedBoundary) {
|
|
371
530
|
if (cursor > 0) {
|
|
@@ -390,7 +549,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
|
|
|
390
549
|
parts.shift();
|
|
391
550
|
discardLeadingFragment = false;
|
|
392
551
|
}
|
|
393
|
-
const harnessSessionId = meta.vendor_session_id
|
|
552
|
+
const harnessSessionId = meta.vendor_session_id ? codexRolloutSessionId(meta, transcript) : codexTranscriptSessionId(transcript);
|
|
394
553
|
for (const line of parts) {
|
|
395
554
|
if (!line.trim())
|
|
396
555
|
continue;
|
|
@@ -401,11 +560,21 @@ export async function observeCodexDone(meta, timeout, requestedCursor, signal) {
|
|
|
401
560
|
try {
|
|
402
561
|
const record = JSON.parse(line);
|
|
403
562
|
const done = codexCompletionEvent(meta, harnessSessionId, record);
|
|
563
|
+
if (done && completedAfter !== null) {
|
|
564
|
+
const at = Date.parse(record.timestamp);
|
|
565
|
+
if (Number.isFinite(at) && at <= completedAfter)
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
404
568
|
if (done) {
|
|
405
569
|
// 上限の知らせはこのturnの終わりにだけ書かれる。画面やpane logの文字は、上限が明けた後も
|
|
406
570
|
// 残り、道具の出力や依頼文にも現れるので見ない(2026-09-28)。
|
|
407
571
|
const limited = codexUsageLimit(record);
|
|
408
|
-
|
|
572
|
+
if (limited)
|
|
573
|
+
return observation("rate_limited", done, limited);
|
|
574
|
+
// サービスの誤りや通信の失敗で終わったturnも、Codexはtask_completeを書く(errorつき、回答は無い)。
|
|
575
|
+
// 完了として返すと、失敗が空の回答に見える。Claude CodeのAPIエラーと同じく、typedな終了として返す(ADR 0103)。
|
|
576
|
+
const failed = codexTurnError(record);
|
|
577
|
+
return failed ? observation("error", done, null, failed) : observation("done", done);
|
|
409
578
|
}
|
|
410
579
|
}
|
|
411
580
|
catch {
|
|
@@ -690,7 +859,8 @@ export function codexMoreReasoningChoice(screen) {
|
|
|
690
859
|
export function codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable, exactCompletion = false) {
|
|
691
860
|
if (!meta.codex_home || !meta.vendor_session_id)
|
|
692
861
|
transcriptUnavailable();
|
|
693
|
-
|
|
862
|
+
// 完了待ちが切り替わった後の会話で完了を見つけた時も、同じ会話から回答を読む(ADR 0105)。
|
|
863
|
+
const transcript = codexRootTranscript(meta);
|
|
694
864
|
if (!transcript)
|
|
695
865
|
transcriptUnavailable();
|
|
696
866
|
const lines = readTranscriptLines(transcript);
|
package/dist/harnesses/cursor.js
CHANGED
|
@@ -114,6 +114,29 @@ export function bindCursorTranscriptSession(meta) {
|
|
|
114
114
|
}
|
|
115
115
|
return transcript;
|
|
116
116
|
}
|
|
117
|
+
// Cursorは、turnが誤りで終わると、turn_endedへ status "error" と文を書く(普通の終わりは "success" で、errorは無い)。
|
|
118
|
+
// 実記録(2026-08-24〜09-28、macbook 23件・rabbit 1件)にあった文:
|
|
119
|
+
// "[unavailable] Error" / "[resource_exhausted] Error"(サービスの誤り)
|
|
120
|
+
// "You've hit your usage limit You've saved $… Your usage limits will reset when …" / "Other Models usage limit reached Switched to …"(利用上限)
|
|
121
|
+
// "User aborted request"(席で止めたturn)
|
|
122
|
+
// 席で止めたturnは誤りに数えない(今までどおり完了として返す)。文は呼んだ側の画面にそのまま出るので、1行にしてURLを伏せ、長さを切る。
|
|
123
|
+
const CURSOR_ERROR_LINE_MAX = 200;
|
|
124
|
+
const CURSOR_LIMIT_LINE_MAX = 600;
|
|
125
|
+
export function cursorTurnError(record) {
|
|
126
|
+
if (record?.type !== "turn_ended" || record?.status !== "error")
|
|
127
|
+
return null;
|
|
128
|
+
const flat = typeof record.error === "string" ? record.error.replace(/\s+/g, " ").trim() : "";
|
|
129
|
+
if (/^user aborted request\b/i.test(flat))
|
|
130
|
+
return null;
|
|
131
|
+
const limit = /usage limit/i.test(flat);
|
|
132
|
+
const text = flat.replace(/https?:\/\/[^\s)\]>"']+/gi, "…") || "Cursorのturnが誤りで終わりました(理由の文は記録にありません)";
|
|
133
|
+
const max = limit ? CURSOR_LIMIT_LINE_MAX : CURSOR_ERROR_LINE_MAX;
|
|
134
|
+
return {
|
|
135
|
+
message: text.length > max ? `${text.slice(0, max - 1)}…` : text,
|
|
136
|
+
kind: /^\[([a-z][a-z0-9_]*)\]/i.exec(flat)?.[1].toLowerCase() ?? null,
|
|
137
|
+
limit,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
117
140
|
export function cursorCompletionEvent(meta, harnessSessionId, record, turnId) {
|
|
118
141
|
if (meta.kind !== "cursor" || record?.type !== "turn_ended" || typeof record?.status !== "string")
|
|
119
142
|
return null;
|
|
@@ -126,7 +149,7 @@ export function cursorCompletionEvent(meta, harnessSessionId, record, turnId) {
|
|
|
126
149
|
turn_id: turnId,
|
|
127
150
|
operation_id: null,
|
|
128
151
|
reason: `Cursor transcript turn_ended:${record.status}`,
|
|
129
|
-
done_status: "turn_done",
|
|
152
|
+
done_status: cursorTurnError(record) ? "turn_error" : "turn_done",
|
|
130
153
|
stop_hook_active: false,
|
|
131
154
|
at: new Date().toISOString(),
|
|
132
155
|
};
|
|
@@ -193,7 +216,7 @@ export async function observeCursorDone(meta, timeout, requestedCursor, readScre
|
|
|
193
216
|
const deadline = performance.now() + timeout * 1000;
|
|
194
217
|
// 画面を読むたびにtmuxを1回起動する。完了の見回りより間隔を空け、1回読んだ画面で利用上限とhook拒否の両方を見る。
|
|
195
218
|
const screenDue = pollGate(CURSOR_SCREEN_POLL_MS);
|
|
196
|
-
const observation = (outcome, ev = null, rateLimit = null, error = null) => ({
|
|
219
|
+
const observation = (outcome, ev = null, rateLimit = null, error = null, errorKind = null) => ({
|
|
197
220
|
schema: "aiterm.agent-wait-result.v1",
|
|
198
221
|
session_id: meta.aiterm_session,
|
|
199
222
|
launch_id: meta.launch_id,
|
|
@@ -207,6 +230,7 @@ export async function observeCursorDone(meta, timeout, requestedCursor, readScre
|
|
|
207
230
|
at: ev?.at ?? null,
|
|
208
231
|
rate_limit: rateLimit,
|
|
209
232
|
error,
|
|
233
|
+
error_kind: errorKind,
|
|
210
234
|
});
|
|
211
235
|
for (;;) {
|
|
212
236
|
signal?.throwIfAborted();
|
|
@@ -219,8 +243,13 @@ export async function observeCursorDone(meta, timeout, requestedCursor, readScre
|
|
|
219
243
|
if (state.userTurns > startBoundary && state.terminalRecord) {
|
|
220
244
|
const harnessSessionId = meta.vendor_session_id ?? cursorTranscriptSessionId(transcript);
|
|
221
245
|
const done = cursorCompletionEvent(meta, harnessSessionId, state.terminalRecord, `cursor:${state.userTurns}`);
|
|
222
|
-
if (done)
|
|
223
|
-
|
|
246
|
+
if (done) {
|
|
247
|
+
// 誤りで終わったturnを完了として返すと、失敗が空の回答に見える。Codexと同じく、typedな終了として返す(ADR 0106)。
|
|
248
|
+
const failed = cursorTurnError(state.terminalRecord);
|
|
249
|
+
if (failed?.limit)
|
|
250
|
+
return observation("rate_limited", done, failed.message);
|
|
251
|
+
return failed ? observation("error", done, null, failed.message, failed.kind) : observation("done", done);
|
|
252
|
+
}
|
|
224
253
|
}
|
|
225
254
|
}
|
|
226
255
|
// 最初の周回では必ず読む(timeout=0の照会も1回は読む)。
|
|
@@ -525,7 +554,9 @@ export function cursorTuiReady(screen) {
|
|
|
525
554
|
return /Cursor Agent/i.test(screen) &&
|
|
526
555
|
(CURSOR_COMPOSER_MARKER_RE.test(screen) || CURSOR_START_PROMPT_MARKER_RE.test(screen));
|
|
527
556
|
}
|
|
528
|
-
// 利用上限に達したCursor
|
|
557
|
+
// 利用上限に達したCursorは、入力欄も戻さずに説明を出して止まる(2026.09.26-dd393fe、macOSで実測)。
|
|
558
|
+
// transcriptには、user turnを残さずにturn_ended(status "error")だけを書く事がある(実記録で8件)。その時は完了の条件
|
|
559
|
+
// (user turnが増えている)に当たらないので、画面で見る。user turnがある時は、完了待ちが記録から読む(cursorTurnError)。
|
|
529
560
|
// Error: You've hit your usage limit
|
|
530
561
|
// You've saved $766 ... Switch to a different model or set a Spend Limit to continue with Sonnet. ...
|
|
531
562
|
// fallbackModel: / spendLimitHit: true / chatMessage: ...
|
package/dist/harnesses/grok.js
CHANGED
|
@@ -273,6 +273,7 @@ export async function observeGrokDone(meta, timeout, requestedCursor, detectRate
|
|
|
273
273
|
at: ev?.at ?? null,
|
|
274
274
|
rate_limit: rateLimit,
|
|
275
275
|
error: ev?.done_status === "turn_error" ? ev.reason : null,
|
|
276
|
+
error_kind: null,
|
|
276
277
|
});
|
|
277
278
|
for (;;) {
|
|
278
279
|
signal?.throwIfAborted();
|
package/dist/interim-words.js
CHANGED
|
@@ -14,7 +14,7 @@ import * as path from "node:path";
|
|
|
14
14
|
import { AitermError } from "./errors.js";
|
|
15
15
|
import { agentHarness, agentsDir, assertSessionName, LAUNCH_ID_RE, readFileRange, safeStatSize, writeJson0600, } from "./agent-shared.js";
|
|
16
16
|
import { claudeSessionTranscriptPath } from "./harnesses/claude.js";
|
|
17
|
-
import { codexRootTranscript, codexTurnError } from "./harnesses/codex.js";
|
|
17
|
+
import { codexBoundTranscript, codexRootTranscript, codexTurnError, codexTurnErrorLine } from "./harnesses/codex.js";
|
|
18
18
|
import { grokSessionDirectory } from "./harnesses/grok.js";
|
|
19
19
|
import { cursorTranscript } from "./harnesses/cursor.js";
|
|
20
20
|
/** BellTeamが要求の_metaへ入れる呼び手の印。値が "BellTeam" の時だけ合間の言葉を返す。 */
|
|
@@ -90,7 +90,9 @@ export function codexInterimWords(lines) {
|
|
|
90
90
|
const error = codexTurnError(record);
|
|
91
91
|
if (error) {
|
|
92
92
|
words.push({
|
|
93
|
-
|
|
93
|
+
// 利用上限はCodexの利用者向けの文をそのまま出す。ほかの誤りは、完了の返り(error)と同じ1行にする
|
|
94
|
+
// (応答の本文やURLが混ざる文があるため)。
|
|
95
|
+
text: error.info === "usage_limit_exceeded" ? error.message : codexTurnErrorLine(error),
|
|
94
96
|
kind: "error",
|
|
95
97
|
at: typeof record.timestamp === "string" ? record.timestamp : null,
|
|
96
98
|
turn_id: typeof payload.turn_id === "string" && payload.turn_id ? payload.turn_id : null,
|
|
@@ -246,8 +248,9 @@ export function readInterimWords(meta, after) {
|
|
|
246
248
|
const boundary = recorded?.boundary ?? 0;
|
|
247
249
|
if (meta.kind === "claude")
|
|
248
250
|
found = claudeInterimWords(readLines(claudeSessionTranscriptPath(meta), boundary));
|
|
251
|
+
// 境界は、送った時に結び付いていた会話のrolloutの位置。会話の切り替えは追わない(ADR 0105)。
|
|
249
252
|
else if (meta.kind === "codex")
|
|
250
|
-
found = codexInterimWords(readLines(codexRootTranscript(meta), boundary));
|
|
253
|
+
found = codexInterimWords(readLines(meta.vendor_session_id ? codexBoundTranscript(meta) : codexRootTranscript(meta), boundary));
|
|
251
254
|
else if (meta.kind === "grok")
|
|
252
255
|
found = grokInterimWords(readLines(grokChatHistory(meta)), boundary);
|
|
253
256
|
else
|
package/docs/DESIGN.md
CHANGED
|
@@ -52,6 +52,7 @@ sessionの表には環境が丸ごと入るので、名指しで登録した名
|
|
|
52
52
|
対象の一覧取得やserver終了が失敗した場合は登録を削除しない(ADR 0087)。
|
|
53
53
|
試験の正規入口(`npm test`とCI)は`test/seat-env.mjs`を前処理として読み、席の保存場所・系譜・tmux環境を外し、
|
|
54
54
|
試験processごとの一時領域を指定する。個別試験が指定する`TMPDIR`と`XDG_RUNTIME_DIR`もそのまま利用できる。
|
|
55
|
+
前処理は、作った一時領域をprocessの終わりに消す。つながるtmuxのserverが残っている時だけ、消さずに場所を標準エラーへ書く(ADR 0104)。
|
|
55
56
|
|
|
56
57
|
agent配送だけを期待する連携元は`pty_send(require_agent:true)`を指定する。agent登録が無ければ、通常PTYへ送る前に
|
|
57
58
|
`AGENT_SESSION_REQUIRED`と「文字列は送信していません」を返す。未送信を確定してから対象sessionを復旧できる。
|
|
@@ -65,6 +66,9 @@ agentの席への`pty_send`は、任意の`preface`(本文の前に置く1行
|
|
|
65
66
|
Codexは回答を流している間、画面に動作中の行を出さない。`pty_send`の振り分けは、Codexの席に限り、画面が入力待ちでも、この起動の記録の
|
|
66
67
|
最後の境界がturnの開始で、processが生きていて、0.4秒おいて見直しても記録が同じturnのままで画面が動いている時を、動いていると数える
|
|
67
68
|
(ADR 0101)。止まっている席を動いていると読まないよう、1つでも欠けたら新しいturnの道へ回す。
|
|
69
|
+
Codexの席の記録は、結び付けた会話(`vendor_session_id`)のrolloutか、それより後に書かれた、同じ起動の印を持つ派生でないroot会話のrollout。
|
|
70
|
+
入力欄の`/new`・`/clear`で会話が切り替わった後も、今の会話から完了・回答・turnの境界を読む。結び付け直すのは送信の道だけで、
|
|
71
|
+
完了待ちは、読み始めた記録が3秒増えない時に今の会話を見直す(ADR 0105)。
|
|
68
72
|
|
|
69
73
|
Windowsの`pty_close`は、Claude Codeの席を止める前に、C-cを間を空けずに2回送って、Claude Codeが自分で終わるのを待つ(ADR 0100)。
|
|
70
74
|
psmuxは切断の合図なしでsessionを止めるので、そのままでは会話終了のhook(SessionEnd)が走らない。動いている番の席へは、先にC-cを
|
|
@@ -348,6 +352,13 @@ agent sessionへの送信1本を守る`<name>.agent-send.lock`は持つ時間が
|
|
|
348
352
|
終了した持ち主のpidを別のprocessが使い回すと、pidだけでは生きて見える。生きて見える持ち主は、processが始まってからの経過とlockを作ってからの時間を比べ、
|
|
349
353
|
lockより後に始まったprocessなら別人と読んで片付ける。消す役の印は、5秒より古ければ残った物と読む。`pty_close`は、どちらも片付ける。
|
|
350
354
|
|
|
355
|
+
Codexは、サービスの誤りや通信の失敗でturnを打ち切る時も、記録へ`task_complete`を書く(回答は無く、`error`が付く)。完了待ちは、
|
|
356
|
+
利用上限(`usage_limit_exceeded`)を`rate_limited`、ほかの`error`つきを`error`で返す。`error`は応答の本文とURLを落とした1行の文、
|
|
357
|
+
Codexが書いた種類は`error_kind`で返す。失敗を空の回答の完了として返さない(ADR 0103)。判定と文の整え方は`src/harnesses/codex.ts`が所有する。
|
|
358
|
+
Cursorは、誤りで終わったturnを、記録の`turn_ended`へ`status: "error"`と文で書く。完了待ちは、利用上限の文を`rate_limited`、
|
|
359
|
+
席で止めたturn(`User aborted request`)を今までどおり`done`、ほかを`error`で返す。`error`は1行の文、文の頭の角括弧の名前は
|
|
360
|
+
`error_kind`で返す(ADR 0106)。判定と文の整え方は`src/harnesses/cursor.ts`が所有する。
|
|
361
|
+
|
|
351
362
|
Grokのread-only sandbox起動拒否は、`src/harnesses/grok.ts`の
|
|
352
363
|
`assertGrokSandboxNotRejected`がCLIのエラー表示から検出する。`src/core.ts`の共通入力受付待機は
|
|
353
364
|
Grokの場合だけこの判定を呼び、`GROK_SANDBOX_STARTUP_FAILED`で原因と未送信を返す。
|
|
@@ -390,7 +401,8 @@ Claude・Grokは2秒、Cursorは1秒に1回で、待ちに入った最初の周
|
|
|
390
401
|
`diagnostics`はread-onlyで、PTY backendとagent dependencyを検査する。runtime error aggregateは
|
|
391
402
|
製品所有のlocal stateに固定codeと集約metadataだけを保存し、既定ではnetwork I/Oを持たない。
|
|
392
403
|
BugHubへの報告は、利用者が明示して有効にし、合鍵のファイルがある端末だけが、別processで行う。
|
|
393
|
-
きっかけは記録・解決・起動・手動の4つで、定期的な見張りは置かない。受け取り済みにするのは応答の署名まで確かめた時だけ(ADR 0079
|
|
404
|
+
きっかけは記録・解決・起動・手動の4つで、定期的な見張りは置かない。受け取り済みにするのは応答の署名まで確かめた時だけ(ADR 0079)。
|
|
405
|
+
記録するのは、Aitermが動く前提が欠けた時の固定の3種類だけ。通信の失敗・取消・待ちの時間切れは記録せず、呼んだ側へ返す(ADR 0102)。工場reporterとの
|
|
394
406
|
連携は明示opt-inの任意adapterであり、未設定時もAiterm本体は単独動作する。raw error、prompt、出力、
|
|
395
407
|
transcript、path、credentialを保存・公開しない。
|
|
396
408
|
親配送hook(Claude Code・Cursor)の登録状態は`src/parent-hook-diagnostic.ts`が利用者設定の読取りだけで要約し、
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.57.
|
|
3
|
+
"version": "0.57.3",
|
|
4
4
|
"mcpName": "io.github.kitepon/aiterm-mcp",
|
|
5
5
|
"description": "Persistent terminal MCP with one harness-based launcher for Claude Code, Codex CLI, Grok CLI, and Cursor Agent CLI, plus durable PTYs for SSH, containers, and REPLs.",
|
|
6
6
|
"keywords": [
|