aiterm-mcp 0.33.1 → 0.34.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 +15 -1
- package/README.ja.md +13 -3
- package/README.md +12 -5
- package/dist/codex-parent-receiver.js +135 -0
- package/dist/core.js +30 -10
- package/dist/harnesses/codex.js +32 -2
- package/dist/harnesses/cursor.js +2 -1
- package/dist/harnesses/grok.js +2 -1
- package/dist/index.js +63 -22
- package/dist/parent-delivery.js +382 -0
- package/dist/setup-integrations.js +11 -0
- package/docs/00_overview.md +4 -0
- package/docs/DESIGN.md +25 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.34.0] - 2026-09-10
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Codex親への子の回答自動配送を追加した。起動・通常dispatch・Claude durable turnの完了をAitermが観測し、回答本文を公式受信キューへ送る。親の待機コマンドと回答回収、子への送信指示は不要になった。
|
|
15
|
+
- 配送IDと状態をreceipt/`pty_observe`へ追加した。再接続後は未送信の記録を再開し、送信結果が不明な場合は本文を保持して自動再送しない。
|
|
16
|
+
- `aiterm-setup`はCodexの公式queue入口を確認する。各dispatchでMCP要求の親threadを確認し、未対応の受信口へ子を送らない。
|
|
17
|
+
|
|
18
|
+
### Compatibility
|
|
19
|
+
|
|
20
|
+
- Codex親は公式の`_meta.threadId`と`thread/queue` APIを提供する環境が必要。Codex CLI 0.154.0で確認した。native sub-agentを親とする外部queue入力はCodexの制約により未対応。
|
|
21
|
+
- Claude等の親の既存waiter契約は維持する。旧版へ戻すと新しい配送記録は処理されないが、既存PTY/harness stateの形式は変わらない。
|
|
22
|
+
|
|
10
23
|
## [0.33.1] - 2026-09-09
|
|
11
24
|
|
|
12
25
|
### Fixed
|
|
@@ -1604,7 +1617,8 @@ prototype (preserved under `prototype/python/` as the porting source and referen
|
|
|
1604
1617
|
`ubuntu-latest` for Node 18/20/22, publishing to npm on `v*` tags with
|
|
1605
1618
|
provenance.
|
|
1606
1619
|
|
|
1607
|
-
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.
|
|
1620
|
+
[Unreleased]: https://github.com/kitepon/aiterm-mcp/compare/v0.34.0...HEAD
|
|
1621
|
+
[0.34.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.33.1...v0.34.0
|
|
1608
1622
|
[0.33.1]: https://github.com/kitepon/aiterm-mcp/compare/v0.33.0...v0.33.1
|
|
1609
1623
|
[0.33.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.32.0...v0.33.0
|
|
1610
1624
|
[0.32.0]: https://github.com/kitepon/aiterm-mcp/compare/v0.31.2...v0.32.0
|
package/README.ja.md
CHANGED
|
@@ -163,7 +163,7 @@ v0.20では、待たずに一度だけ観測する
|
|
|
163
163
|
`running`(exit 5)で返すようにしました。v0.19系では相関済みClaude approval中継を追加し、
|
|
164
164
|
複数行shell配送を維持し、native Windowsのfactory diagnosticsを拡張しました。
|
|
165
165
|
v0.16/0.17以来、親エージェントはaiterm上で一切ブロックしません:
|
|
166
|
-
agent session への send は常に非ブロック dispatch
|
|
166
|
+
agent session への send は常に非ブロック dispatch になり、Codex親には回答本文を自動配送し、それ以外の親の完了待ちは `aiterm-wait`
|
|
167
167
|
(exit code が receipt の outcome を映す: 0=done / 3=timeout=未完了 /
|
|
168
168
|
4=closed / 5=running=待たない観測)、初回 prompt 付き
|
|
169
169
|
launch は structured receipt にコピペ可能な `wait_command` を含む。factory diagnostics と local
|
|
@@ -171,7 +171,7 @@ runtime-error store は canonical dotagents config の `collection.enabled: true
|
|
|
171
171
|
場合だけ収集し、既定OFF、network送信は行いません。tag起点CIのnpm provenance(OIDC Trusted
|
|
172
172
|
Publishing)で公開し、GitHub Release が Official MCP Registry を再登録します。
|
|
173
173
|
|
|
174
|
-
**状態:** 開発継続中 · 現行公開版 **v0.
|
|
174
|
+
**状態:** 開発継続中 · 現行公開版 **v0.34.0** · 動作対象は Linux · WSL2 · macOS · Windows ネイティブ · MIT · [変更履歴](CHANGELOG.md)。
|
|
175
175
|
|
|
176
176
|
### 更新と巻き戻し
|
|
177
177
|
|
|
@@ -205,7 +205,7 @@ pty_read(id, { wait: true }) → 削減済みの出力を読む(完了
|
|
|
205
205
|
|
|
206
206
|
同じprimitiveが別エージェントのTUIを宿す。`agent_launch`の`harness`はagent loop・認証・hook・session・transcriptを所有する実行基盤、`model`は独立した選択。起動processは直接CLIと同じproject/user環境を使い、通常config、MCP、plugin、skill、permission、trust、memory、historyをcopy・filter・置換しない。
|
|
207
207
|
|
|
208
|
-
`aiterm.agent-launch-result.v1`は正規`harness`を返し、旧`provider`は互換fieldとして残す。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`のagent行にも載り、旧vendor/provider/agent fieldは互換用に残る。Codexは通常rollout、Grok CLIは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcript末尾の`turn_ended`を完了正本に使う。`pty_send`は非ブロックdispatchで、vendor別完了境界を表すopaqueな整数`event_cursor
|
|
208
|
+
`aiterm.agent-launch-result.v1`は正規`harness`を返し、旧`provider`は互換fieldとして残す。同じ`harness`はagent dispatch、`aiterm-wait`、`agent_configure`、`pty_list`のagent行にも載り、旧vendor/provider/agent fieldは互換用に残る。Codexは通常rollout、Grok CLIは通常session event、Claudeはlaunch固有Stop hook、Cursorは通常agent transcript末尾の`turn_ended`を完了正本に使う。`pty_send`は非ブロックdispatchで、vendor別完了境界を表すopaqueな整数`event_cursor`を返し、Codex親には回答本文を公式受信キューへ自動配送する。それ以外の親は`aiterm-wait`を親のターンを塞がない別processで受ける。Cursorのsubmitキーはadapterが現行CLIのextended keyboard protocolへ変換する。送信textがCursorのcomposerへ残った場合は成功receiptを返さず失敗する。
|
|
209
209
|
|
|
210
210
|
`agent_launch`・`pty_send`(agent dispatch)・`agent_steer`は任意の`image`(画像ファイルの絶対パスの配列。png/jpg/jpeg/gif/webp)を受ける。aitermが本文末尾へ添付行を付け、どのharnessも自分のfile読取toolでそのpathを画像として開く。呼出し側はharness別の添付手順を覚えない。不正なpathは送信前に拒否する。
|
|
211
211
|
|
|
@@ -225,6 +225,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
|
|
|
225
225
|
→ { session_id: "codex1", … } # Codex が永続端末で稼働開始
|
|
226
226
|
pty_read("codex1", { screen: true }) → 何をしているか読む(トークン削減)
|
|
227
227
|
pty_send("codex1", "also fix the imports it broke") # 非ブロックdispatch=event_cursor入りreceipt
|
|
228
|
+
# Codex親には回答が自動で届く。それ以外の親:
|
|
228
229
|
$ aiterm-wait --session codex1 --cursor <event_cursor> # exit 0=done / 3=timeout(未完了) / 4=closed / 7=error(APIエラー等でturn打ち切り)。回収は pty_read(agent_transcript:true)
|
|
229
230
|
→ 操舵し、Codex の次の入力境界で返る
|
|
230
231
|
```
|
|
@@ -513,6 +514,15 @@ SSH先がPowerShellの場合、`mark:true`は現在の標準`PS ...>`プロン
|
|
|
513
514
|
|
|
514
515
|
`pty_read({ wait: true })`は通常PTYを、process終了/`mark:true` sentinel/`until`一致/shell復帰を伴う出力静止/timeoutの5層で判定する。agent sessionは第6の正確な層を使い、Codexは通常rollout、Grokは通常session event、Claudeはlaunch相関Stop event、Cursorは通常agent transcriptの`turn_ended`を`aiterm-wait --cursor`が観測する。親はブロックもポーリングもしない。
|
|
515
516
|
|
|
517
|
+
### Codex親への回答自動配送
|
|
518
|
+
|
|
519
|
+
Codexから子を起動・通常dispatchした後は、別作業へ進むか親のturnを終了するだけでよい。Aitermが完了を観測し、加工前の回答を保存して、依頼元Codexの公式受信キューへ送る。親はidleになった後に回答を処理する。waiter、`pty_read`による回答回収、子への送信指示は不要。Desktop固有の接続を使わず、CLIでも同じ経路になる。子は全対応harnessから選べる。Claude等の親は既存のwaiter経路を使う。
|
|
520
|
+
|
|
521
|
+
自動配送時はreceiptに`parent_delivery`が付き、`wait_process`/`wait_command`はnullになる。`pty_observe`の`parent_deliveries`で`waiting`、`ready`、`sending`、`submitted`、`failed`、`unknown`を確認できる。`submitted`はキューの受付済みであり、modelの読了ではない。MCP再接続後は未送信の記録を再開し、送信中に接続が切れて結果が分からない場合は本文を保持して`unknown`とする。自動再送はしない。
|
|
522
|
+
|
|
523
|
+
CodexにはMCPの`_meta.threadId`と公式`thread/queue` APIが必要で、Codex CLI 0.154.0で確認している。`aiterm-setup`はインストールされた公式queue入口を確認し、各dispatchでは実際の親threadの受入可否を確認する。Codexのnative sub-agentは外部からのqueue入力を拒否するため、自動配送の親としては未対応。
|
|
524
|
+
|
|
525
|
+
|
|
516
526
|
### トークン削減
|
|
517
527
|
|
|
518
528
|
- `pty_read` は既定で制御文字除去・連続重複圧縮・head+tail 折りたたみ(+復元ヒント・メタ併記)をかける。
|
package/README.md
CHANGED
|
@@ -176,8 +176,8 @@ a non-blocking `aiterm-wait --timeout 0` observation (`running`, exit 5) from a
|
|
|
176
176
|
wait. The v0.19 line added the correlated Claude approval relay,
|
|
177
177
|
preserved multiline shell delivery, and extended factory diagnostics on native
|
|
178
178
|
Windows. As of v0.16/0.17 a parent agent never blocks on aiterm:
|
|
179
|
-
every send to an agent session is a non-blocking dispatch
|
|
180
|
-
|
|
179
|
+
every send to an agent session is a non-blocking dispatch. Codex parents now receive the complete answer automatically; other parents use the
|
|
180
|
+
`aiterm-wait` waiter whose exit codes mirror the receipt outcome
|
|
181
181
|
(`0`=done / `3`=timeout, not finished / `4`=closed / `5`=running for a
|
|
182
182
|
zero-time observation), and a launch with an
|
|
183
183
|
initial prompt returns a ready-made `wait_command` in its structured receipt.
|
|
@@ -187,7 +187,7 @@ collection is off by default and performs no network I/O. It ships via
|
|
|
187
187
|
tag-triggered CI with npm provenance (OIDC Trusted Publishing); the GitHub
|
|
188
188
|
Release re-registers the Official MCP Registry entry.
|
|
189
189
|
|
|
190
|
-
**Status:** actively maintained · current public release **v0.
|
|
190
|
+
**Status:** actively maintained · current public release **v0.34.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).
|
|
191
191
|
|
|
192
192
|
### Update and rollback
|
|
193
193
|
|
|
@@ -227,7 +227,7 @@ pty_read(id, { wait: true }) → read the token-reduced output, completion
|
|
|
227
227
|
|
|
228
228
|
The same primitive hosts another agent's TUI. `agent_launch` starts a selected execution harness inside a fresh persistent terminal and returns a `session_id`. `harness` names the component that owns the agent loop, authentication, hooks, session, and transcript; `model` remains an independent choice. The launched process sees the same project and user environment as a direct CLI invocation: normal configuration, MCPs, plugins, skills, permissions, trust decisions, memory, and history are not copied, filtered, or replaced. Aiterm adds only completion correlation and a non-user sub-agent context containing `role=subagent`, the parent session, delegation depth, lineage, and `delegation_allowed=true`.
|
|
229
229
|
|
|
230
|
-
The human-readable launch text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt containing the canonical `harness`; the old `provider` field remains for compatibility. The same `harness` is carried by agent dispatch, `aiterm-wait`, `agent_configure`, and agent rows in `pty_list`, while their old vendor/provider/agent fields remain compatibility fields. Codex completion comes from its normal durable rollout transcript, Grok CLI from its normal session events, Claude Code from a launch-specific Stop hook settings addition, and Cursor from its normal agent transcript's terminal `turn_ended` record. Sending to any agent session is a non-blocking **dispatch** — the call returns immediately with an opaque, harness-specific integer `event_cursor`, and
|
|
230
|
+
The human-readable launch text is accompanied by an `aiterm.agent-launch-result.v1` structured receipt containing the canonical `harness`; the old `provider` field remains for compatibility. The same `harness` is carried by agent dispatch, `aiterm-wait`, `agent_configure`, and agent rows in `pty_list`, while their old vendor/provider/agent fields remain compatibility fields. Codex completion comes from its normal durable rollout transcript, Grok CLI from its normal session events, Claude Code from a launch-specific Stop hook settings addition, and Cursor from its normal agent transcript's terminal `turn_ended` record. Sending to any agent session is a non-blocking **dispatch** — the call returns immediately with an opaque, harness-specific integer `event_cursor`, and Codex parents receive the answer automatically through the official input queue. Other parents use [`aiterm-wait`](#completion-push-for-parent-agents-aiterm-wait). The Cursor adapter translates submit into the current CLI's extended keyboard protocol. If submitted text remains in Cursor's composer, its dispatch fails instead of returning a successful receipt.
|
|
231
231
|
|
|
232
232
|
`agent_launch`, `pty_send` (agent dispatch), and `agent_steer` 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.
|
|
233
233
|
|
|
@@ -250,6 +250,7 @@ agent_launch({ harness: "codex-cli", session_name: "codex1", cwd: "/repo",
|
|
|
250
250
|
pty_read("codex1", { screen: true }) → read what it's doing (token-reduced)
|
|
251
251
|
pty_send("codex1", "also fix the imports it broke")
|
|
252
252
|
→ non-blocking dispatch; receipt carries event_cursor
|
|
253
|
+
# Codex parents receive the answer automatically. For other parents:
|
|
253
254
|
$ aiterm-wait --session codex1 --cursor <event_cursor> # never in the parent's foreground; exit 0=done, 3=timeout (not done), 4=closed, 7=error (turn aborted by an API error)
|
|
254
255
|
pty_read("codex1", { agent_transcript: true }) → collect the full answer
|
|
255
256
|
```
|
|
@@ -550,7 +551,13 @@ For PowerShell over SSH, `mark:true` recognizes the current standard `PS ...>` p
|
|
|
550
551
|
|
|
551
552
|
### Completion push for parent agents (`aiterm-wait`)
|
|
552
553
|
|
|
553
|
-
|
|
554
|
+
**Codex parents receive child answers automatically.** Launch or send a request, then continue other work or end the turn. Aiterm observes the child, saves the unabridged answer, and submits it to the requesting parent's official Codex input queue. The parent processes it when idle. No waiter, `pty_read`, child callback instructions, or Desktop-specific connection is needed. This covers all supported child harnesses; Claude and other parent hosts retain the waiter flow below.
|
|
555
|
+
|
|
556
|
+
Automatic receipts include `parent_delivery` and set `wait_process` / `wait_command` to null. `pty_observe` exposes `parent_deliveries`: `waiting`, `ready`, `sending`, `submitted`, `failed`, or `unknown`. `submitted` means accepted by the queue, not read by the model. Aiterm retains the answer and resumes unsent work after an MCP restart. An interrupted submission becomes `unknown` and is not blindly retried.
|
|
557
|
+
|
|
558
|
+
Use a Codex runtime that supplies MCP `_meta.threadId` and the official `thread/queue` API (verified with Codex CLI 0.154.0). `aiterm-setup` checks the installed queue entry point; Aiterm verifies the requesting thread before each dispatch. Codex native sub-agents reject external queue input and cannot be automatic-delivery parents. Ordinary CLI and Desktop parents use the same supported route.
|
|
559
|
+
|
|
560
|
+
**For other parent hosts**, dispatch and start the receipt's waiter in a separate process:
|
|
554
561
|
|
|
555
562
|
1. Launch the child with `agent_launch({ harness: ... })`; every launch shares the normal project/user environment and adds only completion correlation plus lineage. Send a turn with plain `pty_send` (or `claude_turn issue` for durable Claude operations). The call returns immediately with an `event_cursor` in its structured receipt.
|
|
556
563
|
2. Pass the receipt's `wait_process.executable` and `wait_process.args` unchanged to a true argv process API. PowerShell 7's `Start-Process` is the exception because it joins `-ArgumentList` arrays; pass `windows_start_process_argument_list` as its one ready-made argument string instead. This invokes the bundled waiter through the exact Node runtime that is already running aiterm, including on native Windows where npm's human-facing bin is a PowerShell script shim and install paths may contain spaces. `wait_command` remains a compatibility display string for humans. The waiter observes the harness-owned completion source, plus Claude's additive launch hook, as a **pure reader** and exits with a one-line `aiterm.agent-wait-result.v1` receipt. **Exit ≠ done**: the receipt's `outcome` is authoritative (`0` = `done`, `3` = `timeout`, `4` = `closed`, `1` = error).
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Codex親の公式受信キューへの接続。親threadのload/resumeやDesktop固有通信は行わない。
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { resolveAgentBin } from "./agent-resolver.js";
|
|
6
|
+
import { realCodexHome } from "./harnesses/codex.js";
|
|
7
|
+
import { AitermError } from "./errors.js";
|
|
8
|
+
export class CodexDeliveryError extends AitermError {
|
|
9
|
+
delivery_code;
|
|
10
|
+
outcome_unknown;
|
|
11
|
+
constructor(delivery_code, message, outcome_unknown = false) {
|
|
12
|
+
super(`${delivery_code}: ${message}`, 2);
|
|
13
|
+
this.delivery_code = delivery_code;
|
|
14
|
+
this.outcome_unknown = outcome_unknown;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** modelの引数ではなく、CodexがMCP要求へ付けるmetadataだけを宛先にする。 */
|
|
18
|
+
export function codexParentFromRequest(clientName, metadata) {
|
|
19
|
+
if (clientName !== "codex-mcp-client")
|
|
20
|
+
return null;
|
|
21
|
+
const threadId = metadata?.threadId;
|
|
22
|
+
if (typeof threadId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(threadId)) {
|
|
23
|
+
throw new CodexDeliveryError("CODEX_PARENT_ID_UNAVAILABLE", "MCP要求に親のthreadIdがありません。対応するCodexへ更新してください");
|
|
24
|
+
}
|
|
25
|
+
return { thread_id: threadId, codex_home: path.resolve(realCodexHome()) };
|
|
26
|
+
}
|
|
27
|
+
async function withCodexReceiver(parent, action, runtime = {}) {
|
|
28
|
+
const executable = runtime.executable ?? resolveAgentBin("codex");
|
|
29
|
+
if (!executable)
|
|
30
|
+
throw new CodexDeliveryError("CODEX_RECEIVER_UNAVAILABLE", "Codexの実行ファイルを確認できません");
|
|
31
|
+
const child = spawn(executable, runtime.args ?? ["app-server", "--listen", "stdio://"], {
|
|
32
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
33
|
+
env: { ...process.env, CODEX_HOME: parent.codex_home },
|
|
34
|
+
windowsHide: true,
|
|
35
|
+
});
|
|
36
|
+
const pending = new Map();
|
|
37
|
+
let sequence = 0;
|
|
38
|
+
let stopped = false;
|
|
39
|
+
let transportError = null;
|
|
40
|
+
const failTransport = (message) => {
|
|
41
|
+
transportError = message;
|
|
42
|
+
for (const item of pending.values()) {
|
|
43
|
+
clearTimeout(item.timer);
|
|
44
|
+
item.reject(new CodexDeliveryError("CODEX_RECEIVER_TRANSPORT_FAILED", message, item.method === "thread/queue/add"));
|
|
45
|
+
}
|
|
46
|
+
pending.clear();
|
|
47
|
+
};
|
|
48
|
+
child.on("error", (error) => failTransport(`Codexを起動できません(${error.code ?? "unknown"})`));
|
|
49
|
+
child.stdin.on("error", (error) => failTransport(`Codexへの書込みに失敗しました(${error.code ?? "unknown"})`));
|
|
50
|
+
const exited = new Promise((resolve) => child.once("close", (code, signal) => {
|
|
51
|
+
if (!stopped)
|
|
52
|
+
failTransport(`Codexの接続が終了しました(exit=${code}, signal=${signal})`);
|
|
53
|
+
resolve();
|
|
54
|
+
}));
|
|
55
|
+
const reader = createInterface({ input: child.stdout });
|
|
56
|
+
reader.on("line", (line) => {
|
|
57
|
+
let value;
|
|
58
|
+
try {
|
|
59
|
+
value = JSON.parse(line);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
failTransport("Codexが不正なJSON応答を返しました");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const item = pending.get(value?.id);
|
|
66
|
+
if (!item)
|
|
67
|
+
return;
|
|
68
|
+
clearTimeout(item.timer);
|
|
69
|
+
pending.delete(value.id);
|
|
70
|
+
if (value.error) {
|
|
71
|
+
item.reject(new CodexDeliveryError("CODEX_RECEIVER_REJECTED", typeof value.error.message === "string" ? value.error.message : "公式受信口が要求を拒否しました"));
|
|
72
|
+
}
|
|
73
|
+
else if ("result" in value)
|
|
74
|
+
item.resolve(value.result);
|
|
75
|
+
else
|
|
76
|
+
item.reject(new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "公式受信口の応答にresultがありません", item.method === "thread/queue/add"));
|
|
77
|
+
});
|
|
78
|
+
const request = (method, params) => new Promise((resolve, reject) => {
|
|
79
|
+
if (transportError) {
|
|
80
|
+
reject(new CodexDeliveryError("CODEX_RECEIVER_TRANSPORT_FAILED", transportError));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const id = ++sequence;
|
|
84
|
+
const timer = setTimeout(() => {
|
|
85
|
+
pending.delete(id);
|
|
86
|
+
reject(new CodexDeliveryError("CODEX_RECEIVER_TIMEOUT", `${method}の応答を確認できません`, method === "thread/queue/add"));
|
|
87
|
+
}, runtime.timeout_ms ?? 15_000);
|
|
88
|
+
pending.set(id, { resolve, reject, timer, method });
|
|
89
|
+
child.stdin.write(JSON.stringify({ id, method, params }) + "\n");
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
await request("initialize", { clientInfo: { name: "aiterm_parent_delivery", version: "1" }, capabilities: { experimentalApi: true } });
|
|
93
|
+
child.stdin.write(JSON.stringify({ method: "initialized" }) + "\n");
|
|
94
|
+
return await action(request);
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
stopped = true;
|
|
98
|
+
for (const item of pending.values())
|
|
99
|
+
clearTimeout(item.timer);
|
|
100
|
+
pending.clear();
|
|
101
|
+
child.stdin.end();
|
|
102
|
+
// stdio終了を公式processへ伝える。終了しない外部processだけを明示的に停止する。
|
|
103
|
+
const terminate = setTimeout(() => child.kill("SIGKILL"), 2_000);
|
|
104
|
+
await exited;
|
|
105
|
+
clearTimeout(terminate);
|
|
106
|
+
reader.close();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** 子へ送る前に、同じstoreの宛先と公式キューの対応を確認する。本文は保存・表示しない。 */
|
|
110
|
+
export async function verifyCodexParent(parent, runtime) {
|
|
111
|
+
await withCodexReceiver(parent, async (request) => {
|
|
112
|
+
const response = await request("thread/read", { threadId: parent.thread_id, includeTurns: false });
|
|
113
|
+
if (response?.thread?.id !== parent.thread_id) {
|
|
114
|
+
throw new CodexDeliveryError("CODEX_PARENT_UNAVAILABLE", "同じCodex環境で親threadを確認できません");
|
|
115
|
+
}
|
|
116
|
+
const subagent = response.thread.source?.subAgent;
|
|
117
|
+
if (subagent && typeof subagent === "object" && "thread_spawn" in subagent) {
|
|
118
|
+
throw new CodexDeliveryError("CODEX_PARENT_UNSUPPORTED", "Codexのnative sub-agentは外部processからのキュー入力を受け付けません");
|
|
119
|
+
}
|
|
120
|
+
await request("thread/queue/list", { threadId: parent.thread_id, limit: 1 });
|
|
121
|
+
}, runtime);
|
|
122
|
+
}
|
|
123
|
+
export async function submitCodexParentAnswer(parent, deliveryId, text, runtime) {
|
|
124
|
+
return withCodexReceiver(parent, async (request) => {
|
|
125
|
+
const result = await request("thread/queue/add", {
|
|
126
|
+
threadId: parent.thread_id,
|
|
127
|
+
input: [{ type: "text", text, text_elements: [] }],
|
|
128
|
+
clientUserMessageId: deliveryId,
|
|
129
|
+
});
|
|
130
|
+
if (typeof result?.queuedSubmission?.id !== "string") {
|
|
131
|
+
throw new CodexDeliveryError("CODEX_RECEIVER_INVALID_RESPONSE", "キューの受付IDを確認できません", true);
|
|
132
|
+
}
|
|
133
|
+
return { queued_submission_id: result.queuedSubmission.id };
|
|
134
|
+
}, runtime);
|
|
135
|
+
}
|
package/dist/core.js
CHANGED
|
@@ -2387,11 +2387,14 @@ async function settlePublishedClaudeCompletionMarker(meta, marker) {
|
|
|
2387
2387
|
/** agent harness の構造化 transcript から直近完了ターンの最終回答と相関情報を読む。 */
|
|
2388
2388
|
export async function readAgentTranscriptResult(name, o = {}) {
|
|
2389
2389
|
const meta = loadAgentMetadata(name);
|
|
2390
|
+
if (o.completion && (o.completion.outcome !== "done" || o.completion.launch_id !== meta.launch_id || o.completion.session_id !== name)) {
|
|
2391
|
+
throw new AitermError("回収対象の完了情報がagent launchと一致しません", 2);
|
|
2392
|
+
}
|
|
2390
2393
|
const operationId = o.operation_id == null ? null : validateOperationId(o.operation_id);
|
|
2391
2394
|
if (operationId && meta.kind !== "claude") {
|
|
2392
2395
|
throw new AitermError("operation_id付き回収はClaude agent sessionだけで使用できます", 2);
|
|
2393
2396
|
}
|
|
2394
|
-
if (meta.kind === "claude") {
|
|
2397
|
+
if (meta.kind === "claude" && !o.completion) {
|
|
2395
2398
|
let active = readClaudeOperationMarker(meta);
|
|
2396
2399
|
if (active)
|
|
2397
2400
|
active = await settlePublishedClaudeCompletionMarker(meta, active);
|
|
@@ -2407,10 +2410,13 @@ export async function readAgentTranscriptResult(name, o = {}) {
|
|
|
2407
2410
|
throw new AitermError(`agent session '${name}' はまだターンが完了していません。agent_done 完了後に再取得してください。${agentWaitGuide(name)}`, 2);
|
|
2408
2411
|
}
|
|
2409
2412
|
const done = latestAgentDoneEvent(meta, operationId);
|
|
2413
|
+
if (o.completion && meta.kind !== "codex" && (!done || done.turn_id !== o.completion.turn_id || done.operation_id !== o.completion.operation_id)) {
|
|
2414
|
+
throw new AitermError("回収対象の完了情報が置換されました。別の回答は配送しません", 2);
|
|
2415
|
+
}
|
|
2410
2416
|
if (operationId && !done) {
|
|
2411
2417
|
throw new AitermError(`operation ${operationId} はまだ完了していません。同じoperation_idで後から再取得してください。${agentWaitGuide(name)}`, 2);
|
|
2412
2418
|
}
|
|
2413
|
-
const turnId = done?.turn_id ?? null;
|
|
2419
|
+
const turnId = o.completion?.turn_id ?? done?.turn_id ?? null;
|
|
2414
2420
|
let text = "";
|
|
2415
2421
|
if (meta.kind === "claude") {
|
|
2416
2422
|
if (!done)
|
|
@@ -2421,7 +2427,7 @@ export async function readAgentTranscriptResult(name, o = {}) {
|
|
|
2421
2427
|
text = cursorTranscriptText(meta, readTranscriptLines, transcriptUnavailable);
|
|
2422
2428
|
}
|
|
2423
2429
|
else if (meta.kind === "codex") {
|
|
2424
|
-
text = codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable);
|
|
2430
|
+
text = codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable, o.completion !== undefined);
|
|
2425
2431
|
}
|
|
2426
2432
|
else {
|
|
2427
2433
|
if (!done)
|
|
@@ -2433,7 +2439,7 @@ export async function readAgentTranscriptResult(name, o = {}) {
|
|
|
2433
2439
|
if (o.lines != null)
|
|
2434
2440
|
text = text.split("\n").slice(-o.lines).join("\n");
|
|
2435
2441
|
const rawChars = text.length;
|
|
2436
|
-
const [body, outputMeta] = reduceOutput(text, name, true);
|
|
2442
|
+
const [body, outputMeta] = o.raw ? [text, ""] : reduceOutput(text, name, true);
|
|
2437
2443
|
const transcriptMeta = [
|
|
2438
2444
|
"agent_transcript",
|
|
2439
2445
|
`vendor=${meta.kind}`,
|
|
@@ -2568,6 +2574,10 @@ export function agentWaitLaunchForm(command) {
|
|
|
2568
2574
|
}
|
|
2569
2575
|
// dispatch / 起動時 prompt 送信後の共通案内。第一文で「待たない」を宣言し、待ち方は後段に置く。
|
|
2570
2576
|
export function agentDispatchGuide(session, cursor) {
|
|
2577
|
+
if (parentClientName === "codex-mcp-client") {
|
|
2578
|
+
return "回答本文はAitermがこのCodex親へ自動配送する。wait起動・ポーリング・通常の回答回収は不要。" +
|
|
2579
|
+
"親は作業を続けるかターンを終え、順番待ちから届く子の回答で続行する。";
|
|
2580
|
+
}
|
|
2571
2581
|
const cmd = `aiterm-wait --session ${session} --cursor ${cursor}`;
|
|
2572
2582
|
return (`投げっぱなしでよい=ここで待たない。親は自分の作業へ戻るか、このターンを終える。\n` +
|
|
2573
2583
|
`完了通知: ${agentWaitLaunchForm(cmd)}。exit が完了通知(${AITERM_WAIT_OUTCOME_NOTE})。\n` +
|
|
@@ -2575,6 +2585,8 @@ export function agentDispatchGuide(session, cursor) {
|
|
|
2575
2585
|
}
|
|
2576
2586
|
// 未完了 session へ触った時の共通案内。ここでも待つのは waiter プロセスであって親ではない。
|
|
2577
2587
|
export function agentWaitGuide(session) {
|
|
2588
|
+
if (parentClientName === "codex-mcp-client")
|
|
2589
|
+
return "回答本文はこのCodex親へ自動配送される。親は作業を続けるかターンを終える。";
|
|
2578
2590
|
const cmd = `aiterm-wait --session ${session ?? "<session_id>"} --cursor 0`;
|
|
2579
2591
|
return `完了通知は ${agentWaitLaunchForm(cmd)} で受ける(親はここで待たない・polling 不要)。receipt の outcome=done を確認してから再取得する。`;
|
|
2580
2592
|
}
|
|
@@ -2634,13 +2646,13 @@ export async function observeAgentDone(name, o = {}) {
|
|
|
2634
2646
|
}
|
|
2635
2647
|
const timeout = o.timeout ?? DEFAULT_AGENT_DONE_TIMEOUT;
|
|
2636
2648
|
if (meta.kind === "codex" && meta.completion_route === "codex_transcript") {
|
|
2637
|
-
return observeCodexDone(meta, timeout, o.cursor, detectAgentRateLimit);
|
|
2649
|
+
return observeCodexDone(meta, timeout, o.cursor, detectAgentRateLimit, o.signal);
|
|
2638
2650
|
}
|
|
2639
2651
|
if (meta.kind === "cursor" && meta.completion_route === "cursor_transcript") {
|
|
2640
|
-
return observeCursorDone(meta, timeout, o.cursor, detectAgentRateLimit);
|
|
2652
|
+
return observeCursorDone(meta, timeout, o.cursor, detectAgentRateLimit, o.signal);
|
|
2641
2653
|
}
|
|
2642
2654
|
if ((meta.kind === "grok" || meta.kind === "composer") && meta.completion_route === "grok_transcript") {
|
|
2643
|
-
return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit);
|
|
2655
|
+
return observeGrokDone(meta, timeout, o.cursor, detectAgentRateLimit, o.signal);
|
|
2644
2656
|
}
|
|
2645
2657
|
const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
|
|
2646
2658
|
// 境界の優先順: dispatch receipt の event_cursor(起動順序に依存しない)→ operation相関
|
|
@@ -2671,6 +2683,7 @@ export async function observeAgentDone(name, o = {}) {
|
|
|
2671
2683
|
error: apiError?.text ?? null,
|
|
2672
2684
|
});
|
|
2673
2685
|
for (;;) {
|
|
2686
|
+
o.signal?.throwIfAborted();
|
|
2674
2687
|
if (!fs.existsSync(metadataFile))
|
|
2675
2688
|
return observation("closed");
|
|
2676
2689
|
const size = safeStatSize(meta.event_file);
|
|
@@ -3103,6 +3116,9 @@ export async function sendInitialAgentPrompt(name, text, o = {}) {
|
|
|
3103
3116
|
const promptText = meta.kind === "cursor" ? cursorPromptWithLineage(meta, text) : text;
|
|
3104
3117
|
const startOffset = agentCompletionCursor(meta);
|
|
3105
3118
|
try {
|
|
3119
|
+
prepareSendText(promptText, { raw: false });
|
|
3120
|
+
await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
|
|
3121
|
+
harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: null });
|
|
3106
3122
|
if (meta.kind === "claude") {
|
|
3107
3123
|
prepareSendText(text, { raw: false });
|
|
3108
3124
|
reserveAnonymousClaudeTurn(meta);
|
|
@@ -3437,6 +3453,9 @@ export async function dispatchAgentTurn(name, text, o = {}) {
|
|
|
3437
3453
|
const dispatchText = meta.kind === "cursor" && !meta.vendor_session_id
|
|
3438
3454
|
? `${subagentInstruction(meta)}\n\n${text}`
|
|
3439
3455
|
: text;
|
|
3456
|
+
prepareSendText(dispatchText, { raw: o.raw });
|
|
3457
|
+
await o.before_send?.({ session_id: name, launch_id: meta.launch_id, vendor: meta.kind,
|
|
3458
|
+
harness: agentHarness(meta.kind), event_cursor: startOffset, operation_id: operationId });
|
|
3440
3459
|
if (meta.kind === "claude") {
|
|
3441
3460
|
// durable/anonymousを分岐する前に同じsend preflightを通す。拒否されるpromptの
|
|
3442
3461
|
// receipt/active markerだけを残して、来ないStopを待つ状態を作らない。
|
|
@@ -3517,7 +3536,7 @@ export async function steerAgentTurn(name, text) {
|
|
|
3517
3536
|
sendKey(name, "Enter");
|
|
3518
3537
|
return { ...receipt, delivery: "steered", pane_input_recovery: paneInputRecovery };
|
|
3519
3538
|
}
|
|
3520
|
-
export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, }) {
|
|
3539
|
+
export async function runClaudeOperation({ session_id: name, action, operation_id: operationIdInput, text, before_send, }) {
|
|
3521
3540
|
assertSessionName(name);
|
|
3522
3541
|
if (action !== "issue" && action !== "recover") {
|
|
3523
3542
|
throw new AitermError('action は "issue" または "recover" を指定してください', 2);
|
|
@@ -3532,7 +3551,7 @@ export async function runClaudeOperation({ session_id: name, action, operation_i
|
|
|
3532
3551
|
throw new AitermError("claude_turn issueには空でないtextが必要です", 2);
|
|
3533
3552
|
}
|
|
3534
3553
|
// v0.16.0: issue は dispatch-only。完了通知は aiterm-wait --operation、回収は recover が担う。
|
|
3535
|
-
dispatchReceipt = await dispatchAgentTurn(name, text, { operation_id: operationId });
|
|
3554
|
+
dispatchReceipt = await dispatchAgentTurn(name, text, { operation_id: operationId, before_send });
|
|
3536
3555
|
}
|
|
3537
3556
|
else {
|
|
3538
3557
|
if (text != null)
|
|
@@ -3873,7 +3892,7 @@ export function openAgent(kind, opts = {}) {
|
|
|
3873
3892
|
}
|
|
3874
3893
|
const driveHint = agentDone
|
|
3875
3894
|
? `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
|
|
3876
|
-
`turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch
|
|
3895
|
+
`turnはpty_send(${sid}, "...")で送る(自動で非ブロックdispatch。${parentClientName === "codex-mcp-client" ? "回答本文はCodex親へ自動配送する" : "完了通知はaiterm-wait"})。中断はpty_key(${sid}, "C-c")、` +
|
|
3877
3896
|
`Stopが来ない場合の解除はpty_close(${sid})を使う。`
|
|
3878
3897
|
: `TUI の描画には数秒かかる。少し置いてから pty_read(${sid}, screen:true) で画面を読み、` +
|
|
3879
3898
|
`pty_send(${sid}, "...") で入力・pty_key(${sid}, "Enter"/"Up"/"C-c" 等) で操作する(対話)。`;
|
|
@@ -3971,6 +3990,7 @@ export async function openAgentWithInitialPrompt(kind, opts = {}) {
|
|
|
3971
3990
|
const initial = await sendInitialAgentPrompt(sid, prompt, {
|
|
3972
3991
|
ready_timeout: opts.ready_timeout ?? undefined,
|
|
3973
3992
|
trust_project: opts.trust_project,
|
|
3993
|
+
before_send: opts.before_send,
|
|
3974
3994
|
});
|
|
3975
3995
|
return [sid, `${hint}\n${initial.text}`, initial.event_cursor, initial.submit_residue, initial.initial_prompt,
|
|
3976
3996
|
{ status: "ready", reason: "composer_ready" }];
|
package/dist/harnesses/codex.js
CHANGED
|
@@ -249,7 +249,7 @@ export function latestCodexCompletion(meta, readTranscriptLines) {
|
|
|
249
249
|
}
|
|
250
250
|
return latest;
|
|
251
251
|
}
|
|
252
|
-
export async function observeCodexDone(meta, timeout, requestedCursor, detectRateLimit) {
|
|
252
|
+
export async function observeCodexDone(meta, timeout, requestedCursor, detectRateLimit, signal) {
|
|
253
253
|
const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
|
|
254
254
|
let transcript = codexRootTranscript(meta);
|
|
255
255
|
const startOffset = requestedCursor ?? (transcript ? safeStatSize(transcript) : 0);
|
|
@@ -275,6 +275,7 @@ export async function observeCodexDone(meta, timeout, requestedCursor, detectRat
|
|
|
275
275
|
error: null,
|
|
276
276
|
});
|
|
277
277
|
for (;;) {
|
|
278
|
+
signal?.throwIfAborted();
|
|
278
279
|
if (!fs.existsSync(metadataFile))
|
|
279
280
|
return observation("closed");
|
|
280
281
|
transcript ??= codexRootTranscript(meta);
|
|
@@ -539,13 +540,42 @@ export function codexMoreReasoningChoice(screen) {
|
|
|
539
540
|
return null;
|
|
540
541
|
}
|
|
541
542
|
// 回収対象turnの最終assistantメッセージをroot rollout transcriptから抽出する。
|
|
542
|
-
export function codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable) {
|
|
543
|
+
export function codexTranscriptText(meta, turnId, readTranscriptLines, transcriptUnavailable, exactCompletion = false) {
|
|
543
544
|
if (!meta.codex_home || !meta.vendor_session_id)
|
|
544
545
|
transcriptUnavailable();
|
|
545
546
|
const transcript = findLatestCodexTranscript(meta.codex_home, meta.vendor_session_id);
|
|
546
547
|
if (!transcript)
|
|
547
548
|
transcriptUnavailable();
|
|
548
549
|
const lines = readTranscriptLines(transcript);
|
|
550
|
+
if (exactCompletion) {
|
|
551
|
+
// 同じturnの本文だけを使う。完了event内の本文と、turn ID付きoutput_textの両形式を扱う。
|
|
552
|
+
const matching = [];
|
|
553
|
+
for (const line of lines) {
|
|
554
|
+
let record;
|
|
555
|
+
try {
|
|
556
|
+
record = JSON.parse(line);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
const payload = record?.payload;
|
|
562
|
+
if (record?.type === "response_item" && payload?.type === "message" && payload?.role === "assistant"
|
|
563
|
+
&& payload?.internal_chat_message_metadata_passthrough?.turn_id === turnId && Array.isArray(payload?.content)) {
|
|
564
|
+
for (const item of payload.content) {
|
|
565
|
+
if (item?.type === "output_text" && typeof item.text === "string")
|
|
566
|
+
matching.push(item.text);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (record?.type === "event_msg" && payload?.type === "task_complete" && payload.turn_id === turnId) {
|
|
570
|
+
if (typeof payload.last_agent_message === "string")
|
|
571
|
+
return payload.last_agent_message;
|
|
572
|
+
if (matching.length > 0)
|
|
573
|
+
return matching.join("\n");
|
|
574
|
+
transcriptUnavailable();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
transcriptUnavailable();
|
|
578
|
+
}
|
|
549
579
|
const matching = [];
|
|
550
580
|
let finalAnswer = "";
|
|
551
581
|
for (const line of lines) {
|
package/dist/harnesses/cursor.js
CHANGED
|
@@ -183,7 +183,7 @@ export function latestCursorCompletion(meta, readTranscriptLines) {
|
|
|
183
183
|
? cursorCompletionEvent(meta, harnessSessionId, state.terminalRecord, `cursor:${state.userTurns}`)
|
|
184
184
|
: null;
|
|
185
185
|
}
|
|
186
|
-
export async function observeCursorDone(meta, timeout, requestedCursor, detectRateLimit) {
|
|
186
|
+
export async function observeCursorDone(meta, timeout, requestedCursor, detectRateLimit, signal) {
|
|
187
187
|
const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
|
|
188
188
|
let transcript = cursorTranscript(meta);
|
|
189
189
|
const startBoundary = requestedCursor ?? (transcript ? cursorTranscriptState(transcript).userTurns : 0);
|
|
@@ -205,6 +205,7 @@ export async function observeCursorDone(meta, timeout, requestedCursor, detectRa
|
|
|
205
205
|
error: null,
|
|
206
206
|
});
|
|
207
207
|
for (;;) {
|
|
208
|
+
signal?.throwIfAborted();
|
|
208
209
|
if (!fs.existsSync(metadataFile))
|
|
209
210
|
return observation("closed");
|
|
210
211
|
transcript ??= cursorTranscript(meta);
|
package/dist/harnesses/grok.js
CHANGED
|
@@ -146,7 +146,7 @@ export function grokInitializationComplete(meta) {
|
|
|
146
146
|
}
|
|
147
147
|
return false;
|
|
148
148
|
}
|
|
149
|
-
export async function observeGrokDone(meta, timeout, requestedCursor, detectRateLimit) {
|
|
149
|
+
export async function observeGrokDone(meta, timeout, requestedCursor, detectRateLimit, signal) {
|
|
150
150
|
const metadataFile = agentMetadataPath(meta.aiterm_session, meta.launch_id);
|
|
151
151
|
const transcript = grokEventsTranscript(meta);
|
|
152
152
|
if (!transcript)
|
|
@@ -174,6 +174,7 @@ export async function observeGrokDone(meta, timeout, requestedCursor, detectRate
|
|
|
174
174
|
error: ev?.done_status === "turn_error" ? ev.reason : null,
|
|
175
175
|
});
|
|
176
176
|
for (;;) {
|
|
177
|
+
signal?.throwIfAborted();
|
|
177
178
|
if (!fs.existsSync(metadataFile))
|
|
178
179
|
return observation("closed");
|
|
179
180
|
if (fs.existsSync(transcript)) {
|
package/dist/index.js
CHANGED
|
@@ -15,19 +15,31 @@ import { z } from "zod";
|
|
|
15
15
|
import * as core from "./core.js";
|
|
16
16
|
import { runtimeErrorStoreDiagnostic } from "./runtime-error-store.js";
|
|
17
17
|
import { createRequire } from "node:module";
|
|
18
|
+
import { ParentDeliveryManager } from "./parent-delivery.js";
|
|
19
|
+
import { codexParentFromRequest } from "./codex-parent-receiver.js";
|
|
18
20
|
// package.json の version を実行時に読み、MCP initialize で配るサーバ版と一致させる。
|
|
19
21
|
// createRequire を使うのは、import 属性 `with { type: "json" }` が Node 18.20+ 限定で
|
|
20
22
|
// engines "node >=18"(18.0〜18.19)を SyntaxError で壊し、旧 `assert` 構文は逆に Node 22 で
|
|
21
23
|
// 除去済み=どちらの静的構文も 18〜22 全域を満たせないため(実行時 require が唯一全域で動く)。
|
|
22
24
|
const pkg = createRequire(import.meta.url)("../package.json");
|
|
23
25
|
const server = new McpServer({ name: "aiterm", version: pkg.version });
|
|
26
|
+
let parentDelivery = null;
|
|
27
|
+
async function deliveryForRequest(extra) {
|
|
28
|
+
const parent = codexParentFromRequest(server.server.getClientVersion()?.name, extra._meta);
|
|
29
|
+
if (!parent)
|
|
30
|
+
return null;
|
|
31
|
+
parentDelivery ??= new ParentDeliveryManager();
|
|
32
|
+
await parentDelivery.prepare(parent);
|
|
33
|
+
return parentDelivery.request(parent);
|
|
34
|
+
}
|
|
24
35
|
/**
|
|
25
36
|
* dispatch 系の説明で共有する非ブロック規範。tool description は registerTool 時=initialize 前に
|
|
26
37
|
* 固定されるため親ホストを名指しできない(ホスト別の具体形は receipt 側が core.agentWaitLaunchForm で出す)。
|
|
27
38
|
* ここでは「待つな」を断定形で先に置き、foreground 実行の禁止までを説明の側に含める。
|
|
28
39
|
*/
|
|
29
40
|
const NON_BLOCKING_RULE = "dispatch した子は投げっぱなしでよい=親はここで待たない。" +
|
|
30
|
-
"
|
|
41
|
+
"Codex親にはAitermが回答本文を自動配送する。parent_deliveryがある場合はwait起動も通常の回答回収も不要。親は作業を続けるかターンを終える。" +
|
|
42
|
+
"その他の親では、完了通知をreceiptの `wait_process.executable` と `wait_process.args` をそのまま親のターンを塞がない別プロセスAPIへ渡して受け、" +
|
|
31
43
|
"PowerShell 7のStart-Processだけは `windows_start_process_argument_list` を単一文字列として渡す。" +
|
|
32
44
|
`exit を完了通知として扱う(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要)。` +
|
|
33
45
|
"`wait_command` は人間向け互換表示でありprocess境界へ使わない。foreground実行で親のターンを塞がない。";
|
|
@@ -38,6 +50,11 @@ const waitProcessOutputSchema = z
|
|
|
38
50
|
windows_start_process_argument_list: z.string().nullable(),
|
|
39
51
|
})
|
|
40
52
|
.nullable();
|
|
53
|
+
const parentDeliveryOutputSchema = z.object({
|
|
54
|
+
delivery_id: z.string(), state: z.enum(["waiting", "ready", "sending", "submitted", "failed", "unknown"]),
|
|
55
|
+
child_outcome: z.enum(["done", "closed", "rate_limited", "error"]).nullable(),
|
|
56
|
+
child_turn_id: z.string().nullable(), queued_submission_id: z.string().nullable(), error_code: z.string().nullable(),
|
|
57
|
+
});
|
|
41
58
|
function ok(s) {
|
|
42
59
|
return { content: [{ type: "text", text: s }] };
|
|
43
60
|
}
|
|
@@ -116,7 +133,7 @@ server.registerTool("pty_send", {
|
|
|
116
133
|
"agent session(launcher起動)への send は自動で dispatch になる: TUI の ready gate と submit 分離を通して即返り、" +
|
|
117
134
|
"receipt の event_cursor を返す。" +
|
|
118
135
|
NON_BLOCKING_RULE +
|
|
119
|
-
"
|
|
136
|
+
"自動配送以外の結果回収は pty_read(agent_transcript:true)、Claude の durable turn は claude_turn を使う。" +
|
|
120
137
|
"force:true は非Claude agent sessionへの手動介入用の素送信。aiterm相関付きClaudeの承認UIはclaude_approvalを使う。",
|
|
121
138
|
inputSchema: {
|
|
122
139
|
session_id: z.string(),
|
|
@@ -149,6 +166,7 @@ server.registerTool("pty_send", {
|
|
|
149
166
|
session_id: z.string(),
|
|
150
167
|
event_cursor: z.number().int().nullable(),
|
|
151
168
|
wait_process: waitProcessOutputSchema,
|
|
169
|
+
parent_delivery: parentDeliveryOutputSchema.optional(),
|
|
152
170
|
launch_id: z.string().nullable(),
|
|
153
171
|
vendor: z.enum(["claude", "codex", "grok", "composer", "cursor"]).nullable(),
|
|
154
172
|
harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]).nullable(),
|
|
@@ -158,7 +176,8 @@ server.registerTool("pty_send", {
|
|
|
158
176
|
// dispatch前に行った pane 入力の回復("fg" / "fg_stopped" / "stty_raw")。通常送信では省略(additive)。
|
|
159
177
|
pane_input_recovery: z.array(z.string()).optional(),
|
|
160
178
|
},
|
|
161
|
-
}, async ({ session_id, text, enter, mark, force, rtk, raw, image }) => {
|
|
179
|
+
}, async ({ session_id, text, enter, mark, force, rtk, raw, image }, extra) => {
|
|
180
|
+
let delivery = null;
|
|
162
181
|
try {
|
|
163
182
|
if (!force && core.isAgentSession(session_id)) {
|
|
164
183
|
if (enter === false)
|
|
@@ -167,8 +186,9 @@ server.registerTool("pty_send", {
|
|
|
167
186
|
throw new Error("agent session への dispatch は mark:true と併用できません");
|
|
168
187
|
if (rtk)
|
|
169
188
|
throw new Error("agent session への dispatch は rtk:true と併用できません");
|
|
170
|
-
|
|
171
|
-
const
|
|
189
|
+
delivery = await deliveryForRequest(extra);
|
|
190
|
+
const receipt = await core.dispatchAgentTurn(session_id, core.attachImages(text, image), { raw, before_send: delivery?.before_send });
|
|
191
|
+
const waitProcess = delivery ? null : core.agentWaitProcess(receipt.session_id, receipt.event_cursor);
|
|
172
192
|
return {
|
|
173
193
|
content: [
|
|
174
194
|
{
|
|
@@ -184,6 +204,7 @@ server.registerTool("pty_send", {
|
|
|
184
204
|
session_id: receipt.session_id,
|
|
185
205
|
event_cursor: receipt.event_cursor,
|
|
186
206
|
wait_process: waitProcess,
|
|
207
|
+
...(delivery?.result() ? { parent_delivery: delivery.result() } : {}),
|
|
187
208
|
launch_id: receipt.launch_id,
|
|
188
209
|
vendor: receipt.vendor,
|
|
189
210
|
harness: receipt.harness,
|
|
@@ -212,6 +233,7 @@ server.registerTool("pty_send", {
|
|
|
212
233
|
};
|
|
213
234
|
}
|
|
214
235
|
catch (e) {
|
|
236
|
+
delivery?.failed(e);
|
|
215
237
|
return fail(e);
|
|
216
238
|
}
|
|
217
239
|
});
|
|
@@ -385,6 +407,7 @@ server.registerTool("pty_close", {
|
|
|
385
407
|
},
|
|
386
408
|
}, async ({ session_id }) => {
|
|
387
409
|
try {
|
|
410
|
+
await parentDelivery?.beforeChange(session_id);
|
|
388
411
|
const result = core.closeSessionResult(session_id);
|
|
389
412
|
return {
|
|
390
413
|
content: [{ type: "text", text: `closed ${session_id}` }],
|
|
@@ -437,13 +460,15 @@ server.registerTool("pty_observe", {
|
|
|
437
460
|
pane_process: nativeProcessIdentitySchema.nullable(), harness_process: nativeProcessIdentitySchema.nullable(),
|
|
438
461
|
process_identity: nativeProcessIdentitySchema.nullable(),
|
|
439
462
|
token_hint: z.number().nullable(),
|
|
463
|
+
parent_deliveries: z.array(parentDeliveryOutputSchema).optional(),
|
|
440
464
|
activity: z.object({ cursor: z.string().nullable(), output_changed: z.boolean().nullable(),
|
|
441
465
|
cpu_seconds: z.number().nullable(), cpu_delta_seconds: z.number().nullable(), cpu_delta_complete: z.boolean().nullable(),
|
|
442
466
|
background_cpu_seconds: z.number().nullable(), background_cpu_delta_seconds: z.number().nullable(), background_cpu_delta_complete: z.boolean().nullable() }),
|
|
443
467
|
},
|
|
444
468
|
}, async ({ session_id, cursor }) => {
|
|
445
469
|
try {
|
|
446
|
-
const
|
|
470
|
+
const observation = core.observeSession(session_id, cursor);
|
|
471
|
+
const result = { ...observation, ...(parentDelivery ? { parent_deliveries: parentDelivery.status(session_id) } : {}) };
|
|
447
472
|
return { ...ok(JSON.stringify(result)), structuredContent: { ...result } };
|
|
448
473
|
}
|
|
449
474
|
catch (e) {
|
|
@@ -493,21 +518,27 @@ server.registerTool("claude_turn", {
|
|
|
493
518
|
// issue時のみdispatch由来のsubmit座礁観測(additive)。true=composerに残存を確認(submit未成立の疑い)/
|
|
494
519
|
// false=残存を観測せず(成立の保証ではない)/ recover等はnull。
|
|
495
520
|
submit_residue: z.boolean().nullable(),
|
|
521
|
+
parent_delivery: parentDeliveryOutputSchema.optional(),
|
|
496
522
|
},
|
|
497
|
-
}, async ({ action, session_id, operation_id, text }) => {
|
|
523
|
+
}, async ({ action, session_id, operation_id, text }, extra) => {
|
|
524
|
+
let delivery = null;
|
|
498
525
|
try {
|
|
526
|
+
if (action === "issue")
|
|
527
|
+
delivery = await deliveryForRequest(extra);
|
|
499
528
|
const result = await core.runClaudeOperation({
|
|
500
529
|
action,
|
|
501
530
|
session_id,
|
|
502
531
|
operation_id,
|
|
503
532
|
text: text ?? undefined,
|
|
533
|
+
before_send: delivery?.before_send,
|
|
504
534
|
});
|
|
505
535
|
return {
|
|
506
536
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
507
|
-
structuredContent: { ...result },
|
|
537
|
+
structuredContent: { ...result, ...(delivery?.result() ? { parent_delivery: delivery.result() } : {}) },
|
|
508
538
|
};
|
|
509
539
|
}
|
|
510
540
|
catch (e) {
|
|
541
|
+
delivery?.failed(e);
|
|
511
542
|
return fail(e);
|
|
512
543
|
}
|
|
513
544
|
});
|
|
@@ -613,12 +644,7 @@ const agentEffortDesc = (kind) => kind === "claude"
|
|
|
613
644
|
: "Grok Build reasoning effort。利用可能値はCLI/modelのlive catalogに従う。省略時はCLI/model既定。";
|
|
614
645
|
// 全launcher共通の完了受信ガイド。machine callerはreceiptのwait_processをそのまま別process APIへ渡す。
|
|
615
646
|
// wait_commandは人間向け互換表示。文型は NON_BLOCKING_RULE と同じく「待たない」が先。
|
|
616
|
-
const agentCompletionDesc =
|
|
617
|
-
`完了通知は起動応答またはpty_send dispatch receiptの wait_processを、親のターンを塞がない` +
|
|
618
|
-
`別プロセスAPIへexecutable/argsの境界を保ったまま渡して受ける` +
|
|
619
|
-
`(PowerShell 7のStart-Processはwindows_start_process_argument_listを使う)` +
|
|
620
|
-
`(${core.AITERM_WAIT_OUTCOME_NOTE}。ポーリング不要・foreground実行はしない)。` +
|
|
621
|
-
`wait_commandは人間向け互換表示。結果回収は pty_read(agent_transcript:true)。`;
|
|
647
|
+
const agentCompletionDesc = NON_BLOCKING_RULE + "自動配送以外の結果回収は pty_read(agent_transcript:true)。";
|
|
622
648
|
const agentEnvironmentDesc = `通常CLIと同じHOME・cwd・project/user/local設定・MCP・plugin・skill・permission/trustを共有する。` +
|
|
623
649
|
`aitermは完了相関stateだけをlaunch単位で所有する。起動されたagentにはsub-agent自己認識、親session、` +
|
|
624
650
|
`delegation depth/lineage、delegation_allowed=trueを注入し、必要な追加委譲は許可する。`;
|
|
@@ -627,14 +653,17 @@ const initialPromptDeliverySchema = z.object({
|
|
|
627
653
|
reason: z.string(), turn_started: z.boolean().nullable(),
|
|
628
654
|
});
|
|
629
655
|
const agentStartupSchema = z.object({ status: z.enum(["ready", "not_checked", "blocked"]), reason: z.string() });
|
|
630
|
-
async function launchAgent(kind, args) {
|
|
656
|
+
async function launchAgent(kind, args, extra) {
|
|
631
657
|
const supportsWriteScope = kind !== "claude";
|
|
632
658
|
const { prompt, image, throughline_source_session, throughline_supplement_file, model, reasoning_effort, env_vars, cwd, session_name, launch_operation_id, write_scope, trust_project } = args;
|
|
659
|
+
let delivery = null;
|
|
633
660
|
try {
|
|
634
661
|
if (!supportsWriteScope && write_scope !== undefined) {
|
|
635
662
|
throw new core.AitermError("claude-code harnessはwrite_scopeに対応していません。指定を外してください", 2);
|
|
636
663
|
}
|
|
637
664
|
const initialPrompt = image && image.length > 0 ? core.attachImages(prompt ?? "", image) : prompt ?? undefined;
|
|
665
|
+
if (initialPrompt || throughline_source_session)
|
|
666
|
+
delivery = await deliveryForRequest(extra);
|
|
638
667
|
const [sid, hint, eventCursor, submitResidue, initialDelivery, startup] = await core.openAgentWithInitialPrompt(kind, {
|
|
639
668
|
prompt: initialPrompt,
|
|
640
669
|
throughline_source_session,
|
|
@@ -643,6 +672,7 @@ async function launchAgent(kind, args) {
|
|
|
643
672
|
reasoning_effort: reasoning_effort ?? undefined,
|
|
644
673
|
env_vars,
|
|
645
674
|
trust_project,
|
|
675
|
+
before_send: delivery?.before_send,
|
|
646
676
|
cwd: cwd ?? undefined,
|
|
647
677
|
session_name: session_name ?? undefined,
|
|
648
678
|
launch_operation_id: launch_operation_id ?? undefined,
|
|
@@ -655,8 +685,9 @@ async function launchAgent(kind, args) {
|
|
|
655
685
|
session_id: sid,
|
|
656
686
|
managed_completion: true,
|
|
657
687
|
event_cursor: eventCursor,
|
|
658
|
-
wait_process: eventCursor === null ? null : core.agentWaitProcess(sid, eventCursor),
|
|
659
|
-
wait_command: eventCursor === null ? null : `aiterm-wait --session ${sid} --cursor ${eventCursor}`,
|
|
688
|
+
wait_process: eventCursor === null || delivery ? null : core.agentWaitProcess(sid, eventCursor),
|
|
689
|
+
wait_command: eventCursor === null || delivery ? null : `aiterm-wait --session ${sid} --cursor ${eventCursor}`,
|
|
690
|
+
...(delivery?.result() ? { parent_delivery: delivery.result() } : {}),
|
|
660
691
|
submit_residue: submitResidue,
|
|
661
692
|
initial_prompt: initialDelivery,
|
|
662
693
|
startup,
|
|
@@ -676,14 +707,16 @@ async function launchAgent(kind, args) {
|
|
|
676
707
|
};
|
|
677
708
|
}
|
|
678
709
|
catch (e) {
|
|
710
|
+
delivery?.failed(e);
|
|
679
711
|
if (e instanceof core.AgentLaunchPromptError) {
|
|
680
712
|
return {
|
|
681
713
|
...fail(e),
|
|
682
714
|
structuredContent: {
|
|
683
715
|
schema: "aiterm.agent-launch-result.v1", provider: kind, harness: core.agentHarness(kind),
|
|
684
716
|
session_id: e.session_id, managed_completion: true, event_cursor: e.event_cursor,
|
|
685
|
-
wait_process: e.event_cursor === null ? null : core.agentWaitProcess(e.session_id, e.event_cursor),
|
|
686
|
-
wait_command: e.event_cursor === null ? null : `aiterm-wait --session ${e.session_id} --cursor ${e.event_cursor}`,
|
|
717
|
+
wait_process: e.event_cursor === null || delivery ? null : core.agentWaitProcess(e.session_id, e.event_cursor),
|
|
718
|
+
wait_command: e.event_cursor === null || delivery ? null : `aiterm-wait --session ${e.session_id} --cursor ${e.event_cursor}`,
|
|
719
|
+
...(delivery?.result() ? { parent_delivery: delivery.result() } : {}),
|
|
687
720
|
submit_residue: null, initial_prompt: e.initial_prompt, startup: e.startup,
|
|
688
721
|
},
|
|
689
722
|
};
|
|
@@ -750,12 +783,13 @@ function registerAgentTool(toolName, kind, desc) {
|
|
|
750
783
|
event_cursor: z.number().int().nullable(),
|
|
751
784
|
wait_process: waitProcessOutputSchema,
|
|
752
785
|
wait_command: z.string().nullable(),
|
|
786
|
+
parent_delivery: parentDeliveryOutputSchema.optional(),
|
|
753
787
|
// 初回prompt dispatch後のsubmit座礁観測(additive)。true=composerに残存を確認(submit未成立の疑い)/
|
|
754
788
|
// false=残存を観測せず(submit成立の保証ではない)/ null=promptなし・判定不能。
|
|
755
789
|
submit_residue: z.boolean().nullable(),
|
|
756
790
|
...writeScopeOutputSchema,
|
|
757
791
|
},
|
|
758
|
-
}, async (args) => launchAgent(kind, args));
|
|
792
|
+
}, async (args, extra) => launchAgent(kind, args, extra));
|
|
759
793
|
}
|
|
760
794
|
server.registerTool("agent_launch", {
|
|
761
795
|
description: "エージェントを単一の標準入口から永続sessionへ起動する。harnessはagent loop・認証・hook・transcriptを所有する実行基盤、" +
|
|
@@ -788,11 +822,12 @@ server.registerTool("agent_launch", {
|
|
|
788
822
|
event_cursor: z.number().int().nullable(),
|
|
789
823
|
wait_process: waitProcessOutputSchema,
|
|
790
824
|
wait_command: z.string().nullable(),
|
|
825
|
+
parent_delivery: parentDeliveryOutputSchema.optional(),
|
|
791
826
|
submit_residue: z.boolean().nullable(),
|
|
792
827
|
write_scope: z.string().optional(),
|
|
793
828
|
write_scope_enforcement: z.enum(["enforced_read_only", "declaration_only_unsupported"]).optional(),
|
|
794
829
|
},
|
|
795
|
-
}, async ({ harness, ...args }) => launchAgent(kindForHarness(harness), args));
|
|
830
|
+
}, async ({ harness, ...args }, extra) => launchAgent(kindForHarness(harness), args, extra));
|
|
796
831
|
registerAgentTool("claude_agent", "claude", "【旧互換alias。新規連携は agent_launch(harness=claude-code)】Claude Codeの対話エージェントTUIを永続端末に起動する。`claude -p`ではなく、" +
|
|
797
832
|
"同じ利用者可視sessionへpty_sendで継続入力する。" +
|
|
798
833
|
agentEnvironmentDesc +
|
|
@@ -824,7 +859,13 @@ async function main() {
|
|
|
824
859
|
// そのホストの実際の起動形で名指しする(実測: claude-code は initialize → notifications/initialized
|
|
825
860
|
// → tools/list の順で送るため、どの tool 呼び出しより先に確定する)。取れない時は汎用文へ落ちるだけ。
|
|
826
861
|
server.server.oninitialized = () => {
|
|
827
|
-
|
|
862
|
+
const name = server.server.getClientVersion()?.name;
|
|
863
|
+
core.setParentClient(name ?? null);
|
|
864
|
+
if (name === "codex-mcp-client")
|
|
865
|
+
parentDelivery ??= new ParentDeliveryManager();
|
|
866
|
+
};
|
|
867
|
+
server.server.onclose = () => {
|
|
868
|
+
void parentDelivery?.close().catch(() => process.stderr.write("aiterm: PARENT_DELIVERY_CLOSE_FAILED\n"));
|
|
828
869
|
};
|
|
829
870
|
const transport = new StdioServerTransport();
|
|
830
871
|
await server.connect(transport);
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
// Aitermが所有する、子の完了観測・回答本文の保存・Codex親への配送。
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { randomUUID, createHash } from "node:crypto";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { observeAgentDone, readAgentTranscriptResult } from "./core.js";
|
|
7
|
+
import { ensureStateRoot, writeJson0600 } from "./agent-shared.js";
|
|
8
|
+
import { readRuntimeProcesses } from "./process-runtime.js";
|
|
9
|
+
import { CodexDeliveryError, submitCodexParentAnswer, verifyCodexParent } from "./codex-parent-receiver.js";
|
|
10
|
+
import { AitermError } from "./errors.js";
|
|
11
|
+
const recordSchema = z.object({
|
|
12
|
+
schema: z.literal("aiterm.parent-delivery.v1"),
|
|
13
|
+
delivery_id: z.uuid(),
|
|
14
|
+
created_at: z.string(),
|
|
15
|
+
updated_at: z.string(),
|
|
16
|
+
parent: z.object({ thread_id: z.uuid(), codex_home: z.string() }).strict(),
|
|
17
|
+
boundary: z.object({
|
|
18
|
+
session_id: z.string().regex(/^[A-Za-z0-9_-]{1,64}$/),
|
|
19
|
+
launch_id: z.string().regex(/^[0-9a-f]{32}$/),
|
|
20
|
+
vendor: z.enum(["claude", "codex", "grok", "composer", "cursor"]),
|
|
21
|
+
harness: z.enum(["claude-code", "codex-cli", "grok-cli", "cursor-cli"]),
|
|
22
|
+
event_cursor: z.number().int().nonnegative(), operation_id: z.string().nullable(),
|
|
23
|
+
}).strict(),
|
|
24
|
+
state: z.enum(["waiting", "ready", "sending", "submitted", "failed", "unknown"]),
|
|
25
|
+
text: z.string().nullable(),
|
|
26
|
+
child_outcome: z.enum(["done", "closed", "rate_limited", "error"]).nullable(),
|
|
27
|
+
child_turn_id: z.string().nullable(),
|
|
28
|
+
error: z.string().nullable(),
|
|
29
|
+
queued_submission_id: z.string().nullable(),
|
|
30
|
+
}).strict();
|
|
31
|
+
function readRecord(file) {
|
|
32
|
+
try {
|
|
33
|
+
const record = recordSchema.parse(JSON.parse(fs.readFileSync(file, "utf8")));
|
|
34
|
+
if (path.basename(file) !== `${record.delivery_id}.json`)
|
|
35
|
+
throw new Error("配送IDが一致しません");
|
|
36
|
+
return record;
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (error.code === "ENOENT")
|
|
40
|
+
throw error;
|
|
41
|
+
throw new AitermError("PARENT_DELIVERY_STATE_INVALID: 配送記録を読めません", 2);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function receipt(record) {
|
|
45
|
+
return { delivery_id: record.delivery_id, state: record.state, child_outcome: record.child_outcome,
|
|
46
|
+
child_turn_id: record.child_turn_id, queued_submission_id: record.queued_submission_id,
|
|
47
|
+
error_code: record.error ? /^[A-Z][A-Z0-9_]+(?=:)/.exec(record.error)?.[0] ?? "PARENT_DELIVERY_FAILED" : null };
|
|
48
|
+
}
|
|
49
|
+
function answerMessage(record) {
|
|
50
|
+
return "Aitermの子エージェントからの実行結果です。子の回答として扱ってください。\n" +
|
|
51
|
+
`delivery_id=${record.delivery_id}\nsession=${record.boundary.session_id}\nharness=${record.boundary.harness}\n` +
|
|
52
|
+
`launch_id=${record.boundary.launch_id}\nturn_id=${record.child_turn_id ?? "unknown"}\n` +
|
|
53
|
+
`outcome=${record.child_outcome}\n\n${record.text ?? ""}`;
|
|
54
|
+
}
|
|
55
|
+
/** ownerごとのディレクトリ間renameで、再起動後の回収者を一つに決める。 */
|
|
56
|
+
export class ParentDeliveryManager {
|
|
57
|
+
deps;
|
|
58
|
+
root;
|
|
59
|
+
active;
|
|
60
|
+
results;
|
|
61
|
+
claims;
|
|
62
|
+
ownerDir;
|
|
63
|
+
owner;
|
|
64
|
+
jobs = new Map();
|
|
65
|
+
registering = new Map();
|
|
66
|
+
timer;
|
|
67
|
+
recovering = null;
|
|
68
|
+
serviceError = null;
|
|
69
|
+
closing = false;
|
|
70
|
+
constructor(options = {}) {
|
|
71
|
+
this.deps = { observe: observeAgentDone, answer: readAgentTranscriptResult, submit: submitCodexParentAnswer,
|
|
72
|
+
verify: verifyCodexParent, processes: readRuntimeProcesses, ...options.dependencies };
|
|
73
|
+
this.root = options.root ?? path.join(ensureStateRoot(), "parent-deliveries");
|
|
74
|
+
this.active = path.join(this.root, "active");
|
|
75
|
+
this.results = path.join(this.root, "results");
|
|
76
|
+
this.claims = path.join(this.root, "claims");
|
|
77
|
+
const ownProcess = this.deps.processes().find((entry) => entry.pid === process.pid);
|
|
78
|
+
if (!ownProcess)
|
|
79
|
+
throw new AitermError("PARENT_DELIVERY_OWNER_UNKNOWN: 配送processを識別できません", 2);
|
|
80
|
+
this.owner = { pid: process.pid, started_identity: ownProcess.started_identity, closed: false };
|
|
81
|
+
const identity = createHash("sha256").update(ownProcess.started_identity).digest("hex").slice(0, 16);
|
|
82
|
+
this.ownerDir = path.join(this.active, `${process.pid}-${identity}-${randomUUID()}`);
|
|
83
|
+
fs.mkdirSync(this.ownerDir, { recursive: true, mode: 0o700 });
|
|
84
|
+
fs.mkdirSync(this.results, { recursive: true, mode: 0o700 });
|
|
85
|
+
fs.mkdirSync(this.claims, { recursive: true, mode: 0o700 });
|
|
86
|
+
writeJson0600(path.join(this.ownerDir, "owner.json"), this.owner);
|
|
87
|
+
// MCPの再接続と旧process終了の順序が前後しても、終了したownerの記録を回収する。
|
|
88
|
+
this.timer = setInterval(() => { void this.recover().catch((error) => this.reportServiceError(error)); }, 5_000);
|
|
89
|
+
this.timer.unref();
|
|
90
|
+
void this.recover().catch((error) => this.reportServiceError(error));
|
|
91
|
+
}
|
|
92
|
+
reportServiceError(error) {
|
|
93
|
+
this.serviceError = error instanceof Error ? error : new Error(String(error));
|
|
94
|
+
process.stderr.write("aiterm: PARENT_DELIVERY_STATE_UNAVAILABLE(配送状態を確認してください)\n");
|
|
95
|
+
}
|
|
96
|
+
async prepare(parent) {
|
|
97
|
+
if (this.closing)
|
|
98
|
+
throw new AitermError("PARENT_DELIVERY_CLOSED: MCP接続を終了しています", 2);
|
|
99
|
+
if (this.serviceError)
|
|
100
|
+
throw this.serviceError;
|
|
101
|
+
await this.recover();
|
|
102
|
+
await this.deps.verify(parent);
|
|
103
|
+
}
|
|
104
|
+
request(parent) {
|
|
105
|
+
let job = null;
|
|
106
|
+
return {
|
|
107
|
+
before_send: async (boundary) => {
|
|
108
|
+
const previous = this.registering.get(boundary.session_id) ?? Promise.resolve();
|
|
109
|
+
const registration = previous.then(async () => {
|
|
110
|
+
await this.beforeChange(boundary.session_id, true);
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
const record = { schema: "aiterm.parent-delivery.v1", delivery_id: randomUUID(),
|
|
113
|
+
created_at: now, updated_at: now, parent, boundary, state: "waiting", text: null,
|
|
114
|
+
child_outcome: null, child_turn_id: null, error: null, queued_submission_id: null };
|
|
115
|
+
job = { record, file: path.join(this.ownerDir, `${record.delivery_id}.json`), controller: new AbortController() };
|
|
116
|
+
this.save(job);
|
|
117
|
+
// 記録を調べてから作るだけでは、別processが同時に同じ子を予約できる。
|
|
118
|
+
// 同一filesystemのhard link作成で、回答をまだ保存していない依頼を一つに決める。
|
|
119
|
+
try {
|
|
120
|
+
fs.linkSync(job.file, path.join(this.claims, `${boundary.session_id}.json`));
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
fs.unlinkSync(job.file);
|
|
124
|
+
job = null;
|
|
125
|
+
if (error.code === "EEXIST") {
|
|
126
|
+
throw new AitermError("PARENT_RESULT_PENDING: 同じ子の回答保存を別の依頼が所有しています", 2);
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
this.jobs.set(record.delivery_id, job);
|
|
131
|
+
this.watch(job);
|
|
132
|
+
});
|
|
133
|
+
this.registering.set(boundary.session_id, registration);
|
|
134
|
+
try {
|
|
135
|
+
await registration;
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
if (this.registering.get(boundary.session_id) === registration)
|
|
139
|
+
this.registering.delete(boundary.session_id);
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
result: () => job ? receipt(job.record) : null,
|
|
143
|
+
failed: (error) => {
|
|
144
|
+
if (!job || job.record.state !== "waiting")
|
|
145
|
+
return;
|
|
146
|
+
job.controller.abort();
|
|
147
|
+
job.record.state = "unknown";
|
|
148
|
+
job.record.error = `AGENT_DISPATCH_UNCONFIRMED: ${error instanceof Error ? error.message : String(error)}`;
|
|
149
|
+
this.finish(job);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
save(job) {
|
|
154
|
+
job.record.updated_at = new Date().toISOString();
|
|
155
|
+
writeJson0600(job.file, job.record);
|
|
156
|
+
}
|
|
157
|
+
finish(job) {
|
|
158
|
+
this.releaseClaim(job);
|
|
159
|
+
this.save(job);
|
|
160
|
+
const destination = path.join(this.results, `${job.record.delivery_id}.json`);
|
|
161
|
+
if (job.file !== destination) {
|
|
162
|
+
fs.renameSync(job.file, destination);
|
|
163
|
+
job.file = destination;
|
|
164
|
+
}
|
|
165
|
+
this.jobs.delete(job.record.delivery_id);
|
|
166
|
+
}
|
|
167
|
+
releaseClaim(job) {
|
|
168
|
+
const file = path.join(this.claims, `${job.record.boundary.session_id}.json`);
|
|
169
|
+
try {
|
|
170
|
+
const claim = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
171
|
+
// 回収済みrecordを復旧している間に始まった、次の依頼の予約は触らない。
|
|
172
|
+
if (claim.delivery_id === job.record.delivery_id)
|
|
173
|
+
fs.unlinkSync(file);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
if (error.code !== "ENOENT")
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
watch(job) {
|
|
181
|
+
const boundary = job.record.boundary;
|
|
182
|
+
job.task = (async () => {
|
|
183
|
+
try {
|
|
184
|
+
const completion = await this.deps.observe(boundary.session_id, {
|
|
185
|
+
cursor: boundary.event_cursor, operation_id: boundary.operation_id,
|
|
186
|
+
timeout: Infinity, signal: job.controller.signal,
|
|
187
|
+
});
|
|
188
|
+
await this.capture(job, completion);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (job.controller.signal.aborted)
|
|
192
|
+
return;
|
|
193
|
+
await this.captureFailure(job, error);
|
|
194
|
+
}
|
|
195
|
+
})().catch((error) => this.reportServiceError(error));
|
|
196
|
+
}
|
|
197
|
+
async capture(job, completion) {
|
|
198
|
+
if (job.capture)
|
|
199
|
+
return job.capture;
|
|
200
|
+
if (job.record.state !== "waiting")
|
|
201
|
+
return;
|
|
202
|
+
job.capture = (async () => {
|
|
203
|
+
if (completion.launch_id !== job.record.boundary.launch_id)
|
|
204
|
+
throw new AitermError("PARENT_DELIVERY_LAUNCH_CHANGED: 子のlaunchが置換されました", 2);
|
|
205
|
+
if (completion.outcome === "running" || completion.outcome === "timeout")
|
|
206
|
+
throw new AitermError("PARENT_DELIVERY_NOT_COMPLETED: 子の完了を確認できません", 2);
|
|
207
|
+
const record = job.record;
|
|
208
|
+
record.child_outcome = completion.outcome;
|
|
209
|
+
record.child_turn_id = completion.turn_id;
|
|
210
|
+
if (completion.outcome === "done") {
|
|
211
|
+
const answer = await this.deps.answer(record.boundary.session_id, { completion, operation_id: completion.operation_id, raw: true });
|
|
212
|
+
if (record.state !== "waiting")
|
|
213
|
+
return;
|
|
214
|
+
record.text = answer.text;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
record.text = completion.error ?? completion.rate_limit ?? "子のセッションが閉じられたため、回答はありません。";
|
|
218
|
+
}
|
|
219
|
+
record.state = "ready";
|
|
220
|
+
this.save(job);
|
|
221
|
+
// 次の依頼へ進める時点は本文保存まで。親のキュー受付を待たせない。
|
|
222
|
+
job.delivery = this.deliver(job).catch((error) => this.reportServiceError(error));
|
|
223
|
+
})();
|
|
224
|
+
return job.capture;
|
|
225
|
+
}
|
|
226
|
+
async captureFailure(job, error) {
|
|
227
|
+
if (job.record.state !== "waiting")
|
|
228
|
+
throw error;
|
|
229
|
+
job.record.child_outcome = "error";
|
|
230
|
+
job.record.error = `AGENT_RESULT_UNAVAILABLE: ${error instanceof Error ? error.message : String(error)}`;
|
|
231
|
+
job.record.text = job.record.error;
|
|
232
|
+
job.record.state = "ready";
|
|
233
|
+
this.save(job);
|
|
234
|
+
await this.deliver(job);
|
|
235
|
+
}
|
|
236
|
+
async deliver(job) {
|
|
237
|
+
this.releaseClaim(job);
|
|
238
|
+
job.record.state = "sending";
|
|
239
|
+
this.save(job);
|
|
240
|
+
try {
|
|
241
|
+
const result = await this.deps.submit(job.record.parent, job.record.delivery_id, answerMessage(job.record));
|
|
242
|
+
job.record.queued_submission_id = result.queued_submission_id;
|
|
243
|
+
job.record.state = "submitted";
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
job.record.state = error instanceof CodexDeliveryError && !error.outcome_unknown ? "failed" : "unknown";
|
|
247
|
+
job.record.error = error instanceof Error ? error.message : String(error);
|
|
248
|
+
}
|
|
249
|
+
this.finish(job);
|
|
250
|
+
}
|
|
251
|
+
/** 次の入力・closeより先に、既に完了した前の本文を確保する。 */
|
|
252
|
+
async beforeChange(sessionId, requireCaptured = false) {
|
|
253
|
+
for (const job of this.jobs.values()) {
|
|
254
|
+
if (job.record.boundary.session_id !== sessionId || job.record.state !== "waiting")
|
|
255
|
+
continue;
|
|
256
|
+
const boundary = job.record.boundary;
|
|
257
|
+
const completion = await this.deps.observe(sessionId, { cursor: boundary.event_cursor, operation_id: boundary.operation_id, timeout: 0 });
|
|
258
|
+
if (completion.outcome !== "running" && completion.outcome !== "timeout") {
|
|
259
|
+
await this.capture(job, completion);
|
|
260
|
+
}
|
|
261
|
+
if (requireCaptured && job.record.state === "waiting") {
|
|
262
|
+
throw new AitermError("PARENT_RESULT_PENDING: 前の依頼の完了と回答保存が済んでいません", 2);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// 別のMCP processが同じ子の回答を保存中なら、入力で記録を上書きしない。
|
|
266
|
+
if (requireCaptured && this.records().some((record) => record.boundary.session_id === sessionId && record.state === "waiting")) {
|
|
267
|
+
throw new AitermError("PARENT_RESULT_PENDING: 別の依頼の回答保存が済んでいません", 2);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
files() {
|
|
271
|
+
const files = fs.readdirSync(this.results).filter((name) => name.endsWith(".json")).map((name) => path.join(this.results, name));
|
|
272
|
+
for (const owner of fs.readdirSync(this.active, { withFileTypes: true })) {
|
|
273
|
+
if (!owner.isDirectory())
|
|
274
|
+
continue;
|
|
275
|
+
const directory = path.join(this.active, owner.name);
|
|
276
|
+
files.push(...fs.readdirSync(directory).filter((name) => name !== "owner.json" && name.endsWith(".json")).map((name) => path.join(directory, name)));
|
|
277
|
+
}
|
|
278
|
+
return files;
|
|
279
|
+
}
|
|
280
|
+
records() {
|
|
281
|
+
const records = [];
|
|
282
|
+
for (const file of this.files()) {
|
|
283
|
+
try {
|
|
284
|
+
records.push(readRecord(file));
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
if (error.code !== "ENOENT")
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return records;
|
|
292
|
+
}
|
|
293
|
+
status(sessionId) {
|
|
294
|
+
if (this.serviceError)
|
|
295
|
+
throw this.serviceError;
|
|
296
|
+
return this.records().filter((record) => record.boundary.session_id === sessionId)
|
|
297
|
+
.sort((a, b) => a.created_at.localeCompare(b.created_at)).map(receipt);
|
|
298
|
+
}
|
|
299
|
+
recover() {
|
|
300
|
+
if (this.recovering)
|
|
301
|
+
return this.recovering;
|
|
302
|
+
if (this.closing)
|
|
303
|
+
return Promise.resolve();
|
|
304
|
+
this.recovering = this.recoverOrphans().finally(() => { this.recovering = null; });
|
|
305
|
+
return this.recovering;
|
|
306
|
+
}
|
|
307
|
+
async recoverOrphans() {
|
|
308
|
+
const processes = this.deps.processes();
|
|
309
|
+
for (const directory of fs.readdirSync(this.active, { withFileTypes: true })) {
|
|
310
|
+
if (!directory.isDirectory())
|
|
311
|
+
continue;
|
|
312
|
+
const oldDir = path.join(this.active, directory.name);
|
|
313
|
+
if (oldDir === this.ownerDir)
|
|
314
|
+
continue;
|
|
315
|
+
let owner;
|
|
316
|
+
try {
|
|
317
|
+
owner = JSON.parse(fs.readFileSync(path.join(oldDir, "owner.json"), "utf8"));
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
if (error.code === "ENOENT")
|
|
321
|
+
continue;
|
|
322
|
+
throw new AitermError("PARENT_DELIVERY_OWNER_INVALID: 配送所有者の記録を読めません", 2);
|
|
323
|
+
}
|
|
324
|
+
if (!Number.isSafeInteger(owner.pid) || typeof owner.started_identity !== "string" || typeof owner.closed !== "boolean") {
|
|
325
|
+
throw new AitermError("PARENT_DELIVERY_OWNER_INVALID: 配送所有者の形式が不正です", 2);
|
|
326
|
+
}
|
|
327
|
+
if (!owner.closed && processes.some((entry) => entry.pid === owner.pid && entry.started_identity === owner.started_identity))
|
|
328
|
+
continue;
|
|
329
|
+
for (const name of fs.readdirSync(oldDir).filter((name) => name !== "owner.json" && name.endsWith(".json"))) {
|
|
330
|
+
const oldFile = path.join(oldDir, name);
|
|
331
|
+
const file = path.join(this.ownerDir, name);
|
|
332
|
+
// 元のpathが消えるrenameを使うため、同じ旧recordを二つのprocessで回収できない。
|
|
333
|
+
try {
|
|
334
|
+
fs.renameSync(oldFile, file);
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
if (error.code === "ENOENT")
|
|
338
|
+
continue;
|
|
339
|
+
throw error;
|
|
340
|
+
}
|
|
341
|
+
const record = readRecord(file);
|
|
342
|
+
const job = { record, file, controller: new AbortController() };
|
|
343
|
+
this.jobs.set(record.delivery_id, job);
|
|
344
|
+
if (record.state === "sending") {
|
|
345
|
+
record.state = "unknown";
|
|
346
|
+
record.error = "PARENT_DELIVERY_INTERRUPTED: キュー送信中にMCP processが終了しました。自動再送はしていません";
|
|
347
|
+
this.finish(job);
|
|
348
|
+
}
|
|
349
|
+
else if (record.state === "waiting" || record.state === "ready") {
|
|
350
|
+
try {
|
|
351
|
+
await this.deps.verify(record.parent);
|
|
352
|
+
if (record.state === "waiting")
|
|
353
|
+
this.watch(job);
|
|
354
|
+
else
|
|
355
|
+
job.delivery = this.deliver(job).catch((error) => this.reportServiceError(error));
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
record.state = "failed";
|
|
359
|
+
record.error = error instanceof Error ? error.message : String(error);
|
|
360
|
+
this.finish(job);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else
|
|
364
|
+
this.finish(job);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
async close() {
|
|
369
|
+
this.closing = true;
|
|
370
|
+
clearInterval(this.timer);
|
|
371
|
+
await this.recovering;
|
|
372
|
+
await Promise.allSettled(this.registering.values());
|
|
373
|
+
const jobs = [...this.jobs.values()];
|
|
374
|
+
for (const job of jobs)
|
|
375
|
+
if (job.record.state === "waiting")
|
|
376
|
+
job.controller.abort();
|
|
377
|
+
await Promise.all(jobs.map((job) => job.task));
|
|
378
|
+
await Promise.all(jobs.map((job) => job.delivery));
|
|
379
|
+
this.owner.closed = true;
|
|
380
|
+
writeJson0600(path.join(this.ownerDir, "owner.json"), this.owner);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -71,6 +71,17 @@ export function configureIntegrations(home, registration, run = runSetupCommand,
|
|
|
71
71
|
mergeJsonMcp(file, client === "claude" ? { type: "stdio", ...registration } : registration);
|
|
72
72
|
}
|
|
73
73
|
else if (client === "codex") {
|
|
74
|
+
// 親threadがないsetupでは公式queue入口まで確認し、宛先は各dispatchで検証する。
|
|
75
|
+
let queueHelp;
|
|
76
|
+
try {
|
|
77
|
+
queueHelp = run(executable, ["queue", "--help"]);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new SetupError("codex_parent_delivery_unavailable", "Codexの公式受信キューを使えません。公式CLIを更新してください");
|
|
81
|
+
}
|
|
82
|
+
if (!queueHelp.includes("--thread") || !queueHelp.includes("--message")) {
|
|
83
|
+
throw new SetupError("codex_parent_delivery_unavailable", "Codexの公式受信キューを確認できません。公式CLIを更新してください");
|
|
84
|
+
}
|
|
74
85
|
const servers = JSON.parse(run(executable, ["mcp", "list", "--json"]));
|
|
75
86
|
if (!Array.isArray(servers))
|
|
76
87
|
throw new SetupError("config_readback_failed", "CodexのMCP一覧形式を確認できません");
|
package/docs/00_overview.md
CHANGED
|
@@ -17,6 +17,10 @@ dotagentsは任意の工場統合を担うが、Aitermの製品正典や実行
|
|
|
17
17
|
Grok/Composerのsandbox起動拒否については、[DESIGNの失敗と復旧](DESIGN.md#failure-and-recovery)に
|
|
18
18
|
検出の所有と適用範囲、[RELEASEの公開後smoke](RELEASE.md#公開後smoke)に検証条件を置く。
|
|
19
19
|
|
|
20
|
+
## 設計中
|
|
21
|
+
|
|
22
|
+
- [子の回答を親へ自動配送する設計・実装計画](https://github.com/kitepon/aiterm-mcp/blob/main/docs/plan_parent-result-delivery.md): Claude Code/Codex親への本文配送。設計案であり、現行APIは未変更。
|
|
23
|
+
|
|
20
24
|
## 履歴と証拠
|
|
21
25
|
|
|
22
26
|
- [`archive/`](https://github.com/kitepon/aiterm-mcp/tree/main/docs/archive): 完了・棄却・中断・失効・置換により現行制御から外れたsnapshot。
|
package/docs/DESIGN.md
CHANGED
|
@@ -47,14 +47,37 @@ harnessがagent loop、認証、hook、session、transcript、model catalogを
|
|
|
47
47
|
project/user環境を置換せず、launch相関と完了回収に必要なstateだけを加える。
|
|
48
48
|
Throughlineの補足記憶はpathを透過搬送するだけで、内容、project束縛、context予算はThroughlineが所有する。
|
|
49
49
|
|
|
50
|
-
agent turnは常に非ブロックdispatch
|
|
51
|
-
|
|
50
|
+
agent turnは常に非ブロックdispatchであり、receiptの`event_cursor`がturn境界になる。
|
|
51
|
+
Codex親にはAitermのMCP processが完了を観測し、回答本文を公式受信キューへ自動配送する。
|
|
52
|
+
それ以外の親には`wait_process`がplatform nativeな別process起動情報を返す。
|
|
53
|
+
waiterは純readerで、親のforeground turnを塞がない。
|
|
52
54
|
回答はharness所有transcriptから同じturnへ相関して回収し、欠落・曖昧・timeout時にpromptを再送しない。
|
|
53
55
|
Grok/Composerの記録先はCLIと同じOS絶対パスへcwdを正規化して導出し、完了通知と回答で同じ関数を使う。
|
|
54
56
|
`agent_steer`は実行中のCodex/Grok turnへ追加textを差し込み、idleなら送信せず状態を返す。
|
|
55
57
|
Cursorのsubmitはadapterがextended keyboard protocolのEnterへ変換し、呼び出し側は通常のdispatchだけを使う。
|
|
56
58
|
起動直後のClaude sessionへの初回dispatchは、他harnessと同じくTUIの入力受付を確認してから貼付とEnterを送る。
|
|
57
59
|
|
|
60
|
+
### Codex親への自動配送
|
|
61
|
+
|
|
62
|
+
`src/parent-delivery.ts`が依頼の送信前に宛先と完了境界を保存し、完了観測、加工前の回答保存、配送を所有する。
|
|
63
|
+
宛先はCodexのMCP handshakeと各要求の`_meta.threadId`から取得する。modelが指定したIDや環境変数で代用しない。
|
|
64
|
+
`src/codex-parent-receiver.ts`は同じ`CODEX_HOME`の公式app-serverへstdioで接続し、`thread/read`と
|
|
65
|
+
`thread/queue/list`で宛先を確認した後、`thread/queue/add`へ本文をJSONで渡す。
|
|
66
|
+
親のload/resume、Desktop固有通信、子への送信指示、別daemonは使わない。
|
|
67
|
+
setupは公式`queue`入口を確認し、各dispatchでは実際の親threadの受入可否を確認する。
|
|
68
|
+
native sub-agentを親にした外部queue入力はCodex自身が拒否するため、子への送信前に明示errorにする。
|
|
69
|
+
|
|
70
|
+
通常結果は親の実行中turnを中断せず、親がidleになった後に処理される。
|
|
71
|
+
`parent_delivery`が配送IDと状態を返し、自動配送時の`wait_process`/`wait_command`はnullになる。
|
|
72
|
+
`pty_observe`の`parent_deliveries`で状態を確認できる。`submitted`はキュー受付済みを示し、modelの読了を意味しない。
|
|
73
|
+
次の依頼へ進める前に前回の回答を保存し、harness所有記録を後の回答と取り違えない。
|
|
74
|
+
|
|
75
|
+
配送記録と本文はAiterm stateの`parent-deliveries`へ保存する。ownerのPIDと開始識別子で生存を判定し、
|
|
76
|
+
再接続後は終了したownerの記録だけを原子的に引き継ぐ。`waiting`は同じ境界から観測を再開し、
|
|
77
|
+
`ready`は保存した本文を送る。送信中断は`unknown`として本文を残し、自動再送しない。
|
|
78
|
+
受信口が明示拒否した場合は`failed`、子の異常終了はそのoutcomeを配送する。
|
|
79
|
+
このstateは既存のPTY/harness stateと独立し、旧版は配送を再開しない。
|
|
80
|
+
|
|
58
81
|
`trust_project:true`は対象projectの既知のworkspace、hooks、MCP初期同意を起動準備として進める意図である。
|
|
59
82
|
promptなしでも入力受付とharness生存を確認して`startup.ready`を返す。指定なしのpromptなし起動は
|
|
60
83
|
従来どおり`startup.not_checked`で返す。初手receiptは未要求・未送信・送信済み未確認・開始確認を分ける。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aiterm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
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": [
|